Preloading SynthDef func args into NamedControls: (almost) unified control namespace

Per previous discussion here, which got pretty long, it would desirable if NamedControls actually saw the arg declared controls, instead of living in a completely separate namespace.

Here’s my solution so far, which does work for the basic use case. Kind of amazed that it was this simple, due to the magic of OutputProxy doing the right wrapping of everything.

+ SynthDef {
	buildUgenGraph { arg func, rates, prependArgs;
		var result;
		// save/restore controls in case of *wrap
		var saveControlNames = controlNames;
		var controlProxies;

		controlNames = nil;

		prependArgs = prependArgs.asArray;
		this.addControlsFromArgsOfFunc(func, rates, prependArgs.size);
		// at this point controlNames is loaded with the ControlNames for the current func
		controlProxies = this.buildControls; // OutputProxies for the Control objects
		NamedControl.preload(controlProxies); // controlNames is also used in preload
		result = func.valueArray(prependArgs ++ controlProxies);

		controlNames = saveControlNames

		^result
	}
}

+ NamedControl {

	//var <name, <values, <lags, <rate, <fixedLag;
	//var <control;

	*preload { arg controlProxies;
		var bsdCNs; // ControlNames for current SynthDef function

		this.initDict; // this also ensures buildSynthDef is valid
		bsdCNs = buildSynthDef.controlNames;

		bsdCNs.do { |cno, i|

			var newNC = super.newCopyArgs(
				cno.name, cno.defaultValue.asArray, cno.lag,
				cno.rate, true, controlProxies[i]);
			currentControls.put(cno.name, newNC);

			// just debug code to see if it matched the right OPs to names
			if(currentEnvironment.notNil) { if(~debugNC == 1) {
				["preloading:", cno.name,
					controlProxies[i].asArray.collect({ |cop|
						[cop.name, cop.source, cop.outputIndex] })].postln } };

		}
	}
}

A simple test of the now-unified ctrl namespace:

(d = SynthDef(\boo) { arg c1 = #[11, 14], c2 = 22;
	Poll.kr(Impulse.kr(0), \c1.kr, "C1:");
	Poll.kr(Impulse.kr(0), \c2.kr, "C2:");
	Out.kr(12, \c1.kr + c2) })

d.allControlNames // expected num of ctrls, good
d.dumpUGens /// no extra Control stuff created, good

d.add

x = Synth(d.name) // correct values printed, i.e. from args.

It also detects errors as in different init values in the unified space… in one direction, i.e. NamedControl picks up conflicts.

SynthDef(\noo) { arg c1 = 11; \c1.kr(22) } // err caught now

You can still override stuff undetected with inner func frames like an alising SynthDef.wrap etc.

SynthDef(\noo) { arg x = 11; SynthDef.wrap { arg x = 22; } } // still not caught

I could add a check for that since the inner frame preload will see the old value. But I’m not sure if I should because that would change “old schood” non-NameControl semantics.

Also, I need to do more tests with more the complicated swizzling that diffrent type of args (ar, tr, ir) introduce due to the ugen reduction (grouping) that SynthDef does for args Controls. Before I have to let go of this for a while. A quick swizzling test does seem to get them in the right order even in the NamedControl namespace.

~debugNC = 1;
(
d = SynthDef(\aa) { arg x = 3, a_i, y = 4 , t_t = 1;
	Poll.kr(\t_t.tr, \x.kr + \y.kr);
	Out.ar(9, a_i) }
)

/* posts something like

[ preloading:, x, [ [ x, a Control, 0 ] ] ]
[ preloading:, a_i, [ [ a_i, an AudioControl, 0 ] ] ]
[ preloading:, y, [ [ y, a Control, 1 ] ] ]
[ preloading:, t_t, [ [ t_t, a TrigControl, 0 ] ] ]

which is expected because SynthDef groups arg-generated Controls 
by type into single giant multi-channel ones that need indexing */

