(See below james added explanation)
From the issue #4580 I was most interested in server clock errors.
I did a measuring in Lua2SC similar to #4580 on linux and win32 for scsynth and supernova:
In win32 portaudio both scsynth and supernova have a very little variation from the intended sample interval between onsets, intended sample interval was 441 and I got minimum: 440 and maximum 442
-- minimum, maximum, mean, standard deviation
In win32 jack scsynth: 362 495 441.02252252252 13.226655278567
In linux supernova behaves as in win32 but scsynth gives wild variations from 408 to 455
mean 440.97895791583 standard deviation 10.14817789853
supernova systemclock numbers 440 442 441.04809619238 0.3493673645775
supernova sampleclock numbers: 441 441 441 0
The conclusion would be that SC_Jack has some fails leading to big standard deviation in win32 and linux
code used:
local s = require"sclua.Server".Server()
local dur = 60*6
local buf = s.Buffer():alloc(s.options.SC_SAMPLERATE* dur, 1)
s:sync()
SynthDef("imp", {},function()
Line.ar{dur=10/SampleRate.ir(), doneAction= 2};
OffsetOut.ar(0, Impulse.ar(0));
end):store();
SynthDef("rec", {}, function()
RecordBuf.ar(In.ar(0, 1),buf.bufnum,nil,nil,nil,nil,0,nil,2)
end):store()
s:sync()
local now = lanes.now_secs()
local inc = 0.3
local rec
s:makeBundle(now,function() rec = s.Synth("rec") end)
local pos = 0
local i = 0
while dur > pos do
i = i + 1
pos = pos + inc
s:sendBundle(pos+now,{"/s_new",{"imp",-1,2,rec.nodeID}})
end
print(i,"impulses sent")
local leng = inc*s.options.SC_SAMPLERATE
window = addWindow{}
grafics = addControl{window=window,panel=panelNO, typex="funcgraph",width=600,height=300,miny=leng*0.99,maxy=leng*1.01,expand=true}
QueueAction(dur+1,{function()
print"QueueAction"
buf:loadToFloatArray(0,-1,function(arr)
print"array arrived"
print(arr)
local onsets = {}
for i=1,#arr do
if arr[i] > 0.5 then onsets[#onsets+1] = i end
end
local diff = TA(onsets):differentiate()
local gdiff = {}
for i=1,#diff do gdiff[i] = {i,diff[i]} end
grafics:val(diff)
print(#diff,diff)
print(diff:min(),diff:max(),diff:mean(),diff:stddev())
end)
end})
theMetro:start()
For sending maximum of 1000 events at a time, this code was used
local s = require"sclua.Server".Server()
local dur = 60*6
local inc = 0.01
local clust = 1000
local buf = s.Buffer():alloc(s.options.SC_SAMPLERATE* dur, 1)
s:sync()
SynthDef("imp", {},function()
Line.ar{dur=10/SampleRate.ir(), doneAction= 2};
OffsetOut.ar(0, Impulse.ar(0));
end):store();
SynthDef("rec", {}, function()
RecordBuf.ar(In.ar(0, 1),buf.bufnum,nil,nil,nil,nil,0,nil,2)
end):store()
s:sync()
Routine(function()
local now = lanes.now_secs()
local rec
s:makeBundle(now,function() rec = s.Synth("rec") end)
local pos = 0
local i = 0
while dur > pos do
local ci = 0
while ci < clust do
i = i + 1; ci = ci + 1
pos = pos + inc
s:sendBundle(pos+now,{"/s_new",{"imp",-1,2,rec.nodeID}})
end
coroutine.yield(clust*inc)
end
print(i,"impulses sent")
local leng = inc*s.options.SC_SAMPLERATE
window = addWindow{}
grafics = addControl{window=window,panel=panelNO, typex="funcgraph", width=600, height=300, miny=leng*0.99, maxy=leng*1.01, expand=true}
QueueAction(clust*inc+1,{function()
print"QueueAction"
buf:loadToFloatArray(0,-1,function(arr)
print"array arrived"
print(arr)
local onsets = {}
for i=1,#arr do
if arr[i] > 0.5 then onsets[#onsets+1] = i end
end
local diff = TA(onsets):differentiate()
local gdiff = {}
for i=1,#diff do gdiff[i] = {i,diff[i]} end
grafics:val(diff)
print(#diff,diff)
print(diff:min(),diff:max(),diff:mean(),diff:stddev())
end)
end})
end) --Routine
theMetro:tempo(60)
theMetro:start()
Ah, you beat me to it, I had said in the other issue that I was going to open a new one.
Thanks for running the tests, especially in Windows. We hadn't noticed that JACK + scsynth specific.
I'd suggest, though, to follow the issue template. There's a reason why the template is designed as it is (to make it easier for developers to get the gist of the issue quickly). Also, I guess that very few developers are going to install Lua2SC for this test -- sclang test code more likely to get traction.
E.g.,
Issue title: OffsetOut timing is inaccurate against JACK
Issue #4580 includes some test code (updated/fixed here) that identifies significant inaccuracy in OffsetOut timing -- originally found in Linux, but further testing shows the same problem in 32-bit Windows connecting to a JACK server.
Sclang test:
s.boot;
(
~clockTest = { |trigStyle = \routine, factor = 100, bufdur = 2, cluster = 1| // also \impulse
// uses interpreter variables, not safe to run concurrently
if(b.isKindOf(Buffer) and: { b.numFrames.notNil }) {
Error("Another test is active. Wait for it to finish.").throw;
};
if(s.serverRunning.not) {
Error("Please boot the server first").throw;
};
Routine.run {
var recSynth, remaining, delta = factor.reciprocal, num;
b = Buffer.alloc(s, (s.sampleRate * bufdur).asInteger, 1);
SynthDef("help-OffsetOut", { arg out = 0, freq = 440, dur = 0.005;
var sig, trig, count, num;
if(trigStyle == \routine) {
sig = Impulse.ar(0);
Line.kr(0, 1, ControlDur.ir, doneAction: 2);
} {
sig = Impulse.ar(factor);
count = PulseCount.ar(sig);
num = b.duration * factor * 0.95;
FreeSelf.kr(count >= num);
};
OffsetOut.ar(out, sig)
}).send(s);
SynthDef(\rec, { |out, bufnum, bus|
var rec, done;
rec = RecordBuf.ar(In.ar(bus, 1), bufnum, loop: 0, doneAction: 2);
Line.kr(0, 1, b.duration, doneAction: 2); // FFS
}).send(s);
if(s.isLocal) { s.sync };
s.makeBundle(0.2, {
recSynth = Synth(\rec, [bufnum: b, bus: 0]);
});
if(trigStyle == \routine) {
remaining = (b.duration * factor * 0.95).asInteger;
while { remaining > 0 } {
num = min(cluster, remaining);
num.do { |i|
s.sendBundle(0.2 + (delta * i), ["/s_new", "help-OffsetOut", s.nextNodeID, 2, recSynth.nodeID]);
};
remaining = remaining - num;
(delta * num).wait;
};
} {
s.sendBundle(0.2, ["/s_new", "help-OffsetOut", -1, 2, recSynth.nodeID]);
b.duration.wait;
};
// exited loop, recording should be done
// b.getToFloatArray(0, b.numFrames, wait: -1, action: { |data| d = data });
b.loadToFloatArray(0, b.numFrames, action: { |data| d = data });
// 'forkIfNeeded' in that method, should be all finished
// scan for onsets
o = List.new;
(2 .. d.size-1).do { |i|
if(d[i-2] == 0 and: { d[i-1] == 0 and: { d[i] != 0 } }) {
o.add(i);
}
};
p = o.differentiate.drop(1);
[p.minItem, p.maxItem, p.mean, p.median].postln;
"% seconds between first and last, expected %\n".postf(
(o.maxItem - o.minItem) / s.sampleRate,
(o.size - 1) / factor
);
b.free;
};
};
)
~clockTest.(\routine, 100, 10);
[ 405, 484, 440.97103004292, 441.0 ]
9.319387755102 seconds between first and last, expected 9.32
That's: minimum measured delta (in samples), maximum, mean, median.
By contrast, Win32 scsynth -> portaudio reads minimum 440 and maximum 442. Mac is similar.
We are supposed to resolve the OSC timestamp as accurately as possible, and OffsetOut is supposed to position the first output sample to match the timestamp. We expect a very small amount of jitter because the sample clock does not necessarily run at a consistent speed, but we do expect jitter to be within +/- 2 or 3 samples at most.
We get exactly this result in Mac, and in Windows vs PortAudio, and with supernova in Linux.
Scsynth vs JACK in Linux and Windows deviate from the expected delta by up to 10% (expected = 441, 408 <= actual <= 484, error of about +/- 40 samples). The cumulative jitter averages out to the right duration (balanced positive/negative), but 10% jitter is unacceptable.
Because it happens with scsynth/JACK in two different operating systems, but not with supernova, and not with scsynth against a different audio backend, the issue must be scsynth's JACK driver.
I could repair SC_Jack.cpp in PR #4599
Could you please @jamshark70 review it?
Cool! I'll check it tomorrow.
I've just tested this on macOS with a custom build using JACK natively (not through CoreAudio). I got:
//scsynth
~clockTest.(\routine, 100, 10);
->
[ 419, 462, 440.99248120301, 441 ]
9.3098412698413 seconds between first and last, expected 9.31
//supernova
~clockTest.(\routine, 100, 10);
[ 441, 442, 441.00107411386, 441 ]
9.310022675737 seconds between first and last, expected 9.31
This indeed seem to be specific to scsynth and JACK backend, and theoretically applies to all platforms (as long as scsynth is using JACK backend).
It's up to @sonoro1234 but maybe the labels should not indicated the OS, since this problem is technically _not_ OS specific.
@dyfer could you test and review PR #4599