NamedControl: a better way to write SynthDef arguments

@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.

1 Like

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

The discussion is more theoretical than practical. And is about : can we replace old style argument in every circumstances by named controls ?

If I go back to my simple example and your suggestion, it doesn’t work either. Worth it breaks with an error.

// -- v2 -- 
~mks2 = {
	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;
};


// "arg" style arguments
~mks2.value(\sine2,~sine);
Synth(\sine2);  // ==> play a 220Hz sine : OK

// named controls
~mks2.value(\sineNC2,~sineNC); //<-- KO: control freq has twice a default value
Synth(\sineNC2);   // <-- KO, No Synth

So my conclusion is (for now) that if one wants to have an argument with a default value in the wrapped function and set another default value when building the Synth, one must use traditional arguments in the wrapped function.

And that is a usage limit of the NamedControls.

1 Like

You certainly can’t. If this thread suggests that NamedControls and arguments are interchangeable, then it would be overselling its case. I don’t think the thread actually makes this claim, but it might not be doing enough to prevent users such as yourself from drawing a conclusion that they are (or should be) interchangeable.

Arguments are converted to controls by the process I outlined above. This involves one step (.value) that allows objects other than auto-generated control outputs to be passed in. SynthDef.wrap depends on this for prependArgs.

NamedControls bypass the conversion process. So, they also bypass the mechanism whereby prependArgs are injected – so they are fundamentally incompatible with prependArgs.

Oh, I see – I misrecalled. I thought it was a warning rather than a fatal error.

In any case, this is not unusual in programming – something looks like a general alternative, until corner cases are found.

(FWIW I never fully agreed with the premise of the thread. I would say that NamedControls have some advantages in some situations, and I’d even say that the situations where NamedControls are advantageous are the most common use cases for synth inputs. But to say flatly that NamedControls are universally better is a mild overstatement IMO, which led to an inflated expectation here.)

hjh

2 Likes

I feel like there is room for improvement here though I may lack the wits/knowledge to offer it.

I think that the “magic” that SC does in converting functions to UGen graphs is on the one hand super neat… at first we are presented with a seamless transition from reasoning about functions to getting synths running on the server… but eventually more and more roadblocks start popping up because so much of the real process is hidden. You have to use a Rand object instead of rand, arguments are controls so their types are constrained. literal array default values are needed. Conditionals don’t work in Synthdefs, UGens can only be defined inside functions and on and on.

It might be nice to have a more “honest” Synthdef object, with explicit controls, as the default. And then have an .asSynthDef method to converts functions which could perhaps have some verbose error/warning posting.

Even if that’s a dumb idea I do think that the current scheme draws new users into entirely predictable confusions and errors !

Finally SynthDefs need be composable - if the current way of doing that doesn’t work with named controls then perhaps there’s an improvement possible.

I considered about 5 ways to respond to this and none of them was very good :laughing: I think that’s because I’m not quite clear whether the improvement you would like to see goes in the direction of more or less seamless – on the one hand, if the roadblocks are unwelcome, then removing them would be good, but on the other, the idea of “a more ‘honest’ SynthDef” seems to suggest that exposing the seams might be better.

I’ve got some specific comments about a couple of the roadblocks. Others would have to wait for another time.

  • You have to use a Rand object instead of rand – from experience answering questions here, often the formulation of a requirement is not sufficiently detailed. A user thinks “I want a random number,” but is that a/ one random number that will never change (rrand), b/ one random number per synth instance (Rand), or c/ one random number per kr or ar sample (WhiteNoise or… ha ha, get this: { DC.ar(1).rand2 }.plot)? That is, I’m noting that your comment assumes “b” but that won’t be true in every case.
    If I haven’t clarified in my own mind what I want, of course I’m going to run into trouble… and, it takes time to learn which aspects of a requirement are important to clarify upfront (I don’t know that I need to know Z until I’ve learned X and Y). A different object design will not relieve users of the need to understand what they want in sufficient detail to express it accurately.

  • conditionals don’t work in Synthdefs – similar to the first roadblock – at what rate should the conditional evaluate?

  • arguments are controls so their types are constrained – first problem is calling them “arguments.” For a while now, I’ve tried to be consistent about referring to “synth inputs” or “control inputs” rather than arguments. Writing \freq.kr is one way to get away from the term “argument,” but this loses effectiveness if we then turn around and call it “argument.” In { |freq = 440| SinOsc.ar(freq) }.play, a function argument is used to produce a control input. It’s unfortunate that SC culture has evolved to elide this into the single term “argument.”

  • literal array default values are needed – I can think of a way to handle this, but it would require massive surgery on the interpreter – high risk. Probably won’t happen.

Putting these two comments together, as I think they are related.

There’s a casual assumption here that SynthDefs should be composable into a SynthDef. In SC culture, SynthDef is the universe. I don’t think so – I think SynthDef is a molecule.

I had an idea a while ago, but I think I will never have time to implement it (it’s already the case that I have no time for my own composition :cry: ), of a synthesis Module object. Each Module would translate into one SynthDef. Module composition would allocate buses as needed for inputs and outputs – if module C consists of module A → module B, then module C would be aware of A’s outputs and B’s inputs, and playing an instance of C on the server would automatically grab buses for the duration of the instance, and automatically release the buses when C stops. C would manage node ordering. C could compose with D and E to form F, and so on.

If this were done properly (and there’s no technical reason why we couldn’t), in the main class library, then the documentation could be revised to use Modules where SynthDefs appear now. And then the whole conversation would be different.

