I have a partial, i.e. “manual” fix for the issue of |... args|
wrapped functions like inEnvir
genates:
// altDefFunc allows the frame of another function to be used to auto-gen contols
// useful when the original function f is wrapped in a { arg ...args; /*stuff*/ f.valueArray(args) }
// or similar which makes the original f def (actual arg names) invisible to the SynthDef graph builder
+ SynthDef {
*new { arg name, ugenGraphFunc, rates, prependArgs, variants, metadata, altDefFunc;
^super.newCopyArgs(name.asSymbol).variants_(variants).metadata_(metadata ?? {()}).children_(Array.new(64))
.build(ugenGraphFunc, rates, prependArgs, altDefFunc)
}
build { arg ugenGraphFunc, rates, prependArgs, altDefFunc;
protect {
this.initBuild;
this.buildUgenGraph(ugenGraphFunc, rates, prependArgs, altDefFunc);
this.finishBuild;
func = ugenGraphFunc;
} {
UGen.buildSynthDef = nil;
}
}
*wrap { arg func, rates, prependArgs, altDefFunc;
if (UGen.buildSynthDef.isNil) {
"SynthDef.wrap should be called inside a SynthDef ugenGraphFunc.\n".error;
^0
};
^UGen.buildSynthDef.buildUgenGraph(func, rates, prependArgs, altDefFunc);
}
buildUgenGraph { arg func, rates, prependArgs, altDefFunc;
var result;
// save/restore controls in case of *wrap
var saveControlNames = controlNames;
controlNames = nil;
prependArgs = prependArgs.asArray;
this.addControlsFromArgsOfFunc(altDefFunc ? func, rates, prependArgs.size); // subst!!
result = func.valueArray(prependArgs ++ this.buildControls);
controlNames = saveControlNames
^result
}
}
Some basic tests:
SynthDef(\err, { arg ...args; Out.ar(0, 0) })
SynthDef(\errNoMore, { arg ...args; Out.ar(0, 0) }, altDefFunc: {})
// ok now!
// A more substantive test; real args
(z = SynthDef(\errNoMore,
{ arg ...args; Out.ar(0, args[0] * SinOsc.ar(args[1])) },
altDefFunc: { arg amp = 0.2, freq = 222; }))
z.allControlNames
// -> [ ControlName P 0 amp control 0.2, ControlName P 1 freq control 222 ]
z.add
Ndef(\zooo, \errNoMore).edit
//// now we can call inEnvir freely as long we can provide the original func as "alt"
f = { ~ctrl.ar(1); Silent.ar };
SynthDef(\hmmm, f.inEnvir( (ctrl:\ah) )) // err of course
d = SynthDef(\hoho, f.inEnvir( (ctrl:\ah) ), altDefFunc: f)
d.allControlNames // -> [ ControlName P 0 ah audio 1 ]
I strongly suspect one can use the altDefFunc
bit to more directly implement arg name changes too without needing a function recompile. I’ll post about that later. (Basically it allows one change the names of the FunctionDef
args for the purpose of SynthDef, whereas the FunctionDef args are alas immutable in sclang.)
I’ve made a bit more progress towards a more useable version of Ndef roles that auto-number their sub-gen params. I’m starting with \mix
rather than \filter
because it has simple code, so it’s easier to understand the changes. The original looks like this:
mix: #{ | func, proxy, channelOffset = 0, index |
{
var ctl = Control.names(["mix" ++ (index ? 0)]).kr(1.0);
var sig = SynthDef.wrap(func);
var curve = if(sig.rate === \audio) { \sin } { \lin };
var env = EnvGate(i_level: 0, doneAction:2, curve:curve);
ctl * sig * env
}.buildForProxy( proxy, channelOffset, index )
};
For now I’m calling mine \mixN
, where is N stands for numbering (I’m open to better name suggestions, as long as they aren’t too long). What I have so far uses all the classs extensions from above, but alas only really works for NamedControls and ContrlName explicit uses, but not for arg
controls, for a somewhat subtle reason that I hope to remedy later. I’ll explain the issue with arg
controls below the code. I left the postln
debugging code in too.
(AbstractPlayControl.buildMethods.put(\mixN,
#{ | func, proxy, channelOffset = 0, index |
{
var ctl = Control.names(["mix" ++ (index ? 0)]).kr(1.0);
var imangler = { arg name; // can't be closed func due to index access
("In imangler for" + name).postln;
// could get these as proxy.internalKeys but that's not entirely correct
// as these need to determined based on the "proxy" that's on right-hand
// of the association, but that hasn't been built yet. Will add a version
// later that supports a ready-made NodeProxy instead of bare function func.
if(not(#[\out, \i_out, \gate, \fadeTime].includes(name.asSymbol))) {
name = name.asString ++ index;
("iMangled::" + name).postln;
} {
("Skipped::" + name).postln;
};
name
};
var menvir = (controlNameMangler: imangler).parent_(currentEnvironment);
var sig = SynthDef.wrap(func.inEnvir(menvir), altDefFunc: func);
var curve = if(sig.rate === \audio) { \sin } { \lin };
var env = EnvGate(i_level: 0, doneAction:2, curve:curve);
ctl * sig * env
}.buildForProxy( proxy, channelOffset, index )
}));
So, to test this beast:
Ndef.clear
n = Ndef(\testMixN, { arg amp = 0.2, freq = 222; amp * SinOsc.ar(freq) })
n[3] = \mixN -> { \amp.kr(0.2) * SinOsc.ar(\freq.kr(777)) }
n.edit // ok; slot 3 param names all get '3' suffix like 'amp3' etc.
n[4] = \mixN -> { arg amp = 0.2, freq = 444; amp * SinOsc.ar(freq) }
// doesn't work; no callbacks to our mangler at all!
The reason why that last line doesn’t work is that in
SynthDef.wrap(func.inEnvir(menvir), altDefFunc: func);
the way wrap
(still) works is that it looks at altDefFunc: func
and emits all its ControlNames immediately before evaluating func.inEnvir(menvir)
. So the mangler is not yet set up at that point. I’m pondering what’s the least intrusive way to fix this issue.
Ok, I have a properly working version of \mixN
now, meaning it works for both arg
and NamedControl. There were two bugs above, actually. The surprisingly easy fix for the arg
issuse was to do just
var sig = menvir.use { SynthDef.wrap(func) }; // fix for arg
So the altDef
business was actually not that useful or needed here. The second issue was that just with that NdefGui was exploding on the arg
version complaining about missing specs. It turns out that I had forgotten a conversion to Symbol
on the last line of mangler, which interestingly was only needed for the latter use case. Any how, here’s the fully working version for \mixN
; debug postln
's commented out.
(AbstractPlayControl.buildMethods.put(\mixN,
#{ | func, proxy, channelOffset = 0, index |
{
var ctl = Control.names(["mix" ++ (index ? 0)]).kr(1.0);
var imangler = { arg name; // can't be closed func due to index access
if(not(#[\out, \i_out, \gate, \fadeTime].includes(name.asSymbol))) {
name = name.asString ++ index;
// ("iMangled::" + name).postln;
} {
// ("Skipped::" + name).postln;
};
name.asSymbol // does .asSymbol fix Spec issue in gui? YESSSSS.
};
var menvir = (controlNameMangler: imangler).parent_(currentEnvironment);
var sig = menvir.use { SynthDef.wrap(func) }; // fix for arg
var curve = if(sig.rate === \audio) { \sin } { \lin };
var env = EnvGate(i_level: 0, doneAction:2, curve:curve);
ctl * sig * env
}.buildForProxy( proxy, channelOffset, index )
}));
So these use cases now both work:
Ndef.clear
n = Ndef(\testMixN, { arg amp = 0.2, freq = 222; amp * SinOsc.ar(freq) })
n[3] = \mixN -> { \amp.kr(0.2) * SinOsc.ar(\freq.kr(777)) }
n[4] = \mixN -> { arg amp = 0.2, freq = 444; amp * SinOsc.ar(freq) }
n.controlNames do: _.postln
n.edit // works now for both 3 & 4
Here’s the \filter
equivalent of that, meaning that does the same index-based auto-renaming:
(AbstractPlayControl.buildMethods.put(\filterN,
#{ | func, proxy, channelOffset = 0, index |
var imangler = { arg name;
if(not(#[\in, \out, \i_out, \gate, \fadeTime].includes(name.asSymbol))) {
name = name.asString ++ index;
("F mangled::" + name).postln;
} {
("F skipped::" + name).postln;
};
name.asSymbol
};
var menvir = (controlNameMangler: imangler).parent_(currentEnvironment);
var ok, ugen;
if(proxy.isNeutral) {
ugen = menvir.use { func.value(Silent.ar) }; // prolly doesn't matter
ok = proxy.initBus(ugen.rate, ugen.numChannels + channelOffset);
if(ok.not) { Error("NodeProxy input: wrong rate/numChannels").throw }
};
{ | out |
var env, ctl = Control.names(["wet"++(index ? 0)]).kr(1.0);
menvir.use { // but this does matter
if(proxy.rate === 'audio') {
env = ctl * EnvGate(i_level: 0, doneAction:2, curve:\sin);
XOut.ar(out, env, SynthDef.wrap(func, nil, [In.ar(out, proxy.numChannels)]))
} {
env = ctl * EnvGate(i_level: 0, doneAction:2, curve:\lin);
XOut.kr(out, env, SynthDef.wrap(func, nil, [In.kr(out, proxy.numChannels)]))
}};
}.buildForProxy( proxy, channelOffset, index )
}));
Test with something like
Ndef.clear
n = Ndef(\testFiltN, { arg amp = 0.3, freq = 888; amp * SinOsc.ar(freq) })
n[3] = \filterN -> { arg in; in * SinOsc.kr(\freqM.kr(2)) }
n[4] = \filterN -> { arg in, freqM = 9; in * SinOsc.kr(freqM)}
n.controlNames do: _.postln
n.edit
Seems ok.
I’ve also noticed a small bug in \filter
that affects uninitialized proxies. I haven’t fixed that above.