Bad timing w/ Logic Pro playing SC via MIDI

Hello all,

I’m trying to play a Supercollider synth from a Logic Pro sequence using MIDI on MacOS. To start, I’m using Fieldsteel’s Companion Code 7.1, which implements a generic SynthDef with MIDIdef for noteOn and a MIDIdef for noteOff. I can get the sequence to play, but the timing is obviously bad. My sequence is straight quantized 8th notes, but I can hear SC kinda speeding up, slowing down, rushing a few notes, etc. When the sequence loops, the imperfections fall at different places. Terrible. HOW CAN I GET ROCK SOLID MIDI TIMING BETWEEN LOGIC PRO AND SC ON A MAC?

I verified the sequence is OK by playing an internal software instrument in Logic Pro and it was perfectly clocked.

Thank you for any help or clues or possibilities!

Fieldsteel’s code:

(
SynthDef(\tone, {
	arg buf = 0, gate = 1, amp = 0.2,
	freq = 220, rel = 0.3, out = 0;
	var sig, env;
	env = Env.asr(0.002, 1, rel).kr(2, gate);
	sig = LFTri.ar(freq * [0, 0.1].midiratio);
	sig = sig * env * amp;
	Out.ar(out, sig);
}).add;
)
(
~bend = 0;
~group = Group();
~notes = Array.newClear(128, nil);
MIDIdef.freeAll;

MIDIdef.noteOn(\on, {
	|val, num, chan, src|
	~notes.put(
		num,
		Synth(\tone, [
			\freq, num.midicps,
			\gate, 1,
			\amp, val.linexp(0, 127, 0.01, 0.25),
			\bend, ~bend
		], ~group);
	);
}).permanent_(true);

MIDIdef.noteOff(\off, {
	arg val, num, chan, src;
	~notes[num].set(\gate, 0);
	~notes.put(num, nil);
}).permanent_(true);

MIDIdef.bend(\pitchbend, {
	arg val, chan, src;
	~bend = val.linlin(0, 16383, -2, 2);
	~group.set(\bend, ~bend);
}).permanent_(true);
)

MacBook Pro, Apple M1 Max, 64G ram, Sonoma 14.6.1
SC 3.13.0
Logic Pro 11.0.01

1 Like

Probably can’t. General-purpose computers simply cannot achieve the same level of low latency as a hardware synthesizer.

The messages from Logic go through:

  • MIDI protocol. These may or may not have timing information attached, and SC may or may not use that timing information (not sure).
  • SC lang → server messaging: First up is network jitter. This should be less than a millisecond, but it isn’t zero. Then, “as soon as possible” messages will fall on the next audio hardware driver block boundary. You can get more accuracy by reducing the audio driver’s buffer size to, oh, 64 or 128 samples, at the expense of higher CPU use. 512 or 1024 samples will not achieve musically useful timing.

You might get a better result by syncing SC to Logic using Ableton Link (SC: LinkClock), and running the note sequence in sclang.

I verified the sequence is OK by playing an internal software instrument in Logic Pro and it was perfectly clocked.

Unfortunately that’s not an apples-to-apples comparison. DAW communication into plug-ins includes internal timing information, which is lost in outgoing MIDI. The plug-in uses the timestamps to perform the audio on time.

hjh

1 Like

Your problems might stem from Logic Pro and not SC. Logic’s midi implementation has always been problematic and still is. If I send one noteOn and noteOff from Logic using the IAC bus I get a ton of noteOns and NoteOffs in SC. Try outputting from Logic Pro Virtual Out instead of using the IAC bus or use a different software than Logic, like Ableton Live or something which handles the IAC bus correctly.

1 Like

JACK MIDI tries to help with some of those issues: Each MIDI event has a timestamp that says which audio sample it should line up within the buffer. So, if you’re working with 20ms chunks of audio samples, you also get 20ms worth of MIDI events with information about when they should happen in this grid. That’s a sample-accurate precision with the same latency as jack audio, at least on the delivery side. In theory, it can have good results if both sides are tuned.