d.add

x = Synth(d.name)
x.set(\t_t, 1) // retrigs

Worth noting thought that this doesn’t completely unify the control namespace. Because totally manual adds using the old school Control.names(\yo).kr is not handled. That is actually rather hard to catch, as discussed in the linked thread. Mainly because the old interface allows one to add stuff out-of-order (add name first or emit ugen first) with any amount of other ugens emitted to the graph in-between.

The smarter-bear version of NamedControl that also imports dynamic uses of Control.names().kr etc… correctly as long as you only pass one name to Control.names() because otherwise the fixup is rather complicated due to the bad structs that Control.names() emits (see linked thread about it’s missing manual.)

Before I dump the class code, here with example what I’m talking about.

~debugNC = 1;
~debugCM = 1;
(d = SynthDef(\hmm, {
	var woot1, loot1, woot2, loot2;
	woot1 = Control.names([\xx]).kr([23]);
	woot2 = Control.names([\yy]).kr([45]);
	loot1 = \xx.kr(23);
	loot2 = \yy.kr; // ok to give nothing here,
	// but it val differs from name.kr(...) above it will catch it!
	Poll.kr(Impulse.kr(0), [woot1, woot2], "Woot:");
	Poll.kr(Impulse.kr(0), [loot1, loot2], "Loot:");
}))

/*
>>>ControlMap.add ?true? [ 0, a Control, [ an OutputProxy ] ]
>>>ControlMap.add ?true? [ 1, a Control, [ an OutputProxy ] ]
NaCo importing:  ControlName  P 0 xx control 23
  >--hooked-to--> a Control spIndex:0 #chans:1 offset:0 
NaCo importing:  ControlName  P 1 yy control 45
  >--hooked-to--> a Control spIndex:1 #chans:1 offset:0 
*/

d.dumpUGens

/*
[ 0_Control, control, nil ]
[ 1_Control, control, nil ]
[ 2_Impulse, control, [ 0, 0.0 ] ]
[ 3_Poll, control, [ 2_Impulse, 0_Control[0], -1, 5, 87, 111, 111, 116, 58 ] ]
[ 4_Poll, control, [ 2_Impulse, 1_Control[0], -1, 5, 87, 111, 111, 116, 58 ] ]
[ 5_Impulse, control, [ 0, 0.0 ] ]
[ 6_Poll, control, [ 5_Impulse, 0_Control[0], -1, 5, 76, 111, 111, 116, 58 ] ]
[ 7_Poll, control, [ 5_Impulse, 1_Control[0], -1, 5, 76, 111, 111, 116, 58 ] ]
*/

You can see the same Control ugens are properly hooked. And it runs ok of course:

x = Synth(d.add.name)
// Woot:: 23
// Woot:: 45
// Loot:: 23
// Loot:: 45

What’s not yet supported the fully weirdness of the names() interface when multiple names are passed.

(d = SynthDef(\hmm, {
	var wootC, loot1, loot2;
	wootC = Control.names([\xx, \yy]).kr([23, 45]);
	loot1 = \xx.kr;
	loot2 = \yy.kr;
	Poll.kr(Impulse.kr(0), wootC, "Woot:");
	Poll.kr(Impulse.kr(0), [loot1, loot2], "Loot:");
}))

/*
>>>ControlMap.add ?true? [ 0, a Control, [ an OutputProxy, an OutputProxy ] ]
NaCo importing:  ControlName  P 0 xx control
  >--hooked-to-->[ a Control spIndex:0 #chans:1 offset:0 , a Control spIndex:0 #chans:1 offset:1  ]
WARNING: NamedControl could not find Control OutputProxies for index #1.
  NamedControl does not (yet) support importing ControlNames with misleading
  default values, such as those generated by Control.names(list) with list.size > 1.
>>>ControlMap.add ?true? [ 2, a Control, [ an OutputProxy ] ]
*/

In this case the graph will be bad, as you can already guess from the above
as a the debug message already hints that a 2nd control was created for loot2.

For me personally it’s not (yet) worth fixing that case.

Multi-channel controls mapped to single name via .names is handled correctly though.

(d = SynthDef(\hmm, {
	var wootC, lootC;
	wootC = Control.names([\xxyy]).kr([23, 45]);
	lootC = \xxyy.kr;
	Poll.kr(Impulse.kr(0), wootC, "Woot:");
	Poll.kr(Impulse.kr(0), lootC, "Loot:");
}))

/*
>>>ControlMap.add ?true? [ 0, a Control, [ an OutputProxy, an OutputProxy ] ]
NaCo importing:  ControlName  P 0 xxyy control [ 23, 45 ] 
  >--hooked-to-->[ a Control spIndex:0 #chans:1 offset:0 , a Control spIndex:0 #chans:1 offset:1  ]
-> a SynthDef
*/

The number of channels (numChannels) seems incorectly set inside the Control objects, but that’s not my bug. I guess there’s some fixup code somewhere later in the SynthDef. It runs properly and the graph is correct too.

d.dumpUGens
/*
[ 0_Control, control, nil ]
[ 1_Impulse, control, [ 0, 0.0 ] ]
[ 2_Poll, control, [ 1_Impulse, 0_Control[0], -1, 5, 87, 111, 111, 116, 58 ] ]
[ 3_Poll, control, [ 1_Impulse, 0_Control[1], -1, 5, 87, 111, 111, 116, 58 ] ]
[ 4_Impulse, control, [ 0, 0.0 ] ]
[ 5_Poll, control, [ 4_Impulse, 0_Control[0], -1, 5, 76, 111, 111, 116, 58 ] ]
[ 6_Poll, control, [ 4_Impulse, 0_Control[1], -1, 5, 76, 111, 111, 116, 58 ] ]
*/

Here’s the class code:

ControlMap {

	classvar <>byIndex, <>isEnabled = false, <buildSynthDef;

	*reinitIfNeeded { // don't really want at initClass time here

		if(UGen.buildSynthDef !== buildSynthDef or: byIndex.isNil) {
			ControlMap.byIndex = SparseArray.new;
			buildSynthDef = UGen.buildSynthDef;
		}
	}

	*add { arg ctrlUGen, itsOutputProxies;

		if(currentEnvironment.notNil) {	if(~debugCM == 1) {
			">>>ControlMap.add ?".post;
			this.isEnabled.post; "? ".post;
			[ctrlUGen.specialIndex, ctrlUGen, itsOutputProxies].postln;
		}};

		if(this.isEnabled.not) { ^this };

		this.reinitIfNeeded;

		if(ctrlUGen.class.isControlUGen) {
			this.byIndex[ctrlUGen.specialIndex] = itsOutputProxies;
		} {
			"Non-conotrol UGen ignored by ControlMap.".warn
		}
	}
}

NaCoExtras {

	// should really be in the NamedControl class, but that's not possible from extention files
	classvar <>aCNimportedIndex = 0;
}

+ SynthDef {

	// mostly the same as the preload-only version, but now also disables dynamic
	// updates during preloading, because the NamedControl dynamic updating code
	// doesn't handle the highly swizzeled controls that buildControls produces
	buildUgenGraph { arg func, rates, prependArgs;
		var result;
		// save/restore controls in case of *wrap
		var saveControlNames = controlNames;
		var controlProxies;

		controlNames = nil;

		prependArgs = prependArgs.asArray;
		ControlMap.isEnabled = false; // don't complicate our life with dynamic loading here
		this.addControlsFromArgsOfFunc(func, rates, prependArgs.size);
		// at this point controlNames is loaded with the ControlNames for the current func
		controlProxies = this.buildControls;
		NamedControl.preload(controlProxies); // controlNames is also used in preload
		// ^^ that now also turns on ControlMap.isEnabled, so Control.names().kr etc.
		// are then dynamically imported from the body of the func when executed below
		result = func.valueArray(prependArgs ++ controlProxies);

		controlNames = saveControlNames;

		^result
	}
}

