NamedControl: a better way to write SynthDef arguments

With https://github.com/supercollider/supercollider/pull/3814 I believe you can do:

SynthDef(\foo, {
    |freq, amp|
   freq.spec = ControlSpec(20, 20000, default:440): 
   amp.spec = \db;
});

… or the equivalent via symbols - maybe more readable and straightforward than your macro example, even.

2 Likes

And - iirc you can use \symbol.kr notation for the same symbol multiple times in a SynthDef and it works as you would expect? If this doesn’t work now, it’s something worth fixing (and not so difficult, engineering-wise).

Hi! Thanks for the NamedControl tutorial! Great tool!
I wonder if it is possibe to have kind of multi type NamedControls

Starting from here

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

Which kind of solution would be able to send an audio rate ugen to \freq.kr?
Better said how could a \freq.kr and a \freq.ar live together in the same SynthDef?
AFAIK that’s a good reason to use Instr instead SynthDef even if i know that using Quarks can be a can of worms.
I guess i’m looking for a kind of polymorphism, like sending a polymorph object as an argument to a polymorph object as almost all SC UGen are with their double kr/ar nature.

An important point is this: the structure of a synthdef on the server is totally fixed. Once you decide for kr it’s kr forever, no way to extend on that base.

That said, there is some kr / ar flexibility: e.g. you can map an audio bus to a kr control. But you must expect artefacts with non-LFO controls (with standard settings kr results in a nyquist frequency of ca 350 Hz which is often forgotten):

(
SynthDef(\test_kr, {
	Out.ar(0, LeakDC.ar(SinOsc.ar(\freq.kr(400), 0, 0.1)))
}).add;

SynthDef(\test_ar, {
	Out.ar(0, LeakDC.ar(SinOsc.ar(\freq.ar(400), 0, 0.1)))
}).add;
)

(
x = Synth(\test_kr);
a = Bus.audio(s, 1);
)

x.map(\freq, a);

// distorted FM
y = { Out.ar(a, SinOsc.ar(1000, 0, 1000)) }.play

y.free
x.free


(
x = Synth(\test_ar);
a = Bus.audio(s, 1);
)

x.map(\freq, a);

// clean FM
y = { Out.ar(a, SinOsc.ar(1000, 0, 1000)) }.play

y.free
x.free

Furthermore you can use A2K and go via a control bus (or K2A on other occasion).

But again: if you intend to go essentially into ar, e.g. do something like FM via buses you need ar args and ar buses.

1 Like

Thanks @dkmayer
that’s the reason because i was looking into Instr…as reported in help doc

" Instr SynthDefs

Unlike SynthDef, Instr can take any kind of input for its arguments, not just (including an Integer quantity, symbols or even other functions) and so while a SynthDef has a fixed architecture for a Synth, an Instr can generate multiple SynthDefs of varying architectures. For instance you could specify an Env or a fixed time duration or a quantity (how many parallel voices to create, detuned against each other) or even the name of a UGen (LFSaw, Pulse) to use for the oscillator."

This is generally good because it groups control names with their “types”, but it does have one downside: if you spread out the SynthDef inputs/controls through the code, and there’s a lot of them e.g. 20 params is not uncommon for some of my synths, it’s a real mess figuring out the full list of parameters when you look at the code. So what I do know is duplicate/move the list to the beginning of the SynthDef as vars, i.e. for a trivial example, instead of just

(SynthDef(\beep, {
	ReplaceOut.ar(\out.ir(0), \amp.kr(0.5) * SinOsc.ar(\freq.kr(440)).dup);
}).add;)

I do

(SynthDef(\beep, {
	var out = \out.ir(0), amp = \amp.kr(0.5), freq = \freq.kr(440);
	ReplaceOut.ar(out, amp * SinOsc.ar(freq).dup);
}).add;)

