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