Does anyone have any large synthdefs that I could use for testing?

I’m currently rewriting the SynthDef compiler ClassLib: fix and refactor SynthDef compiler by JordanHendersonMusic · Pull Request #6405 · supercollider/supercollider · GitHub and need to make sure all your synths continue to work as expected.

If any one has any large complicated SynthDefs with lots of channels or lots of effects (preferably without randomness), would you mind sharing them so I can include them in the testing library. This way, your code will become guaranteed to work on all future versions of supercollider?

Thank you :smile:

1 Like

Hello Jordan,

you could try with some of my pseudo ugen extension classes from miSCellaneous_lib. I don’t know if it helps if the construction principles are somewhat hidden like here, at least they produce a lot of ugens and the dumps can be compared. Also, the input is easy to overlook. I’d be happy if you give it a try !

Especially the usage of Fb1, Fb1_ODE and related is resulting in large SynthDefs. E.g., with a blocksize of 64, this example from the Fb1 helpfile generates ca. 600 ugens (the number depends on the blocksize):

// OnePole is implemented by out(i) = ((1 - abs(coef)) * in(i)) + (coef * out(i-1))
// so it can be written with Fb1

y = { Fb1({ |in, out| (in[0] * 0.05) + (out[1] * 0.95) }, WhiteNoise.ar(0.3), leakDC: false) }.play

You could also check the DX suite helpfiles, e.g., here’s a comparison from the DXEnvGen helpfile, Ex.4:

// As channel switching with DXEnvFan / DXFan is implemented by "watcher ugens",
// it becomes costly if the number of output channels increases (growth of quadratic order).
// The effort is lowered significantly if you pass a reserved bus for this or use DXEnvFanOut,
// this also concerns DXFan / DXFanOut.

(
// load with extended resources
s = Server.local;
Server.default = s;
s.options.numPrivateAudioBusChannels = 256;
s.options.memSize = 8192 * 4;
s.options.numWireBufs = 512;
s.reboot;
)


// on my machine this example needs ca. 2.5 % CPU (818 ugens) ...

(
x = {
    DXEnvFan.ar(
        Dshuf((0..29), inf),
        size: 30,
        fadeTime: 0.005
    ) * 0.25
}.play
)

x.release

// ... whereas this needs ca. 0.6 % CPU (167 ugens)

(
a = Bus.audio(s, 30);

x = {
    DXEnvFan.ar(
        Dshuf((0..29), inf),
        size: 30,
        fadeTime: 0.005,
        bus: a
    ) * 0.25
}.play
)

x.release

(
a.free;
b.free;
)



// care has to be taken with buses and multichannel expansion:

// here two buses have to passed as otherwise we get a wrong result,
// the same bus would be taken for different calculations at the same time

(
a = Bus.audio(s, 8);
b = Bus.audio(s, 8);

{
    Mix(DXEnvFan.ar(
        [Dseq((0..7), inf), Dseq((7..0), inf)],
        size: 8,
        fadeTime: 0.01,
        equalPower: 0,
        bus: [a, b]
    ))
}.plot(0.1)
)

(
a.free;
b.free;
)
1 Like

And would you mind if I copied parts of that quark into the unit testing library? The one-pole seems like a really good example


Just for reference, here are the number of UGens that get produced. I’ve also done a null test between no optimisations and mine, not the existing because it is subtly broken, actually your classes here might run into some of the bugs as they relate to buffer read and write orderings

SynthDef.newWithoutOptimisations(\asd, {
	var out = Fb1({ |in, out| (in[0] * 0.05) + (out[1] * 0.95) }, SinOsc.ar, leakDC: false);
	Out.ar(0, out)
}).dumpUGens

// no optimisations
722 ugens

// existing
571 ugens

// mine
507 ugens
1 Like

Sure, do what it needs, I’m fine with that !

Actually, I’ve used the firstArg operator (<!) to exactly avoid these issues. So, I hope, it should continue to work :slight_smile:

Thank you!

Of course, my change is entirely backwards compatible!

<! is just a terrible solution because it is actually a UGen on the server that is made to fix a bug in SynthDef!

Great !