SCLang’s processing remains a potential timing issue, which can still be jittery if it does not get this information right and fit it in its own latency.

(EDIT: I’m unsure if sclang will guarantee all events have maximum jitter within the audio buffer as mentioned. I would double-check if that is an optimistic assumption since other threads will be up to priorities and scheduling. If I’m not causing some confusion here (I’m probably with something else in mind right now), please someone correct me)

I would be glad to hear about some experiences regarding similar setups.

EDIT2: For context, a comment by Paul Davis, JACK original author:

JACK MIDI is internal to JACK, is entirely synchronous (messages generated by client A are given to client B during the same process callback) and delay free unless you have signal feedback loops. It is much less like any native MIDI system (ALSA on Linux, CoreAudio on OS X etc), and more like a highly customized messaging system designed specifically for low latency, real time music creation … which of course is precisely what it is.

I think the actual solution is to schedule the messages in advance. Since @Ray wants to play a pre-made sequence – and not a live instrument – latency shouldn’t be a concern. In the MIDIDefs, you can wrap everything in s.bind {} calls so all Server messages will be scheduled with the default Server latency.

However, you might still have the problem that the actual MIDI messages might not be timestamped by Logic resp. sclang’s MIDI backend might just ignore the timestamps. Actually, you can test Logic’s MIDI timing by connecting it to another DAW (e.g. REAPER) and check if the sequence is received with the correct relative timing between notes.

1 Like

Thanks everyone for the thoughts and perspectives! @Spacechild1 called it, s.bind{} totally works! Important that I’m not trying to do this stuff live - the latency would be a killer, but after compensating for the delay in Logic, the timing is ROCK SOLID and definitely worth ALL CAPS!! I’m so happy! Details on my solution:

  1. I put s.bind{} into my MIDIdef functions (I copied the code below - please let me know if there is better place to put them - although it is working flawlessly as is)
  2. There is a delay on the performance that corresponds with my Server.local.latency which happens to be the default = 0.2s = 200ms
  3. I went into the Logic Pro External MIDI Track that contains the sequence and gave it a -200ms Delay to compensate for SC latency
  4. Doesn’t seem to matter if I use “IAC Driver Bus 1” or “Logic Pro Virtual Out” - it’s rock solid either way
  5. code:
(
SynthDef(\tone, {
	arg buf = 0, gate = 1, amp = 0.2,
	freq = 220, rel = 0.3, out = 0;
	var sig, env;
	env = Env.asr(0.002, 1, rel).kr(2, gate);
	sig = LFTri.ar(freq * [0, 0.1].midiratio);
	sig = sig * env * amp;
	Out.ar(out, sig);
}).add;
)
(
~bend = 0;
~group = Group();
~notes = Array.newClear(128, nil);
MIDIdef.freeAll;

MIDIdef.noteOn(\on, {
	|val, num, chan, src|
	s.bind{
		~notes.put(
			num,
			Synth(\tone, [
				\freq, num.midicps,
				\gate, 1,
				\amp, val.linexp(0, 127, 0.01, 0.25),
				\bend, ~bend
			], ~group);
		);
	};
}).permanent_(true);

MIDIdef.noteOff(\off, {
	arg val, num, chan, src;
	s.bind{
		~notes[num].set(\gate, 0);
		~notes.put(num, nil);
	};
}).permanent_(true);

MIDIdef.bend(\pitchbend, {
	arg val, chan, src;
	s.bind{
		~bend = val.linlin(0, 16383, -2, 2);
		~group.set(\bend, ~bend);
	};
}).permanent_(true);
)
3 Likes

Great!

Note that the default latency is rather high. You can try to lower it until you get warnings about “late” OSC messages. That being said, it’s probably ok to just keep the default latency since you need to compensate it anyway.

Putting my computer in airplane mode also seems to help keep any late notes to a minimum