`Control.names`: the missing manual

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

Not entirely. The server checks the size of arrays passed to arrayed controls, to prevent overflow. (At least it did, the last time I checked.) I believe this applies to Control.names as well as NamedControl.

hih

Hmm, I’m curious to know for sure if this is true - I have very strong memories of creating BAD bugs for myself by passing arrays of values that are too long for the control I’m setting. I would rest easier if I knew that this was actually “safe” to do, because I’m usually very paranoid…

It’s a two minute test, you don’t have to take my word for it :stuck_out_tongue_winking_eye:

(
a = { |a = #[1, 2], b = 3|
	Poll.kr(Changed.kr(b), b);
	Silent.ar(1)
}.play;
)

a.set(\b, 5);  // prints, good

// this is an array overflow! setting 3 into a 2-element array arg
a.set(\a, [10, 20, 30]);

// didn't print -- so it seems b was ignored

a.trace;

TRACE 1000  temp__0    #units: 12
  unit 0 Control
    in 
    out 10 20 5   -- 10 and 20 went through, 30 did not

hjh

1 Like

I can confirm that if you use names for controls, the check is implemented server-side, even for /n_setn. However if you use numbers for indexing, then there’s no “type check” preventing any overruns, not even across control types, meaning you can even overrun from i_r into adjacent k_r for instance.

(
a = { |a = #[1, 2], b = 3|
	Poll.kr(Changed.kr(b), b);
	Silent.ar(1)
}.play;
)

s.sendMsg("/n_setn", a.nodeID, "a", 3, 44, 55, 66)
// ^^ a array bound enforced

s.sendMsg("/n_setn", a.nodeID, 1, 3, 44, 55, 66)
// NOT enforced

s.sendMsg("/n_setn", a.nodeID, 0, 4, 0, 44, 55, 77)
// also not enforced, this writes starting from the i_out

s.sendMsg("/n_setn", a.nodeID, 0, 0, 3, 44, 55, 88)
// obscure feature (or bug/): zero size makes it write 55 into b!

In that code there’s offset of 1 there because that code actually uses JITLib underneath, so there’s i_out as the first ctrl at 0.

To not get that JITLib interference, you have write it like

(d = SynthDef(\hjh, { |a = #[1, 2], b = 3|
	Poll.kr(Changed.kr(b), b);
	Silent.ar(1)
}).add)

a = Synth(d.name)

s.sendMsg("/n_setn", a.nodeID, "a", 3, 44, 55, 66)
// ^^ a array bound still enforced

s.sendMsg("/n_setn", a.nodeID, 0, 3, 44, 55, 66)
// array bound NOT enforced

I picked up the difference because I have some custom debugging code enable in the SynthDef class, I could instantly tell the kind of graph it produced :slight_smile: