Re: supernova sample-based scheduling bug?
[email protected] Sat, 22 May 2021 17:30:38 +0200
| Newsgroups | gmane.comp.audio.supercollider.user |
|---|---|
| Message-ID | <[email protected]> |
> Am 10.05.2021 um 15:31 schrieb [email protected]: > > The thing is, even if Supernova is using the sample clack, the sclang client always uses the NTP system time. I think, true sample accurate scheduling only works between bundles scheduled at the same logical time in the client. > I’ve done some testing and cannot exactly confirm this. There seems to be no difference between using language-based timing and indirect timing via latency. There is a difference though between supernova and scsynth. On the flip side, compared to my original testing (https://scsynth.org/t/imperfection-of-language-based-timing/350), the bespoken issue seems to have dramatically improved. Now I have tested on OS 10.15.7 with 3.11.2 – no idea what caused the change. These awful jumps (in the size of dozens of samples) that happened every 10-20 seconds or so in earlier tests (which I only performed in scsynth though) seem to have completely disappeared. This has a nice consequence: some variants of timing-sensitive pattern-based granular synthesis that produced unsatisfying discontinuities in realtime for the bespoken reasons are probably working fine now, for the extremely sensitive cases supernova should do it anyway. 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 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 on Mac now). I’d be happy to hear from your results with below tests on other operating systems as well ! // 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 ] ] Greetings Daniel ----------------------------- http://daniel-mayer.at -----------------------------