Imperfection of language-based timing


#1

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


#2

Here are the results of running this experiment on my laptop, sc 3.10-beta on arch linux 64bit, kernel 4.18.5-arch1-1-ARCH, with jack using 1024 samples buffer size and 48000 Hz sampling rate.

On my system instead, I get:

occurences of sample deltas with single synth:
Set[ [ 4800, 600 ] ]

occurences of sample deltas with Pbind:
Set[ [ 4800, 600 ] ]

Which looks quite ok to me.

Gives:
-> 7001
-> 7001


#3

Thanks for the check, that’s very interesting !
So, if I got it right, you are reporting sample-accuracy with language based RT-timing ?
I’m very surprised that this is the case.

On my OSX tests the calibration happened every 10-20 secs or so, it might be that there are larger calibration intervals on linux. So you could check with e.g. t = 200 or 300 instead of 60.

BTW with 48000 Hz on OSX and built-in audio standard driver blocksize 512 I get similar results as with 44100 Hz

occurences of sample deltas with single synth: 
Set[ [ 4800, 600 ] ]

occurences of sample deltas with Pbind: 
Set[ [ 4834, 1 ], [ 4800, 592 ], [ 4833, 2 ], [ 4799, 2 ], [ 4767, 3 ] ]

#4

With t=300, things don’t look as rosy anymore…

occurences of sample deltas with single synth:
Set[ [ 4800, 600 ] ]

occurences of sample deltas with Pbind:
Set[ [ 4810, 42 ], [ 4762, 4 ], [ 4803, 93 ], [ 4794, 77 ], [ 4756, 1 ], [ 4815, 11 ], [ 4792, 48 ], [ 4822, 36 ], [ 4827, 13 ], [ 4771, 3 ], [ 4770, 7 ], [ 4817, 13 ], [ 4808, 75 ], [ 4764, 7 ], [ 4834, 3 ], [ 4755, 3 ], [ 4765, 7 ], [ 4837, 4 ], [ 4818, 15 ], [ 4841, 3 ], [ 4790, 26 ], [ 4820, 26 ], [ 4782, 47 ], [ 4833, 8 ], [ 4838, 3 ], [ 4786, 29 ], [ 4816, 18 ], [ 4780, 40 ], [ 4775, 9 ], [ 4813, 28 ], [ 4789, 30 ], [ 4821, 19 ], [ 4832, 8 ], [ 4781, 40 ], [ 4814, 14 ], [ 4828, 19 ], [ 4757, 2 ], [ 47…etc…


#5

Thanks for rechecking, that looks rather normal (unfortunately)

Ah, yes, for the single synth, duration would have to be replaced too, but the result would be exact anyway (probably)

The best we can do in RT is checking different combinations of samplerate, hardware (driver) and hardware driver buffer size to minimize deviations. IIRC with my setup a buffer size of 512 gave the best results.


#6

You should write SynthDef(...).add, otherwise the parameters like duration are not sent from the pattern.


#7

With .add and t=300 the image is not very different

-> a Function

occurences of sample deltas with single synth:
Set[ [ 4800, 600 ] ]

occurences of sample deltas with Pbind:
Set[ [ 4832, 10 ], [ 4762, 8 ], [ 4801, 139 ], [ 4753, 2 ], [ 4824, 38 ], [ 4834, 6 ], [ 4813, 38 ], [ 4785, 39 ], [ 4759, 2 ], [ 4774, 15 ], [ 4817, 20 ], [ 4755, 1 ], [ 4841, 2 ], [ 4831, 7 ], [ 4770, 9 ], [ 4788, 38 ], [ 4758, 1 ], [ 4805, 89 ], [ 4792, 46 ], [ 4833, 4 ], [ 4793, 50 ], [ 4823, 25 ], [ 4816, 33 ], [ 4795, 73 ], [ 4806, 84 ], [ 4775, 11 ], [ 4830, 14 ], [ 4803, 95 ], [ 4781, 39 ], [ 4778, 25 ], [ 4780, 26 ], [ 4827, 19 ], [ 4768, 6 ], [ 4765, 8 ], [ 4783, 54 ], [ 4842, 1 ], [ 4822, 24 ], […etc…


#8

aha, in this case it probably makes no difference.


#9

‘store’ is needed for the NRT part of the example and is generally ok with patterns, not only in this case,
from the help: “Write the defFile and store it in the SynthDescLib specified by libname”.

So duration as extra arg is passed correctly:

SynthDef(\diracs, { |out = 0, duration = 60, amp = 0.1|
    OffsetOut.ar(out, [Impulse.ar(10), DC.ar(0)] * 
         EnvGen.ar(Env([amp, amp, 0], [duration + 0.01, 0]), doneAction: 2))
}).store;

p = Pbind(\instrument, \diracs, \duration, 0.5, \dur, Pn(1, 1), \amp, 0.3).play

Adding Multiple Synths to a NRT Score