Skip to content

Instantly share code, notes, and snippets.

@ocadaruma
Last active July 6, 2023 22:08
Show Gist options
  • Save ocadaruma/f5f5b17952683941f23a1ff420ffb873 to your computer and use it in GitHub Desktop.
Save ocadaruma/f5f5b17952683941f23a1ff420ffb873 to your computer and use it in GitHub Desktop.
# A script to record and dump midi notes as a stringified Sonic Pi note-array
# which is ready to be played by `play RECORDED.tick; sleep INTERVAL`.
# Usage:
# - 1. Connect midi device and adjust `midi_key` in the config
# - 2. Run the script
# - 3. After the count_in (4 clicks by default), play a phrase you want
# - 4. Stop the script and check the log pane.
# * Note array is logged every measure.
# - 5. Copy & paste the logged array in another editor buffer and play it
# * By default, the resolution is 16th notes (tick_per_beat = 4)
# * Hence, to replay same phrase, you can just run live_loop with `play RECORDED.tick; sleep 0.25`
configs = {
bpm: 60,
count_in: 4,
beat_per_measure: 4,
tick_per_beat: 4,
midi_key: "/midi:microkey-25_keyboard:1/note_on",
}
###########
# helpers #
###########
define :note_to_sym do |n|
info = note_info(n)
"#{info.pitch_class}#{info.octave}".to_sym
end
# Get all past events for the time state key.
# We call private variable directly since currently SonicPi doesn't provide
# an API to do equivalent thing.
# Tested on v4.4.0
# refs: https://github.com/sonic-pi-net/sonic-pi/blob/v4.4.0/app/server/ruby/lib/sonicpi/event_history.rb#L30
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
##############
# initialize #
##############
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]
#########
# Loops #
#########
# The loop which is responsible for playing metronome and
# dump played midi notes every measure.
in_thread name: :metronome do
# to make event-timestamp consistent with midi thread
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
# dump every measure
if look > 0 && look % configs[:tick_per_measure] == 0
metronome_ticks = get_events "/metronome"
notes = get_events configs[:midi_key]
dump = []
# Scale by rt(1) to take bpm into account of tick<->note matching
half = configs[:metronome_interval] / 2.0 / rt(1)
# events are ordered from newer to older
# https://github.com/sonic-pi-net/sonic-pi/blob/v4.4.0/app/server/ruby/lib/sonicpi/event_history.rb#L389C2-L389C2
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment