I’ve been exploring the original Control.names
way of dynamically adding controls for the purpose of understanding what it can and cannot do, so that it can be integrated in a unified namespace with NamedControl. (For plain arg
-generated controls, I’ve already done that unification to a pretty good extent.)
So here are my geeky notes on how Control.names
works, how it fails, and how the sever (and SynthDesc which imitates server behavior pretty well, because it reloads the actual graph data stream sent to the server) surprisingly fix those failures so you probably don’t they can happen… most of the time. I’ll also explain how SynthDef works “more properly” by not using actually Control.names
but instantiating ControlName
s directly (itself a confusing name distinction, perhaps). And I’ll get to the white lie that the default values printed by ControlNames actually are.
So let’s start with
(d = SynthDef(\hmm, {
var woot = Control.names([\xx, \yy]).kr([23, 45, 67]);
var loot = Control.names([\aa, \bb]).kr([3, 5, 7]);
Poll.kr(Impulse.kr(0.2), woot, "Woot:");
Poll.kr(Impulse.kr(0.2), loot, "Loot:");
}))
d.allControlNames do: _.postln
/*
ControlName P 0 xx control
ControlName P 1 yy control [ 23, 45, 67 ]
ControlName P 3 aa control
ControlName P 4 bb control [ 3, 5, 7 ]
*/
The controls appear a bit borken, as the last one in each “issue” appears to eat up all the default values. But the odd bit is that if you play that on the server, it actually works as one might have intended it, keeping in mind that the implied semantics in Control.names
is that every name but the last one in its arg list is going to be a single-channel control. Basically any excess values later issued to a Control.kr
(or similar) methods are going to be assigned to the last name, making only that last name a multi-channel control. (Of course, one could recall here that all controls are really multi-channel if you consider /n_setn
in the raw server protocol which sets a specified number of control slots starting at one index. The distinction between single channel and mutli-channel controls is only relevant for the client-side abstraction and more obvious in GUIs like NdefGui. So if you want me to phrase that more properly: all names given to one Control.names
call denote consecutive positions in the synth control array, with an index difference of one between names sequenced in the same .names
call.)
As I hinted previously, SynthDesc actually does a bit of fixup on those ControlNames, actually rebuilding them.
d.add
d.desc
/*
Controls:
ControlName P 0 xx control 23.0
ControlName P 1 yy control [ 45.0, 67.0 ]
ControlName P 2 ? control 67.0
ControlName P 3 aa control 3.0
ControlName P 4 bb control [ 5.0, 7.0 ]
ControlName P 5 ? control 7.0
*/
So what happens if you issue to few (e.g. Control.kr
) values after one Control.names
call before you issue the next Control.names
? Well, the unassigned names first seem to clobber the next array, but on the fixup that SynthDesc does, the extra names simply disappear.
(d = SynthDef(\hmm, {
var woot = Control.names([\xx, \yy]).kr([23]); // << fewer values
var loot = Control.names([\aa, \bb]).kr([3, 5, 7]);
Poll.kr(Impulse.kr(0.2), woot, "Woot:");
Poll.kr(Impulse.kr(0.2), loot, "Loot:");
}))
d.allControlNames do: _.postln
/*
ControlName P 0 xx control
ControlName P 1 yy control 23
ControlName P 1 aa control
ControlName P 2 bb control [ 3, 5, 7 ]
*/
d.add
d.desc
/*
Controls:
ControlName P 0 xx control 23.0
ControlName P 1 aa control 3.0
ControlName P 2 bb control [ 5.0, 7.0 ]
ControlName P 3 ? control 7.0
*/
In the above example, yy
has completely disappeared in SynthDesc’s view. I verified with the sever that it has the same interpretation:
x = Synth(d.name)
x.set(\yy, 999) // no effect whatsoever now
As far as regular SynthDef control builds from function args, Control.names
isn’t actually called. The reason for this is somewhat obvious from the above and knowing the fact that SynthDef constructs all the ControlNames in one pass then emits all the Controls in a second pass, but the Controls it emits are grouped by type (ir, kr, etc.) so as to minimize the number of UGens. That exact approach is rather impossible to do with Control.names
directly, although I suppose it could be done to a good approximation if one were to “emit” all ir
names, then one ir
control, then do the same for tr
etc. Perhaps in a early version of SC that’s how it might even have been done.
So if you wonder how SynthDef actually does its stuff, it looks like the following, in a simplified example, because it gets verbose…
(d = SynthDef(\hmm, {
var woot;
UGen.buildSynthDef.addControlName(
ControlName.new(\xx, 0, \control, 23));
UGen.buildSynthDef.addControlName(
ControlName.new(\yy, 1, \control, 45));
woot = Control.kr([111, 222]);
Poll.kr(Impulse.kr(0), woot, "Woot:");
}))
I’ve deliberately put different values for the default values in the ControlNames vs actual Controls. If you wonder which one(s) ultimately win, your correct bet is on the latter. But the former are actually shown initially if you ask SynthDef even after it has built the graph…
d.allControlNames do: _.postln
// ControlName P 0 xx control 23
// ControlName P 1 yy control 45
That doesn’t fool SynthDesc though and again the sever has the same behavior
d.add
d.desc
/*
Controls:
ControlName P 0 xx control 111.0
ControlName P 1 yy control 222.0
*/
x = Synth(d.name)
// Woot:: 111
// Woot:: 222
There’s one last point of subtlety here. Besides being added to ControlNames for correct display purposes, the presence of the default values in those ControlNames objects that were explicitly built (via constructor) is to prevent Control.init
(which is called by Control.kr
) from overwriting the last defauilt value in the current SynthDef ControlNames array (which is called… controlNames
). The source of the odd displays in the first example I gave here is precisely that Control.init
can either not write at all any default values back to the synthdef controlNames
array, or it will write all of them in the last slot of that array. It’s easier to show the code for that than to explain it, really:
lastControl = synthDef.controlNames.last;
if(lastControl.defaultValue.isNil) {
// only write if not there yet:
lastControl.defaultValue_(values.unbubble);
}
And another note. There’s seemingly simplified interface in SynthDef as in
(d = SynthDef(\hmm, {
var woot;
UGen.buildSynthDef.addKr(\xx, 23);
UGen.buildSynthDef.addKr(\yy, 45);
woot = Control.kr([111, 222]);
Poll.kr(Impulse.kr(0), woot, "Woot:");
}))
But that fails to fails to increment (add to the) controls…; so \xx
and \yy
go to the same 0 index!!
d.allControlNames do: _.postln
//ControlName P 0 xx control 23
//ControlName P 0 yy control 45
And that certainly causes a loss of control names on the server (it’s similar to not providing enough values to Control.names
)
d.add; d.desc
// ControlName P 0 yy control [ 111.0, 222.0 ]
// ControlName P 1 ? control 222.0
So that addKr
interface normally followed by an additional buildControls
step that does a fixup on those indices. For that reason is not really useable except internally by SynthDef for arg
-derived controls.
It sort-of useable if you limit yourself to alternating calls to that addKr
with
calls to create the actual controls, which will increment the controls.size
that’s used as an index by addKr
. It’s actually what NamedControl does (i.e. ensures that alternation of calls). Something like
(d = SynthDef(\hmm, {
var woot1, woot2;
UGen.buildSynthDef.addKr(\xx, 23);
woot1 = Control.kr(111);
UGen.buildSynthDef.addKr(\yy, 45);
woot2 = Control.kr(222);
Poll.kr(Impulse.kr(0), [woot1, woot2], "Woot:");
}))
//ControlName P 0 xx control 23
//ControlName P 1 yy control 45
is ok. You also can’t pass an array of names to addKr
:
(d = SynthDef(\hmm, {
var woot;
UGen.buildSynthDef.addKr([\xx, \yy], [23, 45]);
woot = Control.kr([111, 222]);
Poll.kr(Impulse.kr(0), woot, "Woot:");
}))
// ControlName P 0 [ xx, yy ] control [ 23, 45 ]
d.add
/*
ERROR: syntax error, unexpected '[', expecting ELLIPSIS
in interpreted text
line 1 char 8:
#{ arg [ xx, yy ];
^
var x72A4A139 = Array.new(2);
-----------------------------------
ERROR: Command line parse failed
*/
So you have to be really careful with addKr
if used directly.