Ndef as a fx chain: i'm stuck

Hello

I have a collection of SynthDef for various effects like reverb and phaser

My idea is to use constructions like Ndef(\bla)[10] = \filter -> { arg in; ... };

Since my fx are defined as SynthDef, i thought I could use something similar to Ndef(\bla)[10] = \synthdef_name and Ndef will load the synthdef for me.

So I tried Ndef(\bla)[10] = \filter -> \fx_synthdef_name but this throw an error, I guess this is not an available feature.

No problem, I will use Ndef(\bla)[10] = \filter -> SynthDescLib.global.at(\fx_synthdef_name).def.func

This works, but if I put several times the same SynthDef (put \fx_synthdef_name in index 10 and 11 for example), the argument names conflicts, I can’t control them separately. The wet argument automatically added by Ndef is suffixed by the index (\wet10 and \wet11) so I thought I would do the same.

So I suffix the argument names with the index : SynthDescLib.global.at(\fx_synthdef_name).controlNames.collect({ arg x; x++10 })
and build a function compile string :
"{ arg %; SynthDescLib.global.at(\fx_synthdef_name).def.func.value(%); }".format(argNames, argNames).interpret

this works fine, until I realize that a SynthDef that use \argumentName.kr for declaring SynthDef parameters breaks my sweet solution.

I find no way to rename the ControlNames of a SynthDef :frowning:

1 Like

What about this ?

SynthDef(\fx_synthdef_name, {
	|asarg=220| Out.ar(0,Pan2.ar(SinOsc.ar(asarg,0,\asctrl.kr(0.2))));
}).add;


(
~transform=
{
|synth,pos|
var code=SynthDescLib.global.at(synth).def.func.asCompileString;
var argNames=SynthDescLib.global.at(synth).controlNames;
argNames.do({|p| 
	var s=p.asString.trim;
	format("--%-- (%)",s,s.class).postln;
	code=code.replace(s,s++pos);
});
code.interpret;
}
)


Ndef(\a,~transform.(\fx_synthdef_name,10));
Ndef(\a)[1]=~transform.(\fx_synthdef_name,11);
Ndef(\a).set(\asarg10,220);
Ndef(\a).set(\asctrl10,0.2);
Ndef(\a).set(\asarg11,330);
Ndef(\a).set(\asctrl11,0.05);

Ndef(\a).gui
2 Likes

To use an existing SynthDef as a filter for an Ndef you would omit the \filter role and just specify your SynthDef as a source. I believe the convention is that you need a NamedControl named \out which will get the private bus that the Ndef will create.

e.g.

SynthDef(\fx1, {
    
    var out = \out.kr(0);
    var dry = In.ar(out, 2);
    var wet = ...
    XOut.ar(out, \mix.kr(0), [dry, wet]);
    
}).add;

Then you would add to your fx chain like this

Ndef(\myndef)[100] = \fx1;

But this won’t help with the name collisions…

2 Likes

This is a general problem with SynthDef.wrap, which is merely reflected in your use case. If you wrap the same function several times in the same synthdef you are responsible for renaming its arguments on every instancce, sadly. Somewhat more practical is to use NamedControls instead of arg, because the former are strings, so you can manipulate them more easily.

f = { arg ctrl; Poll.kr(Impulse.kr(1), ctrl) }

SynthDef("bad_idea", { SynthDef.wrap(f); SynthDef.wrap(f) }).add

// WARNING: Could not build msgFunc for this SynthDesc: duplicate control name ctrl

Unfortunately as discussed here with respect to out, NodeProxy doesn’t print that kind of warning about duplicate control names in its custom graph builder.

Semi-proper solution #1, assuming you want it to be the same control shared by both functions (in your case don’t but we’ll get to that later)

f = { Poll.kr(Impulse.kr(1), \ctrl.kr) }

SynthDef("one_ctrl", { SynthDef.wrap(f); SynthDef.wrap(f) }).add

In your case you need a function generator that renames the controls. Alas I don’t know how to offer a super-general version of this that inspects the function and does some magic, but as a leaky abstraction you can have

g = { arg i; { Poll.kr(Impulse.kr(1), ("ctrl" ++ i).asSymbol.kr) } }