// this is the hook from where the ControlMap is updated
+ MultiOutUGen {

	initOutputs { arg numChannels, rate;
		if(numChannels.isNil or: { numChannels < 1 }, {
			Error("%: wrong number of channels (%)".format(this, numChannels)).throw
		});
		channels = Array.fill(numChannels, { arg i;
			OutputProxy(rate, this, i);
		});
		// just debug check
		if(currentEnvironment.notNil) {	if(~debugMOUG == 1) {
			"MOUG.iO: ".post; [this.class, this.class.isControlUGen, this.specialIndex].postln;
		}};
		// HOOK call if isControlUGen, otherwise, omg warn msg spam
		if(this.class.isControlUGen) {
			ControlMap.add(this, channels)
		};
		if (numChannels == 1, {
			^channels.at(0)
		});
		^channels
	}
}


+ NamedControl {

	// since this is called every time by *new before it does its real work,
	// we'll just hook dynamic updates in there

	// unchaged from the simpler arg-preload-only version, except for the last two lines
	*preload { arg controlProxies;

		var bsdCNs; // ControlNames for current SynthDef function

		this.initDict; // this also ensures buildSynthDef is valid
		bsdCNs = buildSynthDef.controlNames;

		// if using a mangler, the names are already mangled when preloading
		// because that happens when the ControlNames objects are created
		// in the function frame parsing stage that just happened

		bsdCNs.do { |cno, i|

			// need to convert CN to internal NC format
			var newNC = super.newCopyArgs(
				cno.name, cno.defaultValue.asArray, cno.lag,
				cno.rate, true, controlProxies[i]);
			currentControls.put(cno.name, newNC);

			// just debug code to see if it matched the right OPs to names
			if(currentEnvironment.notNil) {	if(~debugNC == 1) {
				["NaCo preloading:", cno.name,
					controlProxies[i].asArray.collect({ |cop|
						[cop.name, cop.source, cop.outputIndex] }).unbubble].postln  } };
		};

		NaCoExtras.aCNimportedIndex = buildSynthDef.allControlNames.size;

		ControlMap.isEnabled = true;
	}

	*initDict {

		if(UGen.buildSynthDef !== buildSynthDef or: currentControls.isNil) {
			buildSynthDef = UGen.buildSynthDef;
			currentControls = IdentityDictionary.new;
			NaCoExtras.aCNimportedIndex = 0; // ADDED
		};

		// avoid trying to do dynamic updates while in preload mode
		// this will alas be called from the preload method as well
		if(ControlMap.isEnabled) {
			this.catchup
		}
	}

	*catchup {

		// update from externally & dynamically added names, e.g. Control.names(...)
		while { NaCoExtras.aCNimportedIndex < buildSynthDef.allControlNames.size } {

			var cno = buildSynthDef.allControlNames[NaCoExtras.aCNimportedIndex];
			var controlProxies = ControlMap.byIndex[cno.index];

			if(controlProxies.notNil) {
				// a bit tricky to add these since we got called from new*
				// need to do it directly via super like preload does
				var newNC = super.newCopyArgs(
					cno.name, cno.defaultValue.asArray, cno.lag,
					cno.rate, true, controlProxies);
				// ^^ this will convert nil defaultValues to [], which
				// should enable NamedControl.new* to catch them as different
				// from anything valid, really

				currentControls.put(cno.name, newNC);

				if(currentEnvironment.notNil) {	if(~debugNC == 1) {
					post("NaCo importing: " + cno + "\n  >--hooked-to--> ");
					postln(unbubble(controlProxies.asArray.collect({ |cop|
						"% spIndex:% #chans:% offset:% ".format(
							//cop.name not yet set here unfortunately
							cop.source, cop.source.specialIndex,
							cop.source.numChannels, cop.outputIndex)})));
				}}
			} {

				warn("NamedControl could not find Control OutputProxies for index #" ++
					cno.index ++ ".\n  NamedControl does not (yet) support importing "
					"ControlNames with misleading\n  default values, such as those "
					"generated by Control.names(list) with list.size > 1.")
			};
			NaCoExtras.aCNimportedIndex = NaCoExtras.aCNimportedIndex + 1;
		} // end while
	}
}

