Ndef as a fx chain: i'm stuck

While writing \mixN and \filterN that use the low-level mangler, I realized that unlike for SynthDef.wrap for which the lower-layer (meaning ControlName and NamedControl) mangler method is the only robust option, just for NodeProxy roles, it’s actually possible to apply the “clean” method I described in my ~makeSDclones earlier with some minor modifications.

This idea works for NodeProxy roles because every \filter, \mix etc. is compiled to a separate ProxySynthDef object, which is actually returned by buildForProxy to AbstractPlayControl.buildMethods where we have easy access to this
ProxySynthDef object after it has been built, but before it is passed back to the NodeProxy. So we can merrily rename its controls in makeSDclones-style before we pass it back.

The advantage of this technique is that just like ~makeSDclones, it requires zero new class extensions!

Here’s how to do it for \mix first, which is the simpler code to understand. I’m going to call this role \mixR for renaming after the ProxySynthDef is built.

(AbstractPlayControl.buildMethods.put(\mixR,
	#{ | func, proxy, channelOffset = 0, index |
		var mixi = "mix" ++ (index ? 0);
		var psd = { // we save this ProxySythnDef an will post-process it!
			var ctl = Control.names([mixi]).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 );
		// we still define this style of control mangler/rename func,
		// but will call it in a different spot than in \mixN
		var postmangler = { arg name;
			name = name.asString;
			if(["out", "i_out", "gate", "fadeTime", mixi].indexOfEqual(name).isNil) {
				name = name.asString ++ index;
				//("renamed::" + name).postln;
			} {
				//("skipped::" + name).postln;
			};
			name.asSymbol
		};
		psd.allControlNames.do { arg cno; cno.name = postmangler.(cno.name) };
		//psd.allControlNames.do(_.postln);
		psd
}));

Test:

Ndef.clear
n = Ndef(\testMixR, { arg amp = 0.2, freq = 222; amp * SinOsc.ar(freq) })

n[3] = \mixR -> { \amp.kr(0.2) * SinOsc.ar(\freq.kr(777)) }
n[4] = \mixR -> { arg amp = 0.2, freq = 444; amp * SinOsc.ar(freq) }

n.controlNames do: _.postln;
n.edit

Instead of writing something like that by hand 2 more times for \filter and \fitlerIn, since AbstractPlayControl.buildMethods gives us direct access to the whole table, we can just
create new functions en-masse by composing the old ones with a “postmangler” for
control names.

(~massPostmaglerInstaller = { arg newRolesSuffix = "M"; // so \mixM etc.
	var targetRoles = #[\mix, \filter, \filterIn];
	var defaultSkipNames = #["out", "i_out", "gate", "fadeTime"];
	var specificSkipNames = (mix: ["mix"], filter: ["wet"], filterIn: ["wet"]);
	var postmangler = { arg name, index, role;
		var skipNames = defaultSkipNames ++ (specificSkipNames[role] +++ index);
		name = name.asString;
		//skipNames.postln;
		if(skipNames.indexOfEqual(name).isNil) {
			name = name.asString ++ index;
			("Renamed::" + name).postln;
		} {
			("Skipped::" + name).postln;
		};
		name.asSymbol
	};
	var wrapperGen = { arg roleName, roleBuildFunc;  // curried targets
		{ arg func, proxy, channelOffset = 0, index;
			var psd = roleBuildFunc.value(func, proxy, channelOffset, index);
			psd.allControlNames.do { arg cno;
				cno.name = postmangler.value(cno.name, index, roleName) };
			psd.allControlNames.do(_.postln);
			psd
		}
	};
	targetRoles.collect { arg roleName;
		var origBuildFunc = AbstractPlayControl.buildMethods[roleName];
		var newBuildFunc = wrapperGen.value(roleName, origBuildFunc);
		var newRoleName = (roleName.asString ++ newRolesSuffix).asSymbol;
		AbstractPlayControl.buildMethods.put(newRoleName, newBuildFunc);
		(newRoleName.asString + "installed").postln;
		[newRoleName, newBuildFunc]  // ret val somewhat irrelevant
	}
})

A modest test of this thingy:

~massPostmaglerInstaller.()
Ndef.clear
n = Ndef(\testM, { arg amp = 0.2, freq = 222; amp * SinOsc.ar(freq) })
n[1] = \mixM -> { \amp.kr(0.2) * SinOsc.ar(\freq.kr(777)) }
n[2] = \mixM -> { arg amp, freq = 444; amp * SinOsc.ar(freq) }
n[11] = \filterM -> { arg in; in * SinOsc.kr(\freq.kr(2)) }
n[12] = \filterInM -> { arg in, freq = 9; in * SinOsc.kr(freq)}
n.edit

As closing thoughts; maybe some kind of index math wouldn’t hurt e.g. above
the three base frequencies and two AM frequencies could get their own counters.
This would need a different approach than just numbering by the Ndef slots,
e.g. a global Bag lookup.

Also I think that with Ndef roles you can only get the effects in linear order, so (sig1 * aM1) + (sig2 * aM2) is not really expressible as a single Ndef filter chain, I think, unless you mess with
multiple channels to trick it somehow with a final down mix and pan.