SynthDef("two_ctrls", { SynthDef.wrap(g.(1)); SynthDef.wrap(g.(2)) }).add

x = Synth("two_ctrls")

s.queryAllNodes(true)

would show something like

     1102 two_ctrls
        ctrl1: 0 ctrl2: 0

lgvr123’s solution above is more transparent than this, but it may run in trouble because it’s just doing string replacements on the function source code. You might accidentally replace something other than an argument name that way, but I suppose it’s useable if you’re careful on that angle, i.e. give your args names that won’t clash e.g. with some methods names and what not.

This discussion reminds of defmacro in Lisp and its use in Nyquist to do score generation (score-gen).

It wold be awesome to be able to compose graph def functions without worrying about name collision.

a wrap function that let you specify a key to prepend to the wrapped function’s controls and falls back to appending a number would be good maybe

but maybe there’s a more general solution even

Disregard my rather failed attempts below the line, but which I’m leaving as an interesting exercise for why the solution looks like this:

(
f = { Poll.kr(Impulse.kr(1), ~ctrl.asSymbol.kr, ~ctrl) };

SynthDef("funny_ctrls", {
	SynthDef.wrap({ (ctrl: "boo").use { f.value } });
	SynthDef.wrap({ (ctrl: "yoo").use { f.value } })
}).add;

x = Synth("funny_ctrls");

s.queryAllNodes(true)
)

This will create two controls of different names, as expected. So you just have to use envir ~vars for the control names (in NamedControl style) and no args.

The trick was to mostly emulate what inEnvir does but return a function that takes no args otherwise the graph builder chokes up.

You can even combine this approach with lgvr123’s idea above, but instead of generating the final synth directly by string substitution every time, you do that only once to generate “templates” like f above, which you can manually check for correctness, but you only need to do this once per template. After that, with the envir ~vars approach, they work as proper macro-like placeholder substitutions, i.e. you don’t risk replacing the wrong sub-string as the placeholders to be replaced are well-defined in the f “template” above when .used.

By the way, .asCompileString doesn’t see too deeply if your synth function calls others or uses variables. Never mind if the final control names are computed in some more intricate way. E.g.

(
c = ["boo", "yoo", "hoo"];

SynthDef("funny_ctrls", {
	c.do { |cn|	SynthDef.wrap { (ctrl: cn).use(f) } };
}).add
)

SynthDescLib.global.at(\funny_ctrls).def.func.asCompileString

returns

-> {
	c.do { |cn|	SynthDef.wrap { (ctrl: cn).use(f) } };
}

You may know the real control names from

SynthDescLib.global.at(\funny_ctrls).controlNames
// ->  -> [ boo, yoo, hoo ]

but it doesn’t always help find where to substitute. “Decompiling” the functions just with asCompileString to find the places to replace isn’t just risking “false positives”, i.e. replacing the wrong stuff, but also “false negatives”, i.e. not finding the actual places where the controls get created, but I do concede that lgvr123’s solution will probably work well in a lot of real-world cases.


Part kept for “historical purposes”, LOL:

I’m trying to fake defmacro via environment vars in SC. Would still be non quite transparent, but less horrible perhaps than my previous attempt. Alas the graph builder doesn’t like .inEnvir functions, even completely empty ones. Not sure why.

The basic idea would be this:

// simple illustration of the replacement inEnvir trick

f = { (~ctrl + "says hi.").postln }
f.() // tries to call with ~ctrl nil

g = { arg e; e.use { f } } // nope this won't work
g.((ctrl: "boo")).()

g = { arg e; f.inEnvir(e) } // this works
g.((ctrl: "boo")).() // boo says hi.

g = { arg e, f; f.inEnvir(e) } // f as arg too
g.((ctrl: "boo"), f).() // ok!!

Alas that’s as far as it goes. Even completely empty functions called .inEnvir upset the synth graph builder.

SynthDef("funny_ctrls", { SynthDef.wrap({}) }).add // ok meh

SynthDef("funny_ctrls", { SynthDef.wrap({}.inEnvir)}).add 
// The preceding error dump is for ERROR: a Control: wrong number of channels (0)

I don’t know how to fix or work around that, right now. I suppose it happens because the function (wrapper) returned by inEnvir has a variable number of args and no named ones

// attach the function to a specific environment
inEnvir { |envir|
	envir ?? { envir = currentEnvironment };
	^{ |... args| envir.use({ this.valueArray(args) }) }
}

So I guess the synthdef graph builder has no idea what to do with that.

SynthDef("funny_ctrls", { SynthDef.wrap({ |... args| })}).add 
// ^^ The preceding error dump is for ERROR: a Control: wrong number of channels (0)
1 Like

Actually this issue is rather unrelated to the discussion about multiple wrap/inclusions in the same synth.

The \filter association there expects a function on the right hand. Even another Ndef won’t work directly, but has to be written as e.g…\filter -> { Ndef(\fx).ar(2) }. But in your case, you can create an anonymous NodeProxy there to satisfy those requirements, even if the syntax looks a bit ugly.

a = Ndef(\ah, { 0.5 * SinOsc.ar(777) !2 }).play;
SynthDef(\bhh, { Out.ar(0, 0.5 * SinOsc.ar(333) !2) }).add;
a[1] = \filter -> { NodeProxy.audio(s, 2).source_(\bhh).ar(2) }
a[1] = \filter -> { NodeProxy(s, \audio, 2, \bhh).ar(2) } // same

The last line does the same as the previous one, but the arguments of the default new constructor are bit harder to remember. :confused:

You could use UniqueID in the control’s name to avoid collisions.

f = { 
    var prefix = UniqueID.next;
    var ctrl = "myctrl_%".format(prefix).asSymbol.kr(1);
    Poll.kr(Impulse.kr(1), ctrl) 
};

SynthDef("bad_idea", { 
    SynthDef.wrap(f); 
    SynthDef.wrap(f) }
).add

x = Synth("bad_idea")

Resulting in these nodes

s.queryAllNodes(true)
290729 bad_idea
        myctrl_1077: 1 myctrl_1078: 1

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!

for that I think you could use the inEnvir technique without having to create a new role

Ndef(\test).play

~f = { ~ctrl.ar(1); Silent.ar  };

Ndef(\test).put(0, \filter -> ~f.inEnvir( (ctrl:\a)  ))
Ndef(\test).put(1, \filter -> ~f.inEnvir( (ctrl:\b)  )) 

Ndef(\test).controlNames

It’s funny that the SynthDef ugen builder is less tolerant than the proxy one

SynthDef(\hmm, ~f.inEnvir( (ctrl: \ah) ))

bombs out with an error as I noted a couple of posts above, at the end of that post.


Something that works every time: add a global name mangler to ControlName:

+ ControlName {
	//var <>name, <>index, <>rate, <>defaultValue, <>argNum, <>lag;

	*new { arg name, index, rate, defaultValue, argNum, lag;
		var mangledName; 
		// Warning: this constructor gets called a helluva lot during sclang startup!
		// If you add any debugging postln here, prepare for massive dumps...
		// During sclang startup, the currentEnvironment is nil sometimes, so we have to check that!
		if(currentEnvironment.isNil) { mangledName = name } {
			if(~controlNameMangler.isNil) { mangledName = name } {
				mangledName = ~controlNameMangler.value(name)
			}
		}
		^super.newCopyArgs(mangledName.asSymbol, index, rate, defaultValue, argNum, lag ? 0.0)
	}
}

Tests ok for me with

SynthDef(\alone, { arg out, freq; Out.ar(out, SinOsc.ar(freq)) } )
.allControlNames // no change of course

~mynme = (controlNameMangler: { arg name; name ++ "Woot" })

z = ~mynme.use { SynthDef(\alone, { arg out, freq; Out.ar(out, SinOsc.ar(freq)) } ) }
z.allControlNames // these are suffixed

Temporary global side effects are not much of an issue since sclang is only cooperatively multithreaded. This feature is (ab)used a lot by the SynthDef graph builder, which e.g. has a global classvar buildSynthDef in UGen for the synthdef currently being compiled. That’s how UGen methods knows where to add nodes to.

You may want a smarter name-mangling function that doesn’t suffix \out etc. as I noted (with example code) in a previous post.

Here’s an attempt to use to make the above more useable for SynthDef.wrapping the same function multiple times in the same SynthDef. It turns out this isn’t as easy/useable as I thought, mainly because NamedControl’s auto-deduplication is fighting us here at cross-purposes.

First define a convenience wrapper:

(~swrap = { arg func, suffix;
	var envir  = (controlNameMangler: { arg name; name ++ suffix.postln });
	SynthDef.wrap { envir.use { func.value } }; // This eats args!
})

Then try using it:

~f1 = { arg freq = 333; SinOsc.ar(freq) } // won't work because ~swrap is eating args
d = SynthDef(\LRtest, { Out.ar(0, [~swrap.(~f1, "Left"), ~swrap.(~f1, "Right")]) })
d.allControlNames // nil! (~f1 args were eaten by ~swrap)

~f2 = { SinOsc.ar(\freq.ar(333)) } // won't work because it deduplicates unsuffixed
d = SynthDef(\LRtest, { Out.ar(0, [~swrap.(~f2, "Left"), ~swrap.(~f2, "Right")]) })
d.allControlNames // well, only got Left... because deduplication in NamedControl

~f3 = { SinOsc.ar(Control.names(\freq).kr(333)) } // finally, ok
d = SynthDef(\LRtest, { Out.ar(0, [~swrap.(~f3, "Left"), ~swrap.(~f3, "Right")]) })
d.allControlNames // two controls, finally

d.add;
Ndef(\tLR, \LRtest).edit // ok, 2 ctrls

To make this more useable in the standard idioms, we’d need to hack NamedControl to understand ~controlNameMangler.

1 Like

The combination of inEnvir and a slight modification to the symbol extension for named controls solves this quite nicely for my purposes since I really only use functions and Ndef. The key thing was learning about inEnvir. Thanks.

+ Symbol {

    kr { | val, lag, fixedLag = false, spec |
        var name = "%%".format(this, ~num ?? {""});
		^NamedControl.kr(name, val, lag, fixedLag, spec)
	}

    ar { | val, lag, spec |
        var name = "%%".format(this, ~num ?? {""});
		^NamedControl.ar(name, val, lag, spec)
	}
...
}
1 Like

Note: earlier version was exhibiting a bug with NdefMixer; likely it would have happened with explicit use of ProxySpace as well, although I’ve stayed away from using that one explicitly myself insofar. I’ve added a check for that now in the first method below (Symbol.mangle).

I’ve got the NamedControl pre-mangling duplication check solved, i.e. now it’s a post-mangling check as it should be. It was quite a bit of code to change for that. Basically it needs to flag whether the mangling happened already in NamedControl.

+ Symbol {

	mangle {
		if(currentEnvironment.notNil) {
			if(~controlNameMangler.notNil) {
				// This is mostly a fix for NdefMixer which creates ControlNames
				// while use()-ing ProxySpace. And in ProxySpace every environment
				// variable key is mapped to a non-nil value! Furthermore, default
				// values differ between keys, so can't check even "==" equality
				// with the value mapped to a "random" key. But it's reasonably safe
				// to check that a random key is mapped to a non-nil value, to
				// detect proxy spacess.
				//this.dumpBackTrace;
				if(~aiospd345fjaiohtgXXO.isNil) {
					//("cnm:" + ~controlNameMangler + "this:" + this).postln;
					^~controlNameMangler.value(this).asSymbol.postln
				} {
					// "Detected ProxySpace".postln;
				}
			}
		}
		^this
	}

+ ControlName {

	*new { arg name, index, rate, defaultValue, argNum, lag, preMangled = false;
		^super.newCopyArgs(
			if(preMangled) { name.asSymbol } { name.asSymbol.mangle }, 
			index, rate, defaultValue, argNum, lag ? 0.0)
	}
}


+ SynthDef {

	// this is actually called from Control with a ControlName object
	// after it is built, so it needs no changes really, just noting that here
	addControlName { arg cn;
		controlNames = controlNames.add(cn);
		allControlNames = allControlNames.add(cn);
	}

	// allow incremental building of controls
	// CHANGED: all these (except addNonControl) are called from NamedControl,
	// so they now need to know if preMangled
	addNonControl { arg name, values, preMangled = false;
		this.addControlName(ControlName(name, nil, 'noncontrol',
			values.copy, controlNames.size, 0.0, preMangled));
	}
	addIr { arg name, values, preMangled = false;
		this.addControlName(ControlName(name, controls.size, 'scalar',
			values.copy, controlNames.size, 0.0, preMangled));
	}
	addKr { arg name, values, lags, preMangled = false;
		this.addControlName(ControlName(name, controls.size, 'control',
			values.copy, controlNames.size, lags.copy, preMangled));
	}
	addTr { arg name, values, preMangled = false;
		this.addControlName(ControlName(name, controls.size, 'trigger',
			values.copy, controlNames.size, 0.0, preMangled));
	}
	addAr { arg name, values, preMangled = false;
		this.addControlName(ControlName(name, controls.size, 'audio',
			values.copy, controlNames.size, 0.0, preMangled))
	}

}


+ NamedControl {

	*new { arg name, values, rate, lags, fixedLag = false, spec;
		var res;

		this.initDict;

		 /* just this line is CHANGED in this whole method (but more in init) */
		name = name.asSymbol.mangle;

		if (spec.notNil) {
			spec = spec.asSpec;

			if (values.isNil) {
				values = spec.default;
			};
		};

		res = currentControls.at(name);

		lags = lags.deepCollect(inf, {|elem|
			if (elem == 0) { nil } { elem }
		});

		if (lags.rate == \scalar) {
			fixedLag = true;
		};

		if(res.isNil) {
			values = (values ? 0.0).asArray;
			res = super.newCopyArgs(name, values, lags, rate, fixedLag).init;
			currentControls.put(name, res);
		} {
			values = (values ? res.values).asArray;
			if(res.values != values) {
				Error("NamedControl: cannot have more than one set of "
					"default values in the same control.").throw;
			};
			if(rate.notNil and: { res.rate != rate }) {
				Error("NamedControl: cannot have  more than one set of "
					"rates in the same control.").throw;
			};

		};

		if(res.fixedLag and: lags.notNil) {
			if( res.lags != lags ) {
				Error("NamedControl: cannot have more than one set of "
					"fixed lag values in the same control.").throw;
			} {
				^res.control;
			}
		};

		if(spec.notNil) {
			res.spec = spec; // Set after we've finished without error.
		};

		^if(lags.notNil) {
			res.control.lag(lags).unbubble
		} {
			res.control
		}
	}

	init { /* CHANGED: all callbacks to buildSynthDef need to pass preMangled: true */
		var prefix, str;

		name !? {
			str = name.asString;
			if(str[1] == $_) { prefix = str[0] };
		};

		if(fixedLag && lags.notNil && prefix.isNil) {
			// not sure why next line doesn't pass lags, by the way (might be no-op downstream)
			buildSynthDef.addKr(name, values.unbubble, preMangled: true);
			if(rate === \audio) {
				control = LagControl.ar(values.flat.unbubble, lags)
			} {
				control = LagControl.kr(values.flat.unbubble, lags)
			};
		} {
			if(prefix == $a or: {rate === \audio}) {
				buildSynthDef.addAr(name, values.unbubble, preMangled: true);
				control = AudioControl.ar(values.flat.unbubble);

			} {
				if(prefix == $t or: {rate === \trigger}) {
					buildSynthDef.addTr(name, values.unbubble, preMangled: true);
					control = TrigControl.kr(values.flat.unbubble);
				} {
					if(prefix == $i or: {rate === \scalar}) {
						buildSynthDef.addIr(name, values.unbubble, preMangled: true);
						control = Control.ir(values.flat.unbubble);
					} {
						buildSynthDef.addKr(name, values.unbubble, preMangled: true);
						control = Control.kr(values.flat.unbubble);
					}
				}
			};
		};

		control = control.asArray.reshapeLike(values).unbubble;
	}
}

With all that now this bit works properly:

(~swrap = { arg func, suffix;
	var envir  = (controlNameMangler: { arg name; (name ++ suffix).postln; });
	SynthDef.wrap { envir.use { func.value } }; // This eats args!
})

~f2 = { SinOsc.ar(\freq.ar(333)) } // works now because it deduplicates suffixed
d = SynthDef(\LRtest, { Out.ar(0, [~swrap.(~f2, "Left"), ~swrap.(~f2, "Right")]) })
d.allControlNames

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.

1 Like

hey, i admire your work on this but have a hard time to follow the discussion.
im using the midisynth class from this thread Video Tutorials on MIDI based music production with Supercollider, jackd and DAW - #10 by droptableuser advanced by the oportunity to specify a specific tuning

MidiSynth : Ndef {

    var <synthdef, <hasGate, <instrument;
    var <noteonkey, <noteoffkey, <cckey;

    *new {|key|
        var res = Ndef.dictFor(Server.default).envir[key];
        if (res.isNil) {
            res = super.new(key).prInit;
        };
        ^res;
    }

    prInit {
        noteonkey = "%_noteon".format(this.key).asSymbol;
        noteoffkey = "%_noteff".format(this.key).asSymbol;
        cckey = "%_cc".format(this.key).asSymbol;
        ^this;
    }

	note {|noteChan, note, root(0), tuning|
		MIDIdef.noteOn(noteonkey, {|vel, note, chan|

			var tunedNote = note - root;
			tunedNote = tuning.wrapAt(tunedNote)
			+ tunedNote.trunc(tuning.stepsPerOctave)
			+ root;

			if (this.hasGate) {
				this.put(note, instrument, extraArgs:[
					\freq, tunedNote.midicps, \vel, vel/127, \gate, 1])
			} {
                this.put(note, instrument, extraArgs:[
					\freq, tunedNote.midicps, \vel, vel/127])
            }
        }, noteNum:note, chan:noteChan)
        .fix;

        MIDIdef.noteOff(noteoffkey, {|vel, note, chan|
            if (this.hasGate) {
                this.objects[note].set(\gate, 0);
            }
        }, noteNum:note, chan:noteChan)
        .fix;
    }

    synth {|synth|
        synthdef = SynthDescLib.global.at(synth);
        instrument = synth;
        hasGate = synthdef.hasGate;
        this.prime(synth);
    }

    cc {|ctrl, ccNum, ccChan=0|
        var order = Order.newFromIndices(ctrl.asArray, ccNum.asArray);
        MIDIdef.cc(cckey, {|val, num|
            var ctrl = order[num];
            var spec = if (this.getSpec(ctrl).notNil) {
                this.getSpec(ctrl)
            }{
                [0, 1].asSpec;
            };
            var mapped = spec.map(val/127);
            this.set(ctrl, mapped);
        }, ccNum:ccNum, chan:ccChan)
        .fix;
    }

    disconnect {
        MIDIdef.noteOn(noteonkey).permanent_(false).free;
        MIDIdef.noteOff(noteoffkey).permanent_(false).free;
        MIDIdef.cc(cckey).permanent_(false).free;
    }
}
(
(1..50).do({|partials|
	SynthDef(\additive ++ partials, {

		var sig, freqs, gainEnv;

		gainEnv = EnvGen.ar(Env.adsr(
			\atk.kr(0.07),
			\dec.kr(0.5),
			\sus.kr(1),
			\rel.kr(2),
			curve: \curve.kr(-4)
		), \gate.kr(1), doneAction:2);

		freqs = Array.fill(partials, {|i|
			\freq.kr(20) * (i+1);
		});

		sig = freqs.collect({|freq, i|
			var amps = \decay.kr(0.5) / (i+1);
			SinOsc.ar(freq) * amps;
		});

		sig = Mix(sig);

		sig = sig * gainEnv * \amp.kr(0.3) * \vel.kr(1);
		sig = Splay.ar(sig);
		Out.ar(\out.kr(0), sig)
	}).add;
});
)

(
MidiSynth(\m1).synth(\additive10);

MidiSynth(\m1).note(noteChan:0, root: 7, tuning: Tuning.at(\just)).cc(ctrl:[
	\atk,
	\dec,
	\sus,
	\rel,
	\curve,
	\amp,
	\vel,
	\out,
], ccNum:(1..8), ccChan:0);

MidiSynth(\m1).addSpec(
	\atk, [0.07, 8],
	\dec, [0.5, 4],
	\sus, [0.07, 8],
	\rel, [0.07, 20],
	\curve, [(-4), 4],
	\amp, [0, 1],
	\vel, [0, 1],
	\out, [0, 1],
).edit;
)

and also encountered the issue of using an exisiting SynthDef for an fx chain.
how can this be done without rewriting the fx SynthDef as a function like this:

	~granular_reverb = {
		arg in=0, overlap=0.6, minGrainDur=0.001,
		tFreq=2, tFreqMF=0, tFreqMD=0,
		rate=1, rateMF=0, rateMD=0,
		offset=0.015, offsetMF=0, offsetMD=0;

		var sig, readPos, writePos, grainDur;
		var trig, bufFrames, sndBuf, bufDur;

		var tFreqMod = {
			SinOsc.ar(tFreqMF, Rand(0.0,2pi)) * tFreqMD;
		};

		var rateMod = {
			SinOsc.ar(rateMF, Rand(0.0,2pi)).range(0, rateMD);
		};

		var offsetMod = {
			SinOsc.ar(offsetMF, Rand(0.0,2pi)).range(0, offsetMD);
		};

		tFreq = tFreq + tFreqMod.dup;
		rate = rate - rateMod.dup;

		bufFrames = 2**16;
		sndBuf = {LocalBuf(bufFrames).clear}!2;
		bufDur = BufDur.ir(sndBuf);

		writePos = Phasor.ar(end: bufFrames);

		trig = Impulse.ar(tFreq, [0, \rightTriggerPhase.kr(0.25)]);
		grainDur = max(tFreq.reciprocal * overlap.lag(5), minGrainDur);

		readPos = writePos - 64 / bufFrames - offset - offsetMod.dup;
		readPos = Wrap.ar(readPos, 0, 1);

		sig = GrainBufJ.ar(
			numChannels: 1,
			loop: 1,
			trigger: trig,
			dur: grainDur,
			sndbuf: sndBuf,
			rate: rate,
			pos: readPos,
			interp: 4,
			pan: 0,
		);

		sig = HPF.ar(sig, \grHpf.kr(90));
		sig = LPF.ar(sig, \grLpf.kr(12500));

		// writing granulated sig + input back to grain buffer
		sndBuf.do { |b i|
			BufWr.ar(sig[i] * \feedback.kr(0.1) + in[i], b, writePos)
		};

		sig.tanh;
	};

and then using:

(
MidiSynth(\m1).filter(210, ~granular_reverb).set(

	\rate, 2.00,
	\tFreq, 5.0,
	\offset, 0.15,

	\rateMD, 0.0,
	\tFreqMD, 5.0,
	\offsetMD, 0.0,

	\rateMF, 0.25,
	\tFreqMF, 2.0,
	\offsetMF, 0.15,

	\grHpf, 75.0,
	\grLpf, 9500.0,

	\overlap, 1,
	\rightTriggerPhase, 0.25,
	\feedback, 0.1,

	\wet210, 1,
);
)

to make the fx chain? thanks a lot.

I’m not sure how your code is related to the issue being discussed here, namely how to automagically rename controls of multiple filters running in the same Ndef.

It’s probably best if you asked your question in a separate thread or perhaps in the one discussing that video tutorial. Honestly, I don’t understand exactly what your question is, other than that your code probably doesn’t do what you want, but it’s not clear to me what the latter is.

i also tried to use the SynthDef(\granular_reverb) like this but had to rewrite it to the function ~granular_reverb and use the filter method fo make it work.

im sorry when my programming knowledge is not sufficient enough to follow the discussion.

Did you try this solution posed by @hemiketal in their original question? It works, but the issue was with name-clashes that result from adding multiple copies of the same SynthDef to Ndef slots. It should work fine with just your one slot dedicated to granular_reverb. Something like this:

MidiSynth(\m1).filter(210, SynthDescLib.global.at(\granular_reverb).def.func)

You can’t use the \filter role while specifying a synthdef. So you would just do something like

MidiSynth(\m1).put(210, \granular_reverb)

In your synth you need to define an \out control which will be the private bus created by the Ndef.

If you look at around line 267 here - /usr/share/SuperCollider/SCClassLibrary/JITLib/ProxySpace/wrapForNodeProxy.sc

you can see how Jitlib creates the fx synth from a function so if you follow the same conventions in your own synthdef it should work seemlessly

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.