This goes back to an observation of my colleague Gerhard Eckel. I posted this a while ago on the mailing list, but can’t find it anymore in the archives. As I think it’s an interesting topic I’m reposting it here again for future reference.
Timing in general is a crucial topic and practically users are mainly confronted with asynchronicity between server and language, latency, quantization, the Out/OffsetOut differences etc. Sample-accurate timing isn’t relevant in all contexts, nevertheless in certain situations it’s absolutely necessary.
Starting point: you can have sample-accurate timing with synths, but not with language based sequencing in RT. The reason for this is a kind of calibration that different clocks, responsible for SC timing and RT output must perform. Frankly I am not aware of the details, there has been a lengthy discussion and there was no consensus if this inaccuracy could be circumvented at all. Practically a workaround is the usage of NRT synthesis, therewith you can get sample-accuracy also with patterns, this is shown in the second part of the example.
The following results come from SC 3.9.3 on OSX with built-in audio and standard driver blocksize 512 (samplerate 44100), other settings might even produce more deviations.
Note that SynthDef ‘dirac’ already uses OffsetOut, so this inaccuracy is independant from the rough one you get with Out alone – see the OffsetOut helpfile for the reason for this (so for lang-based timing with short durations always use OffsetOut).
(
s = Server.local;
Server.default = s;
s.boot;
)
(
// use store for later NRT usage,
// define with dummy channel here for use with pattern recording,
// play reference pulse train of synth to channel 0, "real" Pbind pulse train to channel 1
// we need an out arg anyway for aPattern.record
SynthDef(\diracs, { |out = 0, duration = 60|
OffsetOut.ar(out, [Impulse.ar(10), DC.ar(0)] * EnvGen.ar(Env([1, 1, 0], [duration + 0.01, 0]), doneAction: 2))
}).store;
SynthDef(\dirac, { |out = 0|
OffsetOut.ar(out, [DC.ar(0), FreeSelf.kr(Impulse.ar(1))])
}).store;
// Synth as Pbind for pattern recording, needs legato = 1 to get equal duration
p = Pbind(\instrument, \diracs, \duration, 60);
q = Pbind(\instrument, \dirac, \dur, 0.1);
)
(
// record 60 sec of both pulse trains in realtime to compare,
// wait until finishing confirmed in post window
// mute to avoid playing hard clicks to speakers
c = TempoClock.();
t = 60;
s.volume.mute;
~date = Date.getDate.stamp;
~fileName = "diracsRT_synthL_patR" ++ "_" ++ ~date ++ ".aiff";
// Reference pulse train (Pbind with one synth) needs to know overall duration
r = Ppar([p <> Pbind(\dur, Pn(t, 1)), q]);
r.record(~fileName, clock: c, dur: t + 0.05);
// need CmdPeriod here as record doesn't stop (broken)
c.sched(t + 3, { CmdPeriod.run; s.volume.unmute });
)
(
// helper function for index search
~indicesOfEqualWithPrecision = { |seq, item, prec = 0.0001|
var indices;
seq.do { |val, i|
if (item.equalWithPrecision(val, prec)) { indices = indices.add(i) }
};
indices
};
// analysing function
~analyse = { |fileName|
~soundFile = SoundFile.openRead(fileName);
~data = FloatArray.fill(~soundFile.numFrames * ~soundFile.numChannels, 0);
~soundFile.readData(~data);
~soundFile.close;
~stereo = ~data.clump(2).flop;
~dataL = ~stereo[0];
~dataR = ~stereo[1];
// get indices of diracs
~leftIndices = ~indicesOfEqualWithPrecision.(~dataL, 1);
~rightIndices = ~indicesOfEqualWithPrecision.(~dataR, 1);
// look at deltas
~leftDeltas = ~leftIndices.differentiate.drop(1);
~rightDeltas = ~rightIndices.differentiate.drop(1);
// count occurences of (possibly) different deltas
~leftDeltaSet = ~leftDeltas.asSet.collect { |x| [x, ~leftDeltas.occurrencesOf(x)] };
~rightDeltaSet = ~rightDeltas.asSet.collect { |x| [x, ~rightDeltas.occurrencesOf(x)] };
"".postln;
"occurences of sample deltas with single synth: ".postln;
~leftDeltaSet.postln;"".postln;
"occurences of sample deltas with Pbind: ".postln;
~rightDeltaSet.postln;"".postln;
};
)
// analyse, takes some seconds
~analyse.(~fileName)
->
occurences of sample deltas with single synth:
Set[ [ 4410, 600 ] ]
occurences of sample deltas with Pbind:
Set[ [ 4446, 3 ], [ 4374, 3 ], [ 4410, 594 ] ]
// verify equal start
~leftIndices[0]
~rightIndices[0]
/////
(
// render 60 secs of the same stereo audio generation in NRT mode
c = TempoClock.();
t = 60;
s.volume.mute;
~date = Date.getDate.stamp;
~fileName = "diracsRT_synthL_patR" ++ "_" ++ ~date ++ ".aiff";
// Reference pulse train (Pbind with one synth) needs to know overall duration
r = Ppar([p <> Pbind(\dur, Pn(t + 0.05, 1)), q]);
// to be passed to render stereo
o = ServerOptions.new.numOutputBusChannels = 2;
r.render(~fileName, t + 0.05, options: o);
)
~analyse.(~fileName)
// perfect NRT timing
->
occurences of sample deltas with single synth:
Set[ [ 4410, 600 ] ]
occurences of sample deltas with Pbind:
Set[ [ 4410, 600 ] ]
Daniel