Midi learn for Sonic Pi

Lately i’v been playing more with hardware synths (got a virus ti2 for myself as a birthday present) and thought that midi learn functionality for recording knob movements would be fun thing for Ziffers and Sonic Pi.

Here’s a simple learn method you can use to listen midi CC or Sysex messages from your hardware or vst synths and playback the recorded result:

# Learn method for MIDI CC or SYSEX messages. Listens to given sync port and sends backs the recorded result.
define :learn do |**opts|
  
  knob = opts[:knob]
  raise "No knob name given!" if !knob
  knob = knob.to_s
  sync_path = opts[:sync]
  raise "No sync path given!" if !sync_path
  port = opts[:port]
  raise "No port given!" if !port
  loop = opts[:loop] # Sync to this if given
  channel = opts[:channel] || 1
  length = opts[:length] || 4
  halt = opts[:stop] || false
  resolution = opts[:resolution] || 0.05 # Default sleep and recording "resolution"
  
  set :event_time, 0
  set :event_last_time, 0
  set (knob+"_recording").to_sym, false
  set (knob+"_event_list").to_sym, []
  
  # Timer loop for counting beats
  live_loop knob+"_timer" do
    use_real_time
    stop if halt
    cur_time = get(:event_time)
    if cur_time>=length
      # Restart counters and stop
      set :event_time, 0
      set :event_last_time, 0
      set (knob+"_recording").to_sym, false
      events = get((knob+"_event_list").to_sym)
      print "Recorded events:"
      print events
      stop
    end
    # Wait for first event before starting counter
    if get((knob+"_recording").to_sym)
      print "REC "+knob+": "+cur_time.to_s
      set :event_time, cur_time + resolution
    end
    sleep resolution
  end
  
  # Midi learn loop for recording events (CC or SYSEX)
  live_loop knob+"_recorder" do
    use_real_time
    stop if halt
    cur_time = get(:event_time)
    stop if cur_time>=length
    
    sync_data = sync sync_path
    events = get((knob+"_event_list").to_sym)
    
    # Start recording with first event
    if events.length==0
      cur_time = 0
      set :event_time, 0
      set (knob+"_recording").to_sym, true
    end
    
    last_time = get :event_last_time
    set :event_last_time, cur_time
    difference = cur_time-last_time
    events = events+[[sync_data,difference<=0 ? resolution : difference ]]
    set (knob+"_event_list").to_sym, events
    sleep resolution
  end
  
  # Event playback loop
  live_loop knob+"_playback", delay: length do
    sync loop if loop
    use_real_time
    stop if halt
    recording = get((knob+"_recording").to_sym)
    if !recording # Wait until recording stops
      event_list = get((knob+"_event_list").to_sym)
      event = event_list.ring.tick
      if event then
        if sync_path.end_with? "/control_change"
          midi_cc *event[0], channel: channel, port: port
        elsif sync_path.end_with? "/sysex"
          midi_sysex *event[0], port: port
        else
          raise "Invalid sync path: "+sync_path
        end
        sleep event[1]
      else
        sleep resolution
      end
    else
      sleep resolution
    end
  end
  
end

Example using knobs with sysex:

learn knob: "vol", sync: "/midi:2-_virus_ti_synth_5/sysex", port: "2-_virus_ti_synth_6"

To use the learn method, wiggle some knobs first and Sonic Pi can then autosuggest correct midi out path for the knobs. Knobs will either send CC or Sysex messages, which you can see in the Cues log. Recording will start when you run the method and start turning the knobs.

Example using knobs with cc:

learn knob: "cutoffs", sync: "/midi:2-_virus_ti_synth_5/control_change", channel: 2, port: "2-_virus_ti_synth_6"

You can also stop the recorded cc/sysex loop using stop parameter:

learn knob: "cutoffs", sync: "/midi:2-_virus_ti_synth_5/control_change", channel: 2, port: "2-_virus_ti_synth_6", stop: true

You can also record multiple knobs at the same time or run multiple learn loops. When playing out with multiple learn methods commenting out the running loops is a good way to keep things running smoothly. When learn method is runned again it automatically stops the running loop and starts recording again.

Syncing the CC or Sysex loop to the melody is not the easiest thing to do and there may be some room to improve there. Current default for recording “resolution” is 0.05, which can be changed using resolution parameter. You can also adjust the length of the record using length parameter and optionally sync to some existing loop:

learn knob: "cutoffs", sync: "/midi:2-_virus_ti_synth_5/control_change", channel: 2, port: "2-_virus_ti_synth_6", resolution: 0.1, length: 6, loop: :my_loop_to_sync

That’s about it. Have fun tweaking those knobs.

4 Likes