Supercollider: SC_Jack server clock fails

Created on 26 Sep 2019  路  5Comments  路  Source: supercollider/supercollider

(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()
bug scsynth

All 5 comments

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

Environment

  • SuperCollider version: 3.10.x
  • Operating system: Linux or Windows
  • Other details (Qt version, audio driver, etc.): scsynth connecting to JACK

    • Linux scsynth -> JACK shows the problem

    • Windows scsynth -> JACK shows the problem

    • Windows scsynth -> PortAudio does not show the problem

    • Mac scsynth -> CoreAudio does not show the problem

    • Supernova never shows the problem (on any OS, against any audio backend)

Steps to reproduce

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.

Expected vs. actual behavior

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jamshark70 picture jamshark70  路  5Comments

fmiramar picture fmiramar  路  4Comments

michaeldzjap picture michaeldzjap  路  3Comments

khoin picture khoin  路  3Comments

telephon picture telephon  路  6Comments