Syncing with an external MIDI Clock


#1

I am pretty new, and still trying to figure out how to work with external MIDI devices.

I have a KORG keyboard, which sends MIDI clock messages (but looks as empty messages in Sonic Pi). It works pretty well with other devices or software, but I can’t get it to work properly with Sonic Pi. By properly I mean to say, that I want this device to be the master Clock source.

Can anyone give me some clue as to how to properly sync w/an external MIDI clock?

This is my current unreliable solution, which is clearly not perfectly synced

define :midiClock do |port|
  in_thread do
    lap = nil
    t1 = nil
    ppqn = 24
    t = nil
    
    loop do
      sync port
      t = tick(:clock)
      isQuarterNote = (t%ppqn)==0
      
      if t1 == nil
        t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      elsif t1 != nil && lap == nil && isQuarterNote
        t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        / elapsed time in milliseconds /
        lap = (t2 - t1) * 1000
        t1 = nil
      end
      
      cue :beat
      
      if lap != nil
        if lap > 0
          pps = 1000/lap
          ppm = pps*60
          puts lap, pps, ppm
          set :midibeat, [lap, ppm]
        end
        
        lap = nil
      end
    end
  end
end

midiClock '/midi/microstation_midi_1/2/clock'

The logs show tempo is out of sync most of the times (last number is ppm), considering my external MIDI clock is fixed to 100.00

{run: 6, time: 300.0842}
 └─ 575.7103539999662 1.736984567069396 104.21907402416376
 
{run: 6, time: 300.6842}
 └─ 576.0453549992235 1.7359744181972407 104.15846509183444
 
{run: 6, time: 301.2843}
 └─ 576.4124100005574 1.7348689630034735 104.0921377802084
 
{run: 6, time: 301.8843}
 └─ 575.8693120005773 1.7365051048231537 104.19030628938923
 
{run: 6, time: 302.4843}
 └─ 576.0355580005125 1.7360039430050431 104.16023658030258
 
{run: 6, time: 303.0844}
 └─ 576.1048839995055 1.7357950397116548 104.1477023826993
 
{run: 6, time: 303.6844}
 └─ 576.0853020001377 1.735854041976167 104.15124251857002
 
{run: 6, time: 304.2844}
 └─ 575.6511149993457 1.7371633163624405 104.22979898174643
 
{run: 6, time: 304.8844}
 └─ 575.8668889993714 1.7365124112928318 104.19074467756991
 
{run: 6, time: 305.4844}
 └─ 576.2161530001322 1.735459852684433 104.12759116106598
 
{run: 6, time: 306.0845}
 └─ 576.0676230001991 1.735907313783636 104.15443882701817
 
{run: 6, time: 306.6845}
 └─ 575.8704109994142 1.7365017908534588 104.19010745120752
 
{run: 6, time: 307.2846}
 └─ 576.0708109992265 1.7358977072027741 104.15386243216645
 
{run: 6, time: 307.8846}
 └─ 576.0947459984891 1.7358255858883023 104.14953515329813
 
{run: 6, time: 308.4846}
 └─ 575.8625850012322 1.7365253899901487 104.19152339940892
 
{run: 6, time: 309.0846}
 └─ 576.0855089993129 1.7358534182487009 104.15120509492205
 
{run: 6, time: 309.6846}
 └─ 576.1413180007366 1.7356852715755433 104.1411162945326
 
{run: 6, time: 310.2846}
 └─ 576.0268280009768 1.7360302530879763 104.16181518527858

Thanks!


#2

I never imagined I’d have to do this, but I had to use R and develop a linear regression model in order to fix the timing difference.

          /(Intercept)    midi$ppm    midi$lap
            1.87219822  0.48883835 -0.00175442/
          pps = (1000/lap)
          ppm = pps*60
          tempo = 1.87219822 + (0.48883835 * ppm) + (-0.00175442*lap)

Those coefficients constants were calculated on R using a linear regression model, with all the logged data in Sonic Pi. Hopefully, this is giving me a temporary solution, but of course… this is not ideal, and probably, not widely supported.


#3

Hi there,

unfortunately you’re exploring an undeveloped area of Sonic Pi. I’m yet to put some serious thought into how to make syncing with external clock sources simple and intuitive (hence all the extra work you’re needing to do). Sorry about that - however, perhaps there is some solace in knowing that what you’re doing is tough because it’s a rarely trodden path :slight_smile:

Looking at your code, one thing I released is that you don’t appear to be using use_real_time in your code anywhere. It strikes me that you might benefit from reading the section on MIDI in from the tutorial, especially the section “Removing Latency”:

http://sonic-pi.net/tutorial.html#section-11-1

Essentially, you don’t just need to figure out the starting time and BPM values from the external events, you also need to ensure you add as little latency as possible to outgoing messages sent in sympathy with these timings. By default, Sonic Pi adds 0.5s of latency to all outgoing events - this allows it to be as well-timed as possible. However, in your case, you don’t want well-timed, you want highly-reactive as the well-timed aspect is coming from the MIDI clock.


#4

@samaaron thanks. Actually, I did read the whole tutorial and I’ve just omitted the piece of code where I call use_real_time. The tempo is set right, but I’m still not sure if I’m doing something wrong with the cue and sync calls, because while the tempo is set right, the beats are misplaced. This is the full code, if you find anything please let me know:

use_real_time
use_debug false
use_cue_logging false

require 'csv'

define :midiClock do |port|
  in_thread do
    lap = nil
    t1 = nil
    ppqn = 24
    t = nil
    
    /CSV.open('midi.csv', 'a+') do |csv|/
    loop do
      sync port
      t = tick(:clock)
      isQuarterNote = (t%ppqn)==0
      
      if t1 == nil
        t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      elsif t1 != nil && lap == nil && isQuarterNote
        t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        / elapsed time in milliseconds /
        lap = (t2 - t1) * 1000
        t1 = nil
      end
      
      if lap != nil
        if lap > 0
          /(Intercept)    midi$ppm    midi$lap
            1.87219822  0.48883835 -0.00175442 
            > coeffs[1] + coeffs[2]*263.5005+coeffs[3] * 227.7035/
          pps = (1000/lap)
          ppm = pps*60
          tempo = 1.87219822 + (0.48883835 * ppm) + (-0.00175442*lap)
          /csv << [lap, pps, ppm, '112']/
          set :midibeat, [lap, tempo]
          puts lap, tempo
          cue :beat
        end
        
        lap = nil
      end
    end
  end
  /end/
end

midiClock '/midi/microstation_midi_1/1/clock'

with_fx :reverb, room: 0.75,damp: 0.25 do
  live_audio :ms, stereo: true, input: 1
end

live_loop :tmp do
  sync "/cue/beat"
  lap, tempo = get[:midibeat]
  use_bpm tempo
  
  puts [lap, tempo]
  
  sample :bd_808
  
  sleep 1
end


#5

@webpolis - out of interest which OS are you running and what is the audio latency like?

If you run the following code:

live_audio :foo

(Assuming you have a mic set up as input 1 on your laptop or external sound card)

and then clap - do you hear a delay between your real clap and the clap coming out of the speakers?


#6

I’m also interested in this topic. I’m not sure if this is at all useful in this context, but another strategy for locking up that is used aomw commercial sequencers is to let the slave chase the incoming clock, jiggling the slave clock based on the gap between the last two master clock ticks and accepting that you won’t sync up right away as a result. On each tick the last master gap is analyzed and we move closer to in sync. In my experience with other systems, this resulted in the most robust sync at the expense of not being able to change tempo quickly, but to be honest, the 2 to 4 bars of not quite in sync were not really noticeable in a live situation, just the initial sync up was rough. In my case, we dealt with this by having everything parked on a starting loop that was playing silently and just un-muting stuff on the top for the actual musical start of performance. Now, having not yet dug into the SP clock code yet, I could be saying things of no help whatsoever though… (this was all in Csound)