MIDI clock out: Separate process for better stability

Something fun: Sending MIDI clock sync messages from a separate sclang process (to avoid timing glitches in case the interpreter blocks for a little bit too long).

https://sccode.org/1-5fy

Here, I’m using this utility to sync my friend’s Digitakt (Christmas Eve live jam). We were running like this for a bit more than an hour, never dropped sync.

hjh

10 Likes

Thanks @jamshark70!

As of TidalCycles version 1.9.0 - Announcements - Tidal Club, Tidal uses Ableton Link for its scheduling. I had a plan to synchronize Tidal and SuperCollider over Link and to use MIDIClockOut from crucial library. However, I didn’t take into account that its implementation does not have a quantized start. What I instead did was to take inspiration from your code and I created this class:

LinkToMidiClock {
	var <midiOut, <linkClock, routine, <isPlaying = false, d;

	*new { arg midiOut, linkClock;
		^super.newCopyArgs(midiOut, linkClock)
	}

	start {
		if(isPlaying,{
			"Can't start. LinkToMidiClock is already playing".inform;
		},{
			isPlaying = true;
			d = 1/24;
			routine = Routine {
				midiOut.start;
				loop {
					23.do { |i|
						midiOut.midiClock;
						d.wait;
					};
					midiOut.midiClock;
					(thisThread.clock.beats.ceil - thisThread.beats).wait;
				}
			}.play(~clock, [linkClock.quantum, 0]);
		});
	}

	stop {
		if(isPlaying,{
			isPlaying = false;
			midiOut.stop;
			routine.stop;
		},{
			"Can't stop. LinkToMidiClock is not playing".inform;
		})
	}
}

Here’s some example usage:

MIDIClient.init;
~jdxi = MIDIOut.newByName("JD-Xi", "JD-Xi");
~clock = LinkClock.new.latency_(0.1);
~jdxiclock = LinkToMidiClock(~jdxi, ~clock);
~jdxiclock.start;
~jdxiclock.stop;

I was surprised to see that changing the MIDIOut latency does not have an effect for me - could this be a Windows bug in SuperCollider? Since MIDIOut latency had no effect, this example tweaks the timing of the system by changing the LinkClock latency.

I looked at the source code, and it looks to me like it could be a bug.

We support MIDI in three platforms:

  • macOS: “late” (latency) is a parameter used when adding the MIDI message to a MIDIPacketList (C++ CoreMIDI object, you won’t find this in the class library).
  • Linux: “late” is a parameter for the ALSA MIDI function sendEvent().
  • Windows: The message is sent by the PortMIDI function Pm_WriteShort(). This function accepts a timestamp. Presumably this should be calculated from the current time + latency, but our code does not do that.

The argument in favor of considering it a bug is that, just because it’s harder to handle latency in PortMIDI doesn’t make it OK to not do it.

I’m not in a position to fix it, though. If anyone is interested and has C++ chops, look at prSendMIDIOut() at SC_PortMIDI.cpp line 741. Here, at the end of the function, we should determine what is the PortMIDI timestamp for “now” (I don’t have details on how to do that, you’d have to read PortMIDI references by yourself), then, where the Pm_WriteShort() call specifies 0 as the timestamp, instead do now + late (I guess).

hjh

1 Like