SynthDef struct file: Variant-Spec

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 \name.kr(default) 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|
	Out.ar(out, SinOsc.ar(freq))
}, variants: (a: [freq: 220], b: [freq: 220*9/8]));

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

f = { |c, n|
	var x = Int8Array.fill(n, { c.next });
	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 = c.next;
	String.fill(len, {
		var ch = c.next.asAscii;
		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
(
3.do {
	var ins, outs;
	~pstr.(c).debug("UGen");
	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].last.do { |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].last.do { |i|
		f.(c, 1).debug("out % rate".format(i));
	};
};
)

… to get to variants:

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

(
2.do { |i|
	"\nVariant %\n".postf(i);
	~pstr.(c).debug("variant name");
	
	// above we found 2 params
	2.do { |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.

hjh