Yes, it always felt strange. And there were some really tricky cases, that took me an eternal time to understand and debug, where the ugen order was messed up. Unfortunately, it’s some years back, so I don’t recall them exactly …
Thanks for your efforts, a very valuable initiative !

With ddwWavetableSynth:

(
var numOscs = 15;

d = SynthDef(\wavetbl, { |out, freq = 440, detun = 1.01, gate = 1, amp = 0.1,
	ffreq = 2000, rq = 1,
	wtPos = 0, squeeze = 0, offset = 0, bufnum|
	var sig = MultiWtOsc.ar(freq, wtPos, squeeze, offset, bufnum,
		numOscs: numOscs, detune: detun);
	sig = RLPF.ar(sig, ffreq, rq);
	sig = sig * EnvGen.kr(Env.adsr, gate, doneAction: 2);
	Out.ar(out, sig.dup);
});
)

d.children.size
-> 601

I also have some TX77-style FM synths with a couple hundred UGens, big enough?

hjh

Thanks! I’ve actually got something similar to that already. I’m now looking for things that use buffers, pv ugens, or demand stuff.

This is actually an interesting case as my version always seems to produce 2 more UGens. I will have to look into why.

However, it does produce significantly less wireBufs.

// og 
numWires 243 numWireBuffers 24 UGenCount 217
// new
numWires 248 numWireBuffers 10 UGenCount 219

It might be my duplicate-math-op optimization.

hjh

No it includes that. It is to do with how the maths identities resolve (or perhaps I have missed one). The new approach strictly works from the bottom of the graph (from output) upwards towards the input, changing things below, then jumping back down to check the changed. This means how things combine can get complex. Still, its a minor regression and the only one I’ve observed in a lot of testing.

Aside from DX suite also ZeroXBufRd / ZeroXBufWr / TZeroXBufRd use demand ugens. PV_BinGap and PV_BinRange are PV wrappers, the latter two are relatively simple, though.

Didn’t see your dx example, I’ll have a look later today.

Your first example of the DX stuff which used 818 UGens now uses 611, a 25% reducation.
The second which used 167 now uses 110, a 34% reduction.

Maybe this multichannel (13 channels) circular buffer thing is of use? i’m really bad at optimizing code, so a little bit ashamed to share this… but it works for what i use it!

(
SynthDef(\circularbuffer13, {
    arg in = 0, out = 0;
    var numChannels = 13;
    var input, circularBufs, writePos, readPos, output;
    var bufFrames, numActiveReaders, readerAmps, readerRates, readerOffsets;
    var bufferSize = 20;  // 20 seconds buffer
    var maxReaders = 8;  // Fixed number of maximum readers

    // Create local buffers for each channel
    bufFrames = bufferSize * SampleRate.ir;
    circularBufs = numChannels.collect { LocalBuf(bufFrames, 1).clear };

    // Multichannel input
    input = In.ar(in, numChannels);

    // Control inputs for multiple readers
    numActiveReaders = \numreaders.kr(4);
    readerRates = \rate.kr(1!maxReaders);
    readerOffsets = \offsetms.kr(100!maxReaders, 1/30, fixedLag:true) / 1000 * SampleRate.ir;  // Convert ms to samples

    // Write position
    writePos = Phasor.ar(0, 1, 0, bufFrames);

    // Write to buffers
    numChannels.do { |i|
        BufWr.ar(input[i], circularBufs[i], writePos);
    };

    // Multiple read positions
    readPos = maxReaders.collect { |i|
        var phaseOffset = readerOffsets[i];
        var rate = readerRates[i];
        (writePos - phaseOffset + (Phasor.ar(0, rate - 1, 0, bufFrames))).wrap(0, bufFrames)
    };

    // Calculate amplitudes for each reader
    readerAmps = maxReaders.collect { |i|
        (i < numActiveReaders).lag(0.1)
    };

    // Read from buffers at multiple positions for each channel
    output = numChannels.collect { |chan|
        Mix(
            maxReaders.collect { |i|
                BufRd.ar(1, circularBufs[chan], readPos[i], 1, 4) * readerAmps[i]
            }
        )
    };

    Out.ar(out, output);
}).writeDefFile(d);
)
1 Like

Sounds great, thanks for testing !