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 arg
s 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.)