SynthDef struct file: Variant-Spec

Hello again,
after long time I am getting back since I manged to do some progress in compiling and decompiling synthdefs through vvvv.
However I have a very specific question, what are the mentioned variant-spec (parameters) in the very last bytes of a compiled synthdef ?
What do they represent?

[ variant-spec ] * V
pstring - the name of the variant
[float32] * P - variant initial parameter values

Thanks in advance!

(PS: feel free to move my thread in different category if it doesnt match the context of current one)

Just to point to the reference (which may be annoying to find sometimes): Synth Definition File Format | SuperCollider 3.12.2 Help

Also, sticking to MulAdd (not + and *) simplifies a bit since you avoid variants of this scheme.

I think this is both untrue (a * b + c optimization → MulAdd is reliable, it’s not necessary to write MulAdd directly), and not relevant to SynthDef variants.

@cnisidis See SynthDef | SuperCollider 3.12.2 Help for the purpose of variants.


1 Like

16 posts were split to a new topic: SynthDef and UGen further optimization (even JIT compilation?)

thanks for pointing it out, I was posting in the forum because I couldnt find any explanation in the documentation, especially on the page you mentioned Synth Definition File Format | SuperCollider 3.12.2 Help

maybe I need to reform my question,

What is a variant-spec in SC context?

@jamshark70 just read you answer! I ll have a look, thanks.

1 Like

Split off the long (interesting!) discussion of UGen / SynthDef design – and then discovered that the OP of this thread had an open, on-topic question which may have been lost (which is why keeping threads a bit more on topic really matters – digressions may be ok sometimes, but this was an out and out thread hijack).

@cnisidis I can come back to that question a bit later.


OK… not sure if you have already found the answer, but for future readers:

A variant is an alternate set of control input default values – a type of preset. If your SynthDef defines atk, dcy, sus, rel parameters, those defaults could be middle-of-the-road, e.g. atk = 0.1, dcy = 0.2, sus = 0.7, rel = 0.1, but you could define a short, sharp “variant” with atk = 0.01, dcy = 0.22, sus = 0, rel = 0.22.

The Synth Definition File Format help file uses the term “parameter.” I usually prefer “control” or “control input,” because that helps lead to the Control family of UGens. Colloquially, we often call it “argument” or “argument list” but the more recent trend of defining controls as \ exposes the fact that this isn’t completely accurate.

The initial parameter list is written early in the def: “int32 - number of parameters (P); [float32] * P - initial parameter values” – that is, there’s a flat array of default values for controls.

d = SynthDef(\test, { |out = 0, freq = 440|,
}, variants: (a: [freq: 220], b: [freq: 220*9/8]));

b = d.asBytes;
c = CollStream(b);

f = { |c, n|
	var x = Int8Array.fill(n, { });
	var str = x.collectAs({ |byte|
		var ch = byte.asAscii;
		if(ch.isPrint) { ch } { $. }
	}, String);
	[x, str]

// not using getPascalString
// because I don't want to print control chars accidentally
~pstr = { |c|
	var len =;
	String.fill(len, {
		var ch =;
		if(ch.isPrint) { ch } { $. }

f.(c, 4);  // SCgf
f.(c, 4);  // ver 2
f.(c, 2);  // 1 def

~pstr.(c);  // "test"

f.(c, 4);  // 1 const
f.(c, 4);  // 0.0

f.(c, 4);  // 2 params
f.(c, 4);  // out = 0
f.(c, 4);  // freq default

In this very simple def, there are two inputs:

  • Int8Array[0, 0, 0, 0] → Float.from32Bits(0) == 0.0
  • Int8Array[67, -36, 0, 0]
    • Float.from32Bits((67 << 24) | ((-36+256) << 16))
    • = 440.0

Skip over parameter names (see this thread) and UGens:

f.(c, 4);  // 2 param names
~pstr.(c);  // out
f.(c, 4);  // index 0
~pstr.(c);  // freq
f.(c, 4);  // index 1

f.(c, 4);  // 3 units
( {
	var ins, outs;
	f.(c, 1).debug("rate");
	ins = f.(c, 4).debug("inputs");
	outs = f.(c, 4).debug("outputs");
	f.(c, 2).debug("special index");
	ins[0] { |i|  // should do 4 bytes --> int32 but this is a small test case
		f.(c, 4).debug("in % points to".format(i));
		f.(c, 4).debug("index");
	outs[0] { |i|
		f.(c, 1).debug("out % rate".format(i));

… to get to variants:

f.(c, 2);  // 2 variants

( { |i|
	"\nVariant %\n".postf(i);
	~pstr.(c).debug("variant name");
	// above we found 2 params { |j|
		f.(c, 4).debug("param " ++ j);

Variant 0
variant name: test.a
param 0: [Int8Array[0, 0, 0, 0], ....]
param 1: [Int8Array[67, 92, 0, 0], C\..]

Variant 1
variant name: test.b
param 0: [Int8Array[0, 0, 0, 0], ....]
param 1: [Int8Array[67, 119, -128, 0], Cw..]

So the main parameter list is an array of floats, and each variant is an array of floats with the same size – by Float.from32Bits, you can confirm that these are test.a = [0.0, 220.0] and test.b = [0.0, 247.5].

Synth("test") uses the main parameter array. Synth("test.a") uses the parameter array for test.a, etc. Each variant is an optional replacement for the “initial parameter values” at the beginning of the SynthDef.


Thanks a lot for the explanation, I imagined that they should be something like presets. But your approach is much much more clear and understandable!

So to verify, I noticed when I decompiled a synthdef with variants that in the variants section I could find all the Parameters and there initial values, that’s correct right ? Since they dont have different indexing.

That’s right. Each variant should be the same size as the full parameter list (the number of parameters is stated once at the top and not repeated for variants), and each variant array should be a drop-in replacement for the parameter array at the top (same indexing – if indexing were different, then it would have to be represented in the variants section – but it isn’t, therefore it can only be the same).


1 Like

thanks a million, now it is only matter of decision making for the design on its visual/node aspect.