I’m formally releasing the above under the GPL so it can be integrated in SC if desirable. Don’t ask for me make github pull requests from this though.

In case it’s not obvious: the above only fixes NamedControl to find controls added by args and Control.names, but not the other way around. So the current JITLib code, which unfortunately inserts some Control.names after the user’s function, will still generate duplicate, broken controls. A simple fix for that would be for JITLib to “eat its own dogfood” and use NamedControl now that it works better.

(Before someone wastes their time to correct me on this. I see now that numChannels is always 1 for UGens. That’s counterintuitive, but at least it’s documented in the help. I should have used numOutputs instead. That only affects the debugging messages, so I’m not going to change it now above. Even whoever wrote the initOutputs { arg numChannels ... was probably a bit confused or inconsistent.)

One thing that isn’t clear to me is whether this is primarily for JITLib, or if all SynthDef building would include the new logic.

Direct use of Control.names in JITLib, I’d expect to be nonexistent.

Direct use of Control.names in SynthDefs overall, we have no way to measure. It’s rare but I can’t say that nobody anywhere has any SynthDefs that rely on Control.names with multiple controls. If anyone has, then the new logic would cause (from the user perspective) a regression (“it used to work but now it doesn’t”).

It’s very likely to be better to unify these Control mechanisms, so the present research is valuable and very very impressive (I may not have had the patience! nice stuff here). So I agree with that much of it.

I’m not certain that creating them in multiple, distinct ways and after-the-fact glomming them together is the best way. Perhaps a better design would be to update NamedControl to allow adding multiple controls (because we don’t always want a separate Control object for each arg – IMO this is a weakness of the \name.kr approach and one reason why I don’t use it extensively) and then push all control creation through NamedControl.

This may have the side benefit that refactoring could improve the Control multi-names case, such that a regression could be avoided entirely.

hjh

The are minimal changes to SynthDef (in buildUgenGraph); a bit more info is passed from there to NamedControl to help it populate its own structures.

It’s actually used multiple times in the library, internally. Mostly for NodeProxy roles, but also for the main ProxySynthDef that generates everything NodeProxy.

I agree on that. But short of ditching the current SynthDef class for a complete rewrite, there’s not much way around it. Personally, I would like to see much more encapsulation of the current synthdef parsing state than the current hodgepodge of classvars spread across multiple classes etc.

In the class library, we can control those uses to make sure they don’t do anything illegal (e.g., nothing wrong with Control.names(\out) because it’s only one name). So this is not relevant to the concern I’m trying to raise.

I was referring to user code. We can’t stop someone from writing

Ndef(\x, { var c = Control.names(#[abc, def, ghi]).kr(#[1, 2, 3]); ... });

… and as noted, .add-ing such a SynthDef may not have broken before, but would break if the suggested logic were introduced.

I don’t believe anyone is doing this because JITLib is supposed to be about convenience, and this is not exactly convenient.

As for regular SynthDefs, NamedControl should have superseded pretty much all uses of Control.names but we have no control over legacy code that users might be running. A few years ago, we would have been careless about breaking backward compatibility but in the last few years, we are more cautious about potentially breaking changes. I think this is a good thing – so I’m pointing out that this is, in this version, a breaking change.

Good heavens, no, nothing of that scale should be needed. I see where you’re coming from but I think it can be contained more locally than that.

The SynthDef builder parses function arguments to create a master list of arguments and values, and currently uses Control to expose them as control inputs.

I might be missing something, but:

  1. Add method(s) to NamedControl to handle multiple-input control generation.
  2. Redirect Control.names.kr/ar/ir/tr to use these methods instead.

In 1/, this would be responsible for remembering control names that are already in use, and if a later arg list used a previously defined control, it could substitute the existing OutputProxy.

This would touch a small minority of the SynthDef builder code. If it required any changes to graphdef binary emission, it would be a wrong design.

hjh

Not really; it’s easy to make the code just ignore all errors. If you don’t use any \kr and .name mixtures there’s no difference from before. As I pointed out above, even the mixed code does run, but if you have name conflicts they will have a different resolution than before.

I think it’s probably best you try my code a bit before writing more theoretical critiques.

If you think a bit about it you’ll realize that’s not possible. The semantics are incompatible. Control.names is a write-only interface that “declares” any number of names that may or may not actually receive a control at an arbitrary point in time later.

And if look again at my expose on Control.names, it’s also possible to delete a name by emitting a (seemingly) unrelated one with that interface. These are not exactly sane semantics worth emulating on some other API level.

One thing that could be done to improve compatibility is to fix Control.kr and friends to not issue names that have the wrong slotting of default values in ControlName objects. Meaning like:

ControlName  P 0 xx control
ControlName  P 1 yy control [ 23, 45, 67 ]

Note the complete lack of value to xx. What that actually does via the value hardcoded in the Control itself is to really write 23 to xx and the rest to yy. But the way that fixup is done at SynthDesc and on the Server involves reading the fully serialized graph and get values from the Control objects. If you do that naively during synth generation is becomes an O(n^2) algorithm. Maybe it doesn’t matter because that ‘n’ would only be the size of the controls list.

My primary point is that we should fix the design so that there is a single point where controls are created, stored and later matched. This point may be invoked from multiple places, but the mechanics of creation, storage and mapping should be unified. Otherwise it’s too complicated.

You’re correct that I got the specifics wrong about Control.names, but I don’t believe that this invalidates the broader point.

In very early versions of SC Server, the argument list was the only way to create controls. The API wasn’t expected to be user facing, so it wasn’t designed to be easy to use. Then someone (maybe even James McCartney) figured out how to hack Control.names. As you’ve said, this falls somewhat sort of designing a clean public API. Then someone else said, Control.names is pretty bad, we need something else and then we got NamedControl, but without any attempt to unify.

None of this reflects a deliberate design process. This haphazardness is also reflected in the fact that you found it very difficult to handle all the cases. " … because the NamedControl dynamic updating code doesn’t handle the highly swizzeled controls that buildControls produces" is a symptom of the design problem. We should at least have a code path to un-swizzle them… but really I’m suggesting that we should provide a designed way to create controls that never swizzles them in the first place, and which disallows unreachable controls such as the out that kicked all of this off. (It is always better to create a correct data structure initially, rather than to create a faulty one and fix it later.)

Edit: Also I don’t think you should have been expected to fix all these in a first draft. I’m not saying “look what you didn’t do,” it’s more like, gosh it’s messier under the hood than I thought.

I realize that you might feel I’m criticizing your work, but the large majority of what I’m saying is about some uncoordinated design decisions in the past, which are not your fault. I think we’re working together toward an ideal solution, with bumps in the road.

hjh

I’ve done a bit of a survey of the quarks to see if anyone used Control.names with a list > 1 names because base SC code, including JITLib doesn’t do that.

It turns out there’s at least one such > 1 names use, namely in ‘crucial’ library’s PlayerMixer. So maybe it’s worth writing the “genius bear” version of NamedControl that can even import those, but it will require more memory allocs to map all the “virtual sub-controls” OutputProxies sub-arrays (that can be generated from a given Control and will build.

By the way, generating a graph with 1000 control names (even though this can’t be sent to the server – the max being 255 controls) is already pretty slow, like 1 second on a fairly modern machine! And much slower than generating one with 200 controls. A factor o 5 increase in control names size resulted in a factor of 17 for the compilation time! So there’s probably some quadratic or worse algorithm in the SynthDef compiler already.

n = 200 collect: { |i| "ct"++i }

(bench { (
d = SynthDef(\one, {
	var na = Control.names(n); 
	var ca = Control.kr(1 ! n.size), one = ca.sum;
	Poll.kr(Impulse.kr(0), one, "sum:");
	Out.kr(23, one);
}))})

// time to run: 0.063251100000343 seconds.

bench { d.add }

// -> 0.035941000001912
n = 999 collect: { |i| "ct"++i } // too much for the server

(bench { (
d = SynthDef(\one, {
	var na = Control.names(n); 
	var ca = Control.kr(1 ! n.size), one = ca.sum;
	Poll.kr(Impulse.kr(0), one, "sum:");
	Out.kr(23, one);
})
)})

// -> 1.0726702999964

bench { d.add }
// ERROR: A synthDef cannot have more than 255 control names.

I don’t have much to say about retrofitting Control.names to use the smart or the (yet to be written) genius bear version of NamedControl, othan than the fact it’s probably impossible to do it an maintain 100% of the old semantics of Control.names, even when used alone, meaning not intermixed with NamedControl. I’d be delighted if someone proved me wrong on this. With code.

One possible approach for the latter would be to treat .names.kr as a single call of sorts by:

  • Caching the last .names argument array upon that call.
  • When the next Control is actually emitted it’s no longer an unconditional emit as it currently is, but does lookup using the names cached at the previous bullet.
  • But that also needs to take into account “partial control emits”, so as to move a pointer inside to the next possibly query name.

While thinking about that, there’s this oddball case to consider, which is not based on actual code I’ve seen, but a plausible use of the old interface nonetheless.

(d = SynthDef(\hmmmmmmmm, {
	var wootC;
	Control.names([\xxyy]);
	wootC = [Control.kr([23]), Control.kr([45])];
	Poll.kr(Impulse.kr(0), wootC, "Woot:");
}))

d.allControlNames
// Looks like it's not even an array control!
// -> [ ControlName  P 0 xxyy control 23 ]

// SynthDesc is going to have a different opinion about that one!
d.add.desc
// Controls:
// ControlName  P 0 xxyy control [ 23.0, 45.0 ]
// ControlName  P 1 ? control 45.0

Even more odd, do a type change!

(d = SynthDef(\hmmmmmmmm, {
	var wootC;
	Control.names([\xxyy]);
	wootC = [Control.kr([23]), AudioControl.ar([45])]; // yeah, type change
	Poll.kr(Impulse.kr(0.3), wootC, "Woot:");
}))

d.allControlNames
// -> [ ControlName  P 0 xxyy control 23 ]

d.add.desc
// -> SynthDesc 'hmmmmmmmm' 
// Controls:
// ControlName  P 0 xxyy control [ 23.0, 45.0 ]
// ControlName  P 1 ? audio 45.0

d.dumpUGens
/*
hmmmmmmmm
[ 0_Control, control, nil ]
[ 1_AudioControl, audio, nil ]
[ 2_Impulse, control, [ 0, 0.0 ] ]
[ 3_Poll, control, [ 2_Impulse, 0_Control[0], -1, 5, 87, 111, 111, 116, 58 ] ]
[ 4_Poll, control, [ 2_Impulse, 1_AudioControl[0], -1, 5, 87, 111, 111, 116, 58 ] ]
*/

x = Synth(d.name)


-> Synth('hmmmmmmmm' : 1351)
Woot:: 23
Woot:: 45

x.set(\xxyy, [66, 77]) // works ok

Apparently you can have “mixed control types” under the same (array) control name, server-side, but even SynthDesc doesn’t summarize those properly on the client, type-wise. Basically the ControlName class would have allow for an array of types, not just an array of values, to fully represent what the server can grok. Truth to be told, there’s not much of problem in ControlName even with mixed types, due to duck typing and the fact that it doesn’t have much functionality itself. So its rate can be an array, although much other SC code will probably choke on that. (Spec also works well enough with arrays, although it’s not documented as such, but much stuff that uses it, like NdefGui and what not, doesn’t like arrays in Spec.)

But to put things in perspective: there are a few library call sites to Control.names. It’s much easier to have those call sites changed to use NamedControl and review them for correctness under lookup (instead of write-only) behavior while at it. If you, James, think it’s worthwhile to do it the other way around, i.e. write a genius version of Control.names.kr that does guess the right lookups even though the library code might not expect those semantics at a given call site, go for it and enjoy the glory.

For example, PlayerMixer just assembles its own SynthDef without any user-provided function being mixed into the def.

	asSynthDef {
		^SynthDef(this.defName, { arg out=0;
			var inputBuses;
			if(players.size > 1, {
				inputBuses = Control.names(Array.fill(players.size, { arg i; "in"++i.asString}))
							.ir(Array.fill(players.size, 0))
							.collect({ arg in; In.ar(in, this.numChannels) });
				Out.ar(out,
					Mix.new(
						inputBuses
					)
				)
			}, {
				Out.ar(out,
					In.ar( Control.names(["in0"]).ir([0]), this.numChannels)
				)
			})
		});
	}

So it doesn’t matter that it produces a Control setup that my only moderately smart NamedControl version wouldn’t grok because there’s no way to call NamedControl from within that SynthDef context that PlayerMixer creates just for itself. I’m leaving aside here the case where you’d abuse players, which is a writeable field in that class, to pass in in something that emits NamedControls when players.size is called.

You can fantasize about producing the perfect multi-interface mega API that somehow pretends it’s all things to all people with 100% backwards compatibility and no “user” (meaning mostly library) code changes, or just use something that

  • it’s written (in posts #1-#2 above),
  • works for its intended purpose,
  • improves on the current situation a fair bit, and
  • requires changing a few lines of library code, fewer that you might expect, actually, as the example immediately above shows.

Yes, crucial lib also has an InstrSynthDef that should be changed to use NamedControl so that it can see potentially user-accessed controls in that context instead of overwriting them with non-working ones; recall: non-working because the server uses the first matching name. Same situation as JITLib (ProxySynthDef) in that latter case.

I remarked once before that conversations with you seem to get intense, with no apparent benefit to the intensity. For example, this – “You can fantasize about producing the perfect multi-interface mega API that somehow pretends it’s all things to all people…” – is condescending. I’m perhaps the last person who should be raising that, given some of my history, but in this case, I think I’ve tried my best to raise concerns in a cooperative way. If you feel I’ve failed to be cooperative, I’m willing to listen to reasonable criticism.

If one benchmark for accepting a solution is minimal impact on the library – in fact, I did propose an even less intrusive way of connecting a control’s name to its OutputProxy channel. My thinking was that there is already an object that holds all of the information about control names and mappings – the SynthDef! So it should be possible to look up controls in the SynthDef, without changing any of the control-generating logic – use what is already there.

IIRC Julian then said something about importing all of this information into NamedControl – however, this strikes me as an offhanded suggestion (when brainstorming, it’s completely fair to propose alternatives) but it wasn’t an object-design conversation that ended in consensus.

We actually have a request-for-consideration process for exactly this kind of situation – there are multiple ways to solve the problem, and we want to discuss and reach consensus before committing code. We started this process because there’s a history in SC of committing changes too quickly, without considering future consequences. As a result of this process, and more careful code review, SC’s stability has improved considerably in the last few years. So I’m not quite eager to short-circuit these processes.

hjh