NamedControl: a better way to write SynthDef arguments


#1

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.


Evaluating Code
Multiple and independent pitchEvent keys inside a SynthDef
#2

Thank you for the kind explanation!

Could the following code

SynthDef(\sine, {
    |freq = 440, amp = 0.1, out = 0|
    Out.ar(out, SinOsc.ar(freq) * amp);
}, rates: [\ar, nil, \ir]).add;

be rewritten by using function.play as follows?

(
{
|freq = 440, amp = 0.1, out = 0| Out.ar(out, SinOsc.ar(freq, 0, amp))
}
.asSynthDef(rates: [\ar, nil, \ir])
.play
)

Or is there any other better way? I am asking this because I do not see the argument rates of the .play in the help document of Function.

Anyway, NamedControl style is better!


#3

I totally endorse the NamedControl style. I would add another point - I think the arg keyword vs. double pipes is bad and the NamedControl helps resolve that badness - at lest with synthdefs.


#4

I think the arg keyword vs. double pipes is bad and the NamedControl helps resolve that badness - at lest with synthdefs.

You mean having both styles supported, or you have a problem with pipes?

I always use pipes in SC as I like the way it makes the function interface easy to scan.


#5

Yeah I think supporting both is messy and kinda leads to some unnecessary confusion - i would have liked to have seen a more consistent canonical supercollider style when first learning the language. I think namedcontrol style achieves that for synthdefs. Personally I tend to prefer the arg keyword since it is more consistent with variable declaration syntax.


#6

Actually I’ve changed my mind. arg keyword is terrible.

There’s a reason that languages don’t do function signatures that way. Function signatures and function bodies should always be visually distinct. Pipes achieve that.


#7

I can see your point. But most languages (that i’ve used) designate function args with parenthesis, e.g. Java, Python, JavaScript. I suppose neither the arg keyword nor the double pipes are as clean or familiar as that. Pipes generally have a more functional purpose in most languages (and supercollider) e.g. logic- if (true || false) {}. For me the pipe character used as argument delimiter overloads that meaning. The arg keyword is less terse but its intention is perfectly clear.


#9

did you know that commas and equals signs are optional in the pipe notation?

|x = 3, y = 4|
|x = 3 y = 4|
|x 3, y 4|
|x 3 y 4|

and parentheses are required around the argument if it isn’t a literal?

|freqs = [440, 440]| // syntax error
|freqs = #[440, 440]| // ok
|freqs = ([440, 440])| // ok

|server = Server.default| // syntax error
|server = (Server.default)| // ok

the parentheses are meant to resolve some ambiguous parses. but all the ambiguous cases seem to involve missing commas, leaving me to wonder why commas were ever optional in the first place.


#10

I rather like

|x 3, y 4|

Appeals to my inner Haskell programmer. Probably wouldn’t use it in practice though.

While SuperCollider is a much better language than critics think, parser and API consistency aren’t among its virtues.


#11

Oh and you win Nathan. Named controls - I’m a convert. I just had to write a complicated synthdef and had a moment where I realized that named arguments would make my life way easier.


#12

Nathan, as you have already put energy in collecting these arguments, are you planning to add a tutorial in SCDoc ? That would be really helpful – and: as these errors with argument rates in the SynthDef helpfile were discussed recently, I realized that there’s not a single mentioning of NamedControl in the SynthDef help (!), that should definitely be added! IMO it would be ideal to have a paragraph about the existence of NamedControl in SynthDef help plus an additional tutorial.
BTW: IIRC NamedControl was introduced relatively lately which would explain its underrepresentation in the other (often older) help files. I know for sure that array arguments were introduced later on, and these are also underrepresented.


#13

the new “Learn SC” tutorial effort will teach SC using NamedControls, and will probably contain these arguments as a side note of some kind. help files are nice but in practice they don’t draw quite the attention that forum posts or popular tutorials do.

(also, a completely shallow reason: personally i don’t like working with SCDoc.)


#14

