Imperfection of language-based timing

Ultimately, a comment of James in a thread on the mailing list has shed more light into this: the vast amount of jitter seems to be produced by system action (NTP)! After a switch to OS 10.15 the large jumps in the realtime variants analyzed in the first post of this thread – and confirmed by @shiihs on Linux – have gone away! Besides, if you want to check the realtime tests, take the variants below which contain an additional path definition necessary for newer SC versions.

https://www.listarc.bham.ac.uk/lists/sc-users/msg69793.html

On older OS systems disabling ”Set date and time automatically” in system preferences should do the trick, I’m not aware of the necessary terminal commands on Linux but some googling (NTP) should help.

Besides, tests on supernova show sample-accurate timing with the original examples whereas scsynth gives maximum deviations of just one sample.

I’m very happy about this finding because it enables nice synthesis options with patterns being unreliable so far.

E.g., a pulsar stream like that didn’t sound smoothly. With new settings and max deviations of one sample this is perfectly ok now (at least for my ears). There might be cases where the supernova variant could turn out to be better, I’d have to do further tests.

// ATTENTION: with NTP you get nasty and sometimes loud bumps every some seconds

(
SynthDef(\sinePerc, { |out, freq = 400, att = 0.005, rel = 0.05, amp = 0.1|
	OffsetOut.ar(out, EnvGen.ar(Env.perc(att, rel), doneAction: 2) * SinOsc.ar(freq, 0, amp) ! 2)
}).add;
)

(
x = Pbind(
	\instrument, \sinePerc,
	\freq, 500,
	\dur, 0.005,
	\att, 0.002,
	\rel, 0.002
).play
)

x.stop

It could also be interesting for Windows users to check those issues, I have no machine available at the moment.

Results on OS 10.15.7 and SC 3.11.2 (and on OS 10.10.5 and SC 3.9.3 with disabled automatic date and time setting)

The latency-based timing variant goes back to a suggestion of Christof Ressi to take only one logical time, it doesn’t make a difference in this case, though.

scsynth language-based timing (test 1): max deviation of 1 sample
scsynth latency-based timing (test 2): max deviation of 1 sample

supernova language-based timing (test 1): sample-accurate
supernova latency-based timing (test 2): sample-accurate

There were no principle differences between 44.1 and 48 kHz. Further testing should check longer run times than one minute and other interfaces (checked with built-in out now).

// for tests with scsynth
(
Server.scsynth;
s.reboot;
)

// for tests with supernova
(
Server.supernova;
s.reboot;
)

// prepare for test 1 - language-scheduled timing
(
// 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);
)


// prepare for test 2 - latency-based timing
(
// 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
p = Pbind(\instrument, \diracs, \duration, 60);

~latency = s.latency;

// we need a finite Pattern !
q = Pbind(
	\instrument, \dirac,
	\dur, 0,
	// dur sequencing to be inserted here
	\realDur, Pn(0.1, 601),
	\latency, Pfunc { |ev|
		var latency = ~latency;
		~latency = ~latency + ev[\realDur];
		latency
	}
);
)


// run test 1 or 2

(
// 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 = Platform.recordingsDir +/+ "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)


// results of test 1 and 2 with supernova (44100 Hz):
->

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

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


// results of test 1 and 2 with scsynth (of course second set can vary)
->

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

occurences of sample deltas with Pbind:
Set[ [ 4409, 8 ], [ 4411, 52 ], [ 4410, 540 ] ]
2 Likes