i had posted an earlier version of this to the list, but that was during the dark ages between the nabble shutdown and the creation of this forum. reposting so that more people see it, since this is sort of a PSA.
the prominent way of writing SynthDefs that you’ve learned in tutorials looks like this:
SynthDef(\sine, {
|freq = 440, amp = 0.1, out = 0|
Out.ar(out, SinOsc.ar(freq) * amp);
}).add;
if you’re curious about what’s going on under the hood with this method, SynthDef.new
eventually calls the SynthDef:buildUgenGraph
instance method, which calls SynthDef:addControlsFromArgsOfFunc
. this analyzes the argument list of the function using some rather dank function reflection properties in sclang (FunctionDef:argNames
and FunctionDef:prototypeFrame
). take a look at the code if you’d like.
an alternate method for writing arguments in SynthDefs was quietly introduced in 2008 (SC 3.3?). i call it the “NamedControl style,” and it looks like this:
SynthDef(\sine, {
Out.ar(\out.kr(0), SinOsc.ar(\freq.kr(440)) * \amp.kr(0.1));
}).add;
the method Symbol:kr
is a shortcut for calling (e.g.) NamedControl.kr(\freq, 440)
. the argument to the method, if supplied, is the default value.
a lot of SC users are not aware of the NamedControl style. it came later and hasn’t made it to any popular SC tutorials that i’m aware of. i used the argument style for a long time, but i switched to NamedControls sometime last year soon after finding out about them.
here’s why i think you should make this switch too!
Advantage: Less typing
the argument style requires you to make your change in two places if you add, remove, or rename arguments. this becomes especially unwieldy when you have a long synthdef with dozens of arguments, requiring a lot of jumping around. the NamedControl style localizes the specification of arguments. this saves you a lot of typing especially if you’re the kind who live codes SynthDefs (yikes).
some people like having a nice visual summary of their SynthDef arguments at the top. you can still do that with the NamedControl style:
SynthDef(\sine, {
var out = \out.kr(0), freq = \freq.kr(440), amp = \amp.kr(0.1);
Out.ar(out, SinOsc.ar(freq) * amp);
}).add;
this is a perfectly okay style (Scott Carver says he uses something like this). admittedly, it renders the advantage of terseness a moot point, since it’s actually more code than the argument style.
Advantage: Rate specification of parameters is much less awkward
it is possible for SynthDef arguments to be control rate, trigger rate, audio rate, or initial rate. in the argument style, you have two different ways to access the latter three rates, and they both feel really awkward to me.
one is to use the t_
, a_
, and i_
prefixes on the argument names:
SynthDef(\sine, {
|a_freq = 440, amp = 0.1, i_out = 0|
Out.ar(i_out, SinOsc.ar(a_freq) * amp);
}).add;
i think this looks real ugly, and obscures the actual names of the arguments (freq
and out
in the compiled SynthDef). worst of all, it is possible to write them by accident. there’s been at least one instance on the list of someone accidentally writing t_freq
as an abbreviation for “trigger frequency” and encountering a hard-to-debug issue with a seemingly unresponsive parameter that’s actually named freq
. encoding semantic information into argument names overall strikes me as a pretty gross design decision.
the other is to use the rates
parameter of SynthDef.new
:
SynthDef(\sine, {
|freq = 440, amp = 0.1, out = 0|
Out.ar(out, SinOsc.ar(freq) * amp);
}, rates: [\ar, nil, \ir]).add;
which fragments the argument list further, silently causing bugs if we forget to update that list when adding or removing an argument. it’s also far less readable and obvious what it’s doing.
in the NamedControl style, you can simply write \foo.kr
, \foo.tr
, \foo.ar
, \foo.ir
:
SynthDef(\sine, {
Out.ar(\out.ir(0), SinOsc.ar(\freq.ar(440)) * \amp.kr(0.1));
}).add;
it’s terse, crystal-clear explicit, and parallels the same class method names used in ugens.
Advantage: Better multichannel controls
this is the correct way to create an eight-channel SynthDef argument:
SynthDef(\sine, { |freqs = #[440, 440, 440, 440, 440, 440, 440, 440]| ... }).add;
my immediate reaction to seeing this code would be, “can’t we reduce repetitions and ask for 8 copies of 440”? well, it would be nice if this worked…
SynthDef(\sine, { |freqs = (440 ! 8)| ... }).add;
…but it silently fails and gives you a one-channel freqs
argument. it takes some digging to figure out why. as described above, SynthDef
grabs the default values of the function using FunctionDef:prototypeFrame
. this method behaves fine for literals (including literal arrays), but seemingly not so well for non-literal arguments:
{ |foo = #[8, 8, 8]| }.def.prototypeFrame // -> [ [ 8, 8, 8 ] ]
{ |foo = ([8, 8, 8])| }.def.prototypeFrame // -> [ nil ]
{ |foo = (8 ! 3)| }.def.prototypeFrame // -> [ nil ]
so when you type { |foo = (8 ! 3)| }
, there is no way for SynthDef to probe the function and recover the 8 ! 3
due to this limitation in sclang. also, there is no literal array equivalent to !
(.dup
) that i am aware of.
NamedControls do not have this issue, and will play nice with default values that aren’t literals:
SynthDef(\sine, {
Out.ar(\out.kr(0), SinOsc.ar(\freqs.kr(440 ! 8)) * \amp.kr(0.1));
}).add;
Tip: Duplicating NamedControls
if you wire the same NamedControl into multiple places like this:
SynthDef(\sine, {
Out.ar(\out.kr(0), SinOsc.ar(\freq.kr(440)) + Pulse.ar(\freq.kr(440)));
}).add;
it will work fine, but you’re duplicating code by having the 440 in two places. note that NamedControl will get mad at you and throw an error if you try to use the same NamedControl twice with different default values (e.g. SinOsc.ar(\freq.kr(440)) + Pulse.ar(\freq.kr(880))
):
ERROR: NamedControl: cannot have more than one set of default values in the same control.
whenever i want to reuse a NamedControl, i always assign it to a variable:
SynthDef(\sine, {
var freq = \freq.kr(440);
Out.ar(\out.kr(0), SinOsc.ar(freq) + Pulse.ar(freq));
}).add;
this is something you will have to get used to if you move from arguments to NamedControls. however, it’s not that much of a leap — if you want to wire the same UGen into multiple places, you generally have to assign it to a variable anyway.
alternatively, you can just use the aforementioned method where we just assign all NamedControls to variables the top, and you never have to worry about minor refactoring whenever you want to reuse a parameter somewhere.
Conclusions
i’ve tried to think of any reasons you’d want to use the argument style, but to me its advantages seem pretty meager. the biggest one is its familiarity: code using NamedControl style understandably raises eyebrows for people who haven’t heard of it. of course, the point of this post is to help remedy that.
the idea of SynthDef arguments being function arguments seems conceptually elegant, but in the real world, SynthDef arguments are different things from function arguments because they are tagged with additional info — default values, argument rates (kr/ar/ir/tr), and even a ControlSpec in a future version of SC. maybe if sclang had more powerful ways of adding metadata to function arguments, the argument style might have more footing.
if you haven’t heard of the NamedControl style before, please try it out for a while and see what you think. while i don’t think it will dethrone the 10+ years of use that the argument style has seen, i’m hoping for it to gain some more traction in the SC user base.