Like I said the other day – because SynthDef is everywhere in the documentation, we assume that SynthDef should be able to do everything, including composition. But we need SynthDef to remain the language-side representation of the server GraphDef. I think building superstructures on top of the existing architecture would be the way to go. Part of the static here is pressing SynthDef into service of needs that SynthDef simply shouldn’t be managing.

hjh

2 Likes

I’d love to have this.
I’m personally trying to go in that direction by composing my Synthdef as a sequence of reusable wrapped functions and custom code.

We could have a new “Rate” “cr” for “Constant Rate” or “Static Rate”. Then your 3 examples woul be become
a) var a=Rand.cr() // static, same for all Synths
B)var a=Rand.ir() // constant by Synth
C)var a=Rand.kr() // new value at each control rate

thought: perhaps sythdef functions should not use arguments to make controls - there could be a fourth(?) type declaration - var, arg, ctrl, const. The current implementation is elegant when it works but brings about one user a week here with questions about why you can’t pass a symbol into a SynthDef!

Maybe SynthDef functions should have a distinguishing brace and/or be subbclassed from Function. Again to help users anticipate the constraints.

and +1 for Modules…

1 Like

You could do this with the preprocessor. It would be a tricky parsing job but it’s possible to recognize “ctrl” declarations and replace them with NamedControl syntax.

At this point it’s starting to look like over-engineering.

I’d be extremely skeptical (even hostile) toward language syntax changes in the SC3 line that break compatibility with existing SC3 code. Forbidding synth controls from being declared as arguments shouldn’t be done in SC3 (and there’s currently no proposal for an SC4).

So then we are brainstorming about adding Yet Another Way to declare synth controls… and “too many ways to do the same thing” has also been cited as a point of confusion.

If the suggestion is to increase complexity, then it should solve a real problem. Confusion over synth controls is a problem, sure, but is it a five-alarm fire, or more of a nuisance? “One user a week” strikes me as exaggerated – 50+ questions a year?

Also part of the solution would be to explain, in the documentation, the exact process of converting arguments to controls, and the constraints. I think the help doesn’t really do that, leaving users to guess.

I’d love it if someone could take that up.

In the crucial library quark, there’s Instr and Patch, but they don’t fully integrate with the event system. I found composing them to be a bit fiddly. But interested people could have a look to see some of the technical problems. For example, an Instr with a StaticIntegerSpec argument for number of channels could generate many SynthDefs, that need to be distinguished based on the value of that argument. Serializing static argument values into a SynthDef name is harder than it sounds. The time demands of technical issues like that are why I can’t take this project on, sorry.

hjh

Thinking about it some more – I don’t want to be too dismissive of the idea of an improvement in control specification. (I even started once on a preprocessor for it, but ran out of time and dropped it.)

At the same time, my experience finally coming to grips with Max/Pd style graphical patching makes me question the notion that design can significantly reduce user confusion. (It can reduce it a little, and that may be worth doing.)

After some 40 years’ exposure to programming languages, and 18+ now with SC, I wouldn’t have considered myself a novice – but I floundered for the better part of a year with Pd. In hindsight, that was an excellent, and usefully humbling experience. I found that fluency in a programming environment arises from trying and discarding wrong approaches, and I now believe there is simply no substitute for that. Design can trim away parts of the large field of wrong approaches, but every new design feature may be misunderstood in a new way, so the field never fully disappears.

A control keyword is not a bad idea (and could be implemented now with a preprocessor). OTOH, a design process for a permanent language feature should begin by researching concretely where users get tripped up with synthesis function arguments – otherwise, you end up adding something that might not address the confusion, and then it has to be permanently maintained.

I’m curious how relatively new users feel about this – big problem, minor confusion? What specifically was irritating while learning it?

hjh

1 Like

Frankly I never found the built-in lags that Control (probably via LagControl) comes with very useful. The built-in lag curve always seemed wrong somehow (it departs so sharply from the present value, i.e. at high derivative, that it easily causes audible glitching when used on a phase or frequency) so I either end up using .lag2 or lag3 often enough in their ud variation or .varlag, which is linear by default, and map that e.g. through a sine shape.

I suppose LagControl is an attempt at convenience plus optimization, but it ends up being not very useful in my experience.

I suppose an interesting application is to exploit the fact that the names are strings when passed to NamedControls do a sort of meta-programming to generate multiple controls from the same function “pattern” as discussed here. In a nutshell example:

(
f = { Poll.kr(Impulse.kr(1), ~ctrl.asSymbol.kr, ~ctrl) };

SynthDef("funny_ctrls", {
	SynthDef.wrap({ (ctrl: "boo").use { f.value } });
	SynthDef.wrap({ (ctrl: "yoo").use { f.value } })
}).add;

x = Synth("funny_ctrls");

s.queryAllNodes(true)
)

And I apologize for not reading all the 50+ messages above, where this might have been mentioned already, but NamedControl presently can’t be mixed too well with older ways of doing things if there’s expectation of a shared namespace, which the documentation alas created for me.

It’s also not the case that arg-generated Controls s mix gracefully with Control.names either, duplication-wise, or even with themselves in case of SynthDef.wrap.

I’m doin’ it. Ya can’t stop me

3 Likes

My observation, based on a couple recent threads about NamedControl, is that SynthDef arguments cause some types of confusion and NamedControls cause other types of confusion. In both cases, SynthDef and its inputs prove to be abstract concepts that often require trial and error to understand.

hjh

2 Likes