Yet another MIDI recorder which dumps notes array readily playable by ticking

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
1 Like