That looks duplicative, but with a lot controls (20+) and 20+ lines of code in a SynthDef, it helps me to have the full list upfront in the code (which is what args`does).

The var “overloading” of the named controls does work properly, i.e. there are no compile errors and you can still change their values with messages (e.g. Node.set, .map) still work properly, as it turns out.

Oddly enough, you can also write arg instead of var there:

(SynthDef(\beep, {
	arg out = \out.ir(0), amp = \amp.kr(0.5), freq = \freq.kr(440);
	ReplaceOut.ar(out, amp * SinOsc.ar(freq).dup);
}).add;)

It seems to work too for set or map but it doesn’t for the initial values which don’t get set anymore in this latter (arg instead of var approach). The reason for that is clear if you do

SynthDescLib.global.synthDescs.at(\beep);

With the var you get

-> SynthDesc 'beep' 
Controls:
ControlName  P 0 out scalar 0.0
ControlName  P 1 amp control 0.5
ControlName  P 2 freq control 440.0
   O audio ReplaceOut out 2

but with the arg instead you get

-> SynthDesc 'beep' 
Controls:
ControlName  P 0 out control 0.0
ControlName  P 1 amp control 0.0
ControlName  P 2 freq control 0.0
   O audio ReplaceOut out 2

Likewise if you “save” the SynthDef to a client var, and perform allControlNames on that object:

-> [ ControlName  P 0 out scalar 0, ControlName  P 1 amp control 0.5, ControlName  P 2 freq control 440 ]

vs

-> [ ControlName  P 0 out control 0.0, ControlName  P 1 amp control 0.0, ControlName  P 2 freq control 0.0 ]
1 Like

There’s probably too many of these having been re-invented; at least PdefGui, Instr.gui, and VarGui. But that’s probably best left for a different discussion.

By the way, how does this named control thing this play with NodeProxy's? Those expect function args, if I’m not mistaken… I don’t even know how to make NodeProxy type (e.g. \ar) its generated (actual SynthDef) controls.

Sure, but one needs to consider all hacky ways in which synths also get generated, e.g. NodeProxy. So a pre-processor might not be the best idea. Probably having the NamedControls in an environment, e.g. with Halos for the “extra stuff” like specs, might be the more general approach. Something like thisControls being predefined in a Synth or SynthDef like currentEnvironment is on the client. The named controls essentially define an environment like that anyway… except you cannot write (a working)

(d = SynthDef(\beep, {
	var out = ~out.ir(0), amp = ~amp.kr(0.5), freq = ~freq.kr(440);
	ReplaceOut.ar(out, amp * SinOsc.ar(freq).dup);
}).add;)

because the lookup for ~amp etc. is in currentEnvironment. Basically having a thisControls (or synthEnvironment) made explicit might be better.

Quick bullet point edition:

  • Synthesis function args look up their default values in the function’s prototypeFrame.
  • prototypeFrame contains only literal defaults. Arguments with expression defaults appear as nil in the prototypeFrame.
  • Probably (I haven’t investigated) SynthDef is building Controls for the arguments, and then \name.kr is looking for existing controls with the same name – and finding them. But \name.kr(value) shouldn’t blow away defaults from previously created control objects.

tl;dr you can use arg there, but you shouldn’t.

Part of the reason for that is that there’s no good one in the main library. (Also GUIs are somewhat personal.) By “good,” I mean general enough to handle a broad range of use cases with straightforward visual controls, and working with baseline SynthDefs. (I admit I haven’t looked at VarGui so I can’t comment on it.) There’s a makeGui for SynthDesc I think, but it’s pretty… erm… basic.

A comprehensive SynthDef GUI is a very hard design problem – I don’t really want to tackle it either!

Quite well, as I recall. I’ve used \name.ar in proxies and they do appear in the GUI.

Ok, I see your point. A preprocessor wouldn’t be good if it’s stashing specs outside of the synthesis function (as my original idea suggested).

But…

This is now merged in, so all of this is easier. (I’m still not crazy about the name duplication of var freq = \freq.kr but a preprocessor could do something about that.)

hjh

Yes, that’s correct, I’ve just tried

n = NodeProxy.audio(s, 2);
(n.source = { var out = \out.ir(0), amp = \amp.kr(0.5), freq = \freq.kr(440);
	ReplaceOut.ar(out, amp * SinOsc.ar(freq).dup); })
n.gui;

And the two params do show up. (out doesn’t but I think that’s “by design”).

Also the types get updated properly when changing the proxy source, e.g.

n = NodeProxy.audio(s, 2);
n.source = { var amp = \amp.ar(0.5), freq = \freq.ar(440); amp * SinOsc.ar(freq).dup }
n.controlNames // -> [ ControlName  P 0 amp audio 0.5, ControlName  P 1 freq audio 440 ]
n.source = { var amp = \amp.kr(0.5), freq = \freq.ar(440); amp * SinOsc.ar(freq).dup }
n.controlNames // -> [ ControlName  P 0 amp control 0.5, ControlName  P 1 freq audio 440 ]

Indeed GUIs are personal. Main lib has at least this:

SynthDescLib.global[\default].makeGui

But it’s not for controling synths and patterns at the same time. That was my motivation for writing VarGui - and I hate to write GUIs …
On the other hand I rarely work with proxies, and if, then I rather don’t want GUIs for them, so I didn’t include that in my design decisions. Maybe inevitable to tackle the own GUI concepts …

Amusingly enough perhaps, the auto-generated window title for that is “another control panel” :slight_smile:

The main advantage of NamedControls with NodeProxies seems to be not having to put the variable type in its name, i.e. one can also get typing via name prefixes (in SynthDef arg, in general), but this can be bit a annoying to carry through the code and/or change later if the type turns out not to be desired one (e.g. going from kr to ar to remove “zipper” noise.)

n = NodeProxy.audio(s, 2);
(n.source = { arg a_amp, a_freq; a_amp * SinOsc.ar(a_freq).dup; })
n.controlNames // -> [ ControlName  P 0 a_amp audio 0.0, ControlName  P 1 a_freq audio 0.0 ]

@nathan thanks a lot for this topic!

I was checking NamedControl reference and some crucial examples there are not working, specially those related to using different lag values for the same control name:

// multiple usage of the same name:
a = { SinOsc.ar(\freq.kr(440, 3.5)) + Saw.ar(\freq.kr(440, 0.05) * 0.5) * 0.1 }.play;

Should this be working or is not allowed anymore?

Is there anyone planning to improve NamedControl main documentation with the main stuff from this discussion ? This would be very helpfull and I think that miSCellaneous’ tutorial “Event Patterns and array args” examples would also be very handy to have in this main doc.

1 Like

I’ve logged an issue for this, since it reflects an error in the development process. (Such errors are less likely to occur now, because code changes are reviewed more thoroughly, including asking tough questions about changes that will break compatibility with existing code.)

I suspect, though, that after 7.5 years in the current state, we probably won’t change the behavior back.

But there is a fairly easy way to get the same effect: move the lag value outside the kr parens and call the .lag UGen method:

a = { SinOsc.ar(\freq.kr(440).lag(3.5)) + Saw.ar(\freq.kr(440).lag(0.05) * 0.5) * 0.1 }.play;

hjh

2 Likes

If using NamedControl I’d prefer to have it defined at one place

a = { 
	var freq = \freq.kr(440);
	SinOsc.ar(freq.lag(3.5)) + 
	Saw.ar(freq.lag(0.05) * 0.5) * 0.1 
}.play;

a.set(\freq, 500)

But, admittedly, then we’re again close to standard args :slight_smile:
IMO the main advantage of NamedControl occurs together with array args, but it’s probably one of those syntax questions where a component of personal taste comes into play.

1 Like

Being new at all this I tried both systems and liked the NamedControl. This until I started (over) documenting the code.

(
SynthDef.new(\basicFM, {
	/*: basic frequency modulator using one carrier and two modulators
	and an envelope.

	:carrier:
	carHz (Hz, 500): carrier frequency (to be modulated by modulated modulator 1).
	amp (float, 0.5): attenuation, amplification of the modulated carrier.
	:modulator 1:
	modHz (Hz, 100): modulator frequency (to be modulated by modulator 2).
	modAmp (float, 200): attenuation, amplification of the modulator.
	:modulator 2:
	mod2Hz (Hz, 100): modulator frequency (modulates modulator 1).
	mod2Amp (float, 200): attenuation, amplification of the modulator.
	:envelope:
	atk (s, 0.01): attack time.
	rel (s, 1): release time.
	:soundfield:
	pan (float, 0): position of signal in soundfield.
	*/
	var car, carHz=\carHz.kr(500), mod, mod2, env;
	env = EnvGen.kr(Env.perc( \atk.kr(0.01), \rel.kr(1) ), doneAction:2);
	mod = SinOsc.ar(\modHz.kr(100), mul: \modAmp.kr(200));
	mod2 = SinOsc.ar(\mod2Hz.kr(100), mul: \mod2Amp.kr(200));
	car = SinOsc.ar(carHz + (mod + mod2)) * env * \amp.kr(0.2);
	car = Pan2.ar(car, \pan.kr(0));
	Out.ar(0, car);
}).add;
)
(
SynthDef.new(\basicFM, {
	/*: basic frequency modulator using one carrier and two modulators
	and an envelope.*/

	//:carrier:
	arg carHz=500 /*:(Hz) carrier frequency (to be modulated by modulated modulator 1).*/
	,amp=0.5      /*:(float) attenuation, amplification of the modulated carrier.*/
	//:modulator 1:
	,modHz=100    /*:(Hz) modulator frequency (to be modulated by modulator 2).*/
	,modAmp=200   /*:(float) attenuation, amplification of the modulator.*/
	//:modulator 2:
	,mod2Hz=100   /*:(Hz) modulator frequency (modulates modulator 1).*/
	,mod2Amp=200  /*:(float) attenuation, amplification of the modulator.*/
	//:envelope:
	,atk=0.01     /*:(s) attack time.*/
	,rel=1        /*:(s) release time.*/
	//:soundfield:
	,pan=0        /*:(float) position of signal in soundfield.*/
	;
	var car, mod, mod2, env;
	env = EnvGen.kr(Env.perc(atk, rel), doneAction:2);
	mod = SinOsc.ar(modHz, mul: modAmp);
	mod2 = SinOsc.ar(mod2Hz, mul: mod2Amp);
	car = SinOsc.ar(carHz + (mod + mod2)) * env * amp;
	car = Pan2.ar(car, pan);
	Out.ar(0, car);
}).add;
)
1 Like

Good point. This also confirms what Julian Rohrhuber is often emphazising, choosing speaking names for arguments and variables is extremely important. You have chosen meaningful names. If they are collected at the top of the code as arguments it might save documentation in many cases.
carHz might be unusual but why not, passing it as midi number would also be possible and that could then be named carMidi. Any naming that is meaningful to you (especially after a work break of some weeks or so!) is fine.

How do those NamedControl work with SynthDef.wrap and “prependArgs” ?

In this mcve example, the ~mks function builds a Synth using a function and (tries to) set an argument (here the freq) to a different value than the default one using the “prependArg” argument of the “wrap” method .

With “arg” style arguments, it is working fine.
With the “NamedControl” style, it is not working.

~mks = {
	arg name, func, lags;
	SynthDef(name, {
		var out=\out.kr(0);
		Out.ar(out,Pan2.ar(SynthDef.wrap(func, lags, [220]),0));  // <-- prependArgs 
	}).add;
};

~sine= { |freq=440| SinOsc.ar(freq,0.2); } // <-- synthdef function with "arg" style arguments
~mks.value(\sine,~sine);
Synth(\sine);  // ==> play a 220Hz sine : OK

~sineNC= { SinOsc.ar(\freq.kr(440),0.2); }  <-- synthdef function with "NamedControl" style 
~mks.value(\sineNC,~sineNC);
Synth(\sineNC);   // ==> play a 440Hz sine : KO

Is this the limit of NamedControl ?

I don’t think this example is “OK” in fact – if you had saved it in a variable and then tried ~mySynth.set(\freq, 330), you would have discovered that there is no freq control at all.

Function arguments in a SynthDef are handled like this:

  1. Collect the names and (literal) default values from the FunctionDef.
  2. Build a Control containing all of these.
  3. Build an array of the Control’s OutputProxies (if there are any literal array defaults, clump the OutputProxies into the same shape).
  4. Pass the OutputProxies to the function arguments.

prependArgs allows you to supply values for the leading arguments – but this bypasses the full argument → Control process for these arguments. SynthDef.wrap will create a Control for unassigned arguments, but pass your values through directly. So the 220 does not mean “a Control with default = 220” – it’s literally (pun :wink: ) just the hardcoded number.

It will probably work better like this.

~mks = {
	arg name, func, lags;
	SynthDef(name, {
		var out=\out.kr(0);
        var freq = \freq.kr(220);
		Out.ar(out,Pan2.ar(SynthDef.wrap(func, lags, [freq]),0));  // <-- prependArgs 
	}).add;
};

The argument freq will receive the control as its value. The inner NamedControl will find freq as a previously-created control (and probably warn you about the default value discrepancy, but it should work in the end).

hjh