Ndef as a fx chain: i'm stuck

Sure, that was my first idea, but it’s a pretty leaky abstraction because all your (filter) synths now need to internally know to auto-number their controls. Ideally you want this magic to happen externally of your synth function code and as much as possible transparently, meaning without changing the (filter) synth code much. Just writing ~foo.kr instead of \foo.kr in the synth to access the controls is less leaky in my view. The example I came up with also needing .asSymbol was probably not the best with respect to that least-code-change aspect. I had forgotten about the Symbol vs. String issue, to be honest.

This won’t be enough if your filters e.g. use LocalIn and LocalOut, which mine often do, because you can only have one of those local things per synth so combining such filters with SynthDef.wrap will probably not work even if you manage to get past the control renaming issues. (Faust’s approach to a more purely functional composition abstraction certainly helps work around such issues.)

Generally speaking, it looks like this approach of internally combining previously encapsulated user stuff into bigger SynthDefs isn’t favored much in SC. The simpler philosophy being that you just instantiate them as separate synths and route them as needed on the server.

If you don’t need such internal combining though, it turns out it’s possible to a have simpler step that does a sort of linker-like renaming just on the SynthDef, i.e. not recompiling (==rebuilding the ugen graph) function at all. This isn’t advertised in the help, but it does work properly, if you’re careful to deepCopy the SynthDef before making changes.

// for Ndefs, you may or may not want different \gate in each...
(~makeSDclones = { arg sdt, num, skipCtrlNames = #[\out];
	num.collect { arg i;
		var sdc = sdt.deepCopy;
		sdc.name = sdt.name.asString ++ i; // no eff without asString!
		sdc.allControlNames.do { arg cno;
			if(not(skipCtrlNames.includes(cno.name))) {
				cno.name = (cno.name.asString ++ i);
			} {
				// ("Skipped renaming" + cno.name + "in" + sdc).postln
			}
		};
		sdc // return whole sd clone to be collect-ed
	}
})

// Some tests just generating the clones, nothing sent to server (yet)

d = SynthDef(\alone, { arg out = 0, freq = 111; Out.ar(out, SinOsc.ar(freq)) } )
z = ~makeSDclones.(d, 3)
z.do { arg i; i.post; i.allControlNames.postln }

would post something like

SynthDef:alone0[ ControlName  P 0 out control 0, ControlName  P 1 freq0 control 111 ]
SynthDef:alone1[ ControlName  P 0 out control 0, ControlName  P 1 freq1 control 111 ]
SynthDef:alone2[ ControlName  P 0 out control 0, ControlName  P 1 freq2 control 111 ]

As .collect is “polymorphic” you can pass different things there for renaming e.g. arrays of specific numbers or strings:

~makeSDclones.(d, (1,3..9)) // just odd numbers
~makeSDclones.(d, ["Left", "Right"]) // or some strings etc.

Finally, some actual testing with a NodeProxy sources array.

z.do(_.add) // actually send defs to server

n = NodeProxy(s, \audio, 2)
n[0] = \alone0
n[1] = \alone1
n[2] = \alone2

n.edit // should see 3 controls

NodeProxy’s constructor is smart enough to interpret arrays of symbols as SynthDef names, so there you can just write e.g.

n = NodeProxy(s, \audio, 2, [\alone1, \alone2])

Beware however that something like that might hang the sever with Ndef’s constructor though.


Although a clean solution, this alas still won’t work Ndef-wise with \filter roles, or any roles for that matter (\mix etc.). Contrast

n = NodeProxy(s, \audio, 2)
n[0] = { arg fre1 = 777; 0.5 * SinOsc.ar(fre1) !2}
n[1] = \filter -> { arg in, fre2 = 333; 0.5 * SinOsc.ar(fre2) !2} 
//  fre2 exposed at top level func, so...
n.edit // gui "sees" fre2 and makes slider for it

with

n = NodeProxy(s, \audio, 2)
n[0] = { arg fre1 = 777; 0.5 * SinOsc.ar(fre1) !2}
m = NodeProxy(s, \audio, 2, { arg in, fre2 = 333; 0.5 * SinOsc.ar(fre2) !2})
n[1] = \filter -> { arg in; m.ar(2) } 
n.edit // doesn't see fre2 anymore

n.set(\fre2, 999) // doesn't do a thing either

The issue here is how JITlib treats filter “sub-nodes” in such cases. It won’t import their
own controls if there’s some indirection. Unlike the SynthDef graph builder, NodeProxy (built-in) roles only examine functions. I’m pondering whether a custom role could do better.

If you look at the \filter role implementation in wrapForNodeProxy.sc you can see the problem.
The function on the right side of the Association is SynthDef.wrap-ed. And that only works for
functions and nothing else. There’s alas not a way to SynthDef.wrap another SynthDef, but only
a bare function!