Last active
July 6, 2023 22:08
-
-
Save ocadaruma/f5f5b17952683941f23a1ff420ffb873 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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