Though I know there are several attempts and approaches to record midi input as Sonic Pi code, I wrote yet another one to satisfy my use-case.
What I wanted:
- Play a phrase on MIDI device and dump it as Sonic Pi notes array, which are readily playable by ticking
- With respecting timing by inserting rests appropriately
- Support recording chords
To achieve above, I needed to join ticks and played midi notes based on the timing.
I ended up with accessing private variable of EventHistory class because other approaches (e.g. store event-history as an array in time-state) didn’t work as expected.
Tested on v4.4.0.
I hope it might be useful.
gist: record_midi.rb · GitHub
configs = {
bpm: 60,
count_in: 4,
beat_per_measure: 4,
tick_per_beat: 4,
midi_key: "/midi:microkey-25_keyboard:1/note_on",
}
define :note_to_sym do |n|
info = note_info(n)
"#{info.pitch_class}#{info.octave}".to_sym
end
define :get_events do |path|
segments = path.delete_prefix("/").split("/")
node = @event_history
.instance_variable_get("@state")
.instance_variable_get("@children")
segments.each_with_index do |segment, i|
n = node[segment]
if n.nil?
return []
end
if i < segments.size - 1
node = n.instance_variable_get("@children")
else
return n.events
end
end
end
use_debug false
use_bpm configs[:bpm]
configs[:metronome_interval] = 1.0 / configs[:tick_per_beat]
configs[:tick_per_measure] = configs[:beat_per_measure] * configs[:tick_per_beat]
in_thread name: :metronome do
use_real_time
pattern = []
configs[:tick_per_beat].times do |i|
if i == 0
pattern += [:hat_zap]
else
pattern += [:hat_bdu]
end
end
configs[:count_in].times do
sample :drum_cowbell
sleep 1
end
loop do
s = pattern.tick
if look > 0 && look % configs[:tick_per_measure] == 0
metronome_ticks = get_events "/metronome"
notes = get_events configs[:midi_key]
dump = []
half = configs[:metronome_interval] / 2.0 / rt(1)
metronome_ticks[0...configs[:tick_per_measure]].each do |t|
lb, ub = t.time - half, t.time + half
chord_notes = []
# FIXME: Get rid of nested-loop
notes.each do |n|
if lb <= n.time && n.time < ub
chord_notes << (note_to_sym n.val[0])
end
end
dump << (chord_notes.any? ? chord_notes : nil)
end
puts dump.reverse
end
cue "/metronome"
sample s
sleep configs[:metronome_interval]
end
end
in_thread name: :midi_listener do
use_real_time
loop do
note, velocity = sync configs[:midi_key]
synth :piano, note: note, amp: velocity / 127.0
end
end