It’s good to know that there is an alternative syntax for arguments. However, using the arg keyword has one big advantage: by looking at the beginning of the SynthDef I can easily tell all the arguments and their default values. If I used the NamedControl style, I’d have to look all over the definition for finding the same information.

In most programming languages it’s usually discouraged to have functions with more than 3-4 arguments but sclang is a bit special in this regard: a SynthDef can easily have more than 5. If someone wants to write code which is easy to read for others (or for themselves later down the line) they should pay attention to formatting arguments.

A thought about multichannel controls: can’t the freqs be specified as Array.fill(8, 440) or even [440].stutter(8)?


#15

It’s good to know that there is an alternative syntax for arguments. However, using the arg keyword has one big advantage: by looking at the beginning of the SynthDef I can easily tell all the arguments and their default values. If I used the NamedControl style, I’d have to look all over the definition for finding the same information.

if you want all your arguments at the top, you can easily do so:

SynthDef(\sine, {
    var out = \out.kr(0), freq = \freq.kr(440), amp = \amp.kr(0.1);
    Out.ar(out, SinOsc.ar(freq) * amp);
}).add;

A thought about multichannel controls: can’t the freqs be specified as Array.fill(8, 440) or even [440].stutter(8) ?

try it :slight_smile:

i guarantee you it won’t work, and the reasons are clearly explained in the original post. it has to do with limitations in the way sclang stores default arguments in function metadata — if the default argument is not a literal, it can’t be accessed through the function’s prototype frame.

{ |foo = (Array.fill(8, 440))| }.def.prototypeFrame // -> [ nil ]
{ |foo = ([440].stutter(8))| }.def.prototypeFrame // -> [ nil ]

#16

Yes, but in this case you lose the advantage of less typing – actually it looks a bit more complicated to me. But it all comes down to personal preferences, I think (as most coding style does).

And you’re right, Array.fill() etc. cannot be used as default values for arguments in this case. I found the relevant part in the documentation (at least I think this is it):

In general arguments may be initialized to literals or expressions, but in the case of Function:play or SynthDef:play, they may only be initialized to literals.

Coming from a software developer background with experience mostly in modern high level languages (JavaScript, Python, etc.) sclang often confuses me…


#17

i get your point, but the cool thing is that NamedControls at least give you a choice. when i’m doing some quick sound synthesis stuff, i’m not so concerned about clean and readable synthdefs. interspersing parameters with code allows me to quickly replace any constant value with a parameter, and i’m no longer slowed down by needing to properly maintain that argument list.

for more complex synthdefs or code meant for sharing with other people, yes, it’s better to summarize parameters at the top, and this puts NamedControls are at a verbosity disadvantage to argument lists. either way, i think this is greatly outweighed by the other glaring issues with argument style.

yeah, believe me, i’m fully aware of how annoying sclang can be. the lexer/parser/compiler is probably the oldest surviving part of the platform, and the most difficult for contributors to do any kind of maintenance or changes on — huge messy codebase with zero documentation. a makeover for the language is on the horizon though.

nevertheless i do hold that sclang is a relatively nice language if you can avoid the traps. some popular tutorials (w/ all due respect to their luminous authors) encourage some sloppy habits and can give bad first impressions to seasoned programmers. the new tutorial effort will hopefully remedy that.


#18

The issues with creating synthdefs aren’t really a language thing. SynthDefs are better thought of as DSLs for describing how a particular synth should be constructed. The issues with parameters are flaws with the synth construction ‘library’. My opinion is that given what we know now (after 20 years of DSL knowledge), but didn’t know then, that was probably not the best way of doing things - but hey, it still works remarkably well and is a stunningly successful achievement.

While I think SCLang does have it’s flaws and idiosyncrasies, I don’t know that these are (other than a couple of glaring examples) particularly worse than most other mainstream languages. Compared to JavaScript SCLang looks pretty good (SCLang just doesn’t have a ‘SCLang the Good Bits’ tutorial). And for musical purposes SCLang has a number of features which are really good and are probably unmatched by any mainstream language that I know of (e.g. JavaScript has the flexibility offered by events/environments in SuperCollider, but lacks the flexibility of routines and easy task scheduling).


#19

I’m really interested in that. What I’d like to see is some kind of API which makes it easy to create different kind of frontends for scsynth – either in another language or a visual environment, maybe something like Audulus or Reaktor.


#20

Hi Nathan,

Thanks a lot for these new insights! I am struggling a bit with implementing an array of controls for my Out buses:

SynthDef(\soundin, {
        var sig, env, fx, fxlvl, numFx;
        numFx = \numFx.ir(4);
        env = EnvGen.kr(Env.asr(0.01, 1.0, 0.01), \gate.kr(0), doneAction: \da.kr(2));
        sig = SoundIn.ar(\in.kr(0), mul: \lvlin.kr(0.2));
        sig = sig * env * \lvlout.kr(0.2);
        sig = PanAz.ar(4, sig, \pan.kr(0), width: \width.kr(2.0));
        Out.ar(\out.kr(0), sig);
        Out.ar(\fxout.kr(0 ! numFx), sig * \fxlvl.kr(0 ! numFx));
    }).add;

This doesn’t work, unfortunately… I would like to be able to do something like this:

x = Synth(\soundin, [\fxout, [4, 8, 12, 16], \fxlvl, [0, 0.2, 0.1, 0.4]]);
x.set(\fxlvl[0], 1);

… and so on…

Any help on this would be greatly appreciated!

Best,
Kenneth


#21

this is a limitation not specific to namedcontrols, but a general problem with SynthDefs. the UGen graph is fixed at compile time, so you can’t e.g. change the size of an array of UGens in response to a parameter.

the typical workaround is to programmatically define a family of SynthDefs.

(
(1..8).do { |numFx|
    SynthDef(\soundin ++ numFx.asString, {
        var sig, env, fx, fxlvl;
        env = EnvGen.kr(Env.asr(0.01, 1.0, 0.01), \gate.kr(0), doneAction: \da.kr(2));
        sig = SoundIn.ar(\in.kr(0), mul: \lvlin.kr(0.2));
        sig = sig * env * \lvlout.kr(0.2);
        sig = PanAz.ar(4, sig, \pan.kr(0), width: \width.kr(2.0));
        Out.ar(\out.kr(0), sig);
        Out.ar(\fxout.kr(0 ! numFx), sig * \fxlvl.kr(0 ! numFx));
    }).add;
};
)

there is an important caveat to be aware of with the Out ugen that i am also noticing here. the second argument to Out does not multichannel expand like a normal UGen. Out.ar(0, [foo, bar, baz]) does a special Out-specific trick where foo is outputted to bus 0, bar to bus 1, and baz to bus 2.

i think something like this would work.

(
(1..8).do { |numFx|
    SynthDef(\soundin ++ numFx.asString, {
        var sig, env, fxlvl, fxout;
        env = EnvGen.kr(Env.asr(0.01, 1.0, 0.01), \gate.kr(0), doneAction: \da.kr(2));
        sig = SoundIn.ar(\in.kr(0), mul: \lvlin.kr(0.2));
        sig = sig * env * \lvlout.kr(0.2);
        sig = PanAz.ar(4, sig, \pan.kr(0), width: \width.kr(2.0));
        Out.ar(\out.kr(0), sig);
        fxlvl = \fxlvl.kr(0 ! numFx);
        fxout = \fxout.kr(0 ! numFx);
        Out.ar(fxout, sig *.t fxlvl);
    }).add;
};
)

personally, in this case, i would separate the fx gains into “send” Synths that simply bus audio from one place to another with controllable gain. i use this SynthDef for routing different tracks to aux busses:

SynthDef(\send, {
    var snd;
    snd = In.ar(\in.kr(0), 2);
    snd = snd * \amp.kr(1);
    Out.ar(\out.kr(0), snd);
}).add;

i’m terribly sorry, kind of in a hurry and can’t really explain this better, but hope this helps at least a little.