SynthDef.wrap with two layers of outs

I think I know the answer to this, but asking in case I’m not seeing something.

Basically, I would like to specify a function like

{ |out|
  var sig = In.ar(out, 2);
  NHHall.ar(sig, 10);
}

which could also have other control arguments, and wrap it in a ReplaceOut… but when I do like:

(
var func = { |out|
  var sig = In.ar(out, 2);
  NHHall.ar(sig, 10);
};
SynthDef(\test, { |out|
  var sig = SynthDef.wrap(func);
  ReplaceOut.ar(out, sig);
}).add;
)

I get warned because both levels need access to the out parameter.

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

Your synthdef has been saved in the library and loaded on the server, if running.
Use of this synth in Patterns will not detect argument names automatically because of the duplicate name(s).

I have solved this temporarily using an Environment:

Environment.new.use {
  SynthDef(\test, { |out|
    ~out = out;
    ...

and then I can use ~out in my function. But this requires the user (= me in 10 years) to know that they need to in this one instance use ~out instead of |out| for the control. So still I wonder: Is there any way to collapse these |out|s?

Like this?

var sig = func.(out):

I don’t think wrap is ever a good idea because it hides the controls.

I don’t think so… what I want is something like {}.play, where it hides the additional out but exposes all function arguments as controls

Somehow this works, but I’m not sure how:

(
{ |in = 10|
  In.ar(in, 2);
}.play(outbus: 0);

x = { |out, time = 2.5|
  var sig = In.ar(out, 2);
  NHHall.ar(sig, time);
}.play(outbus: 10);

Pbind(
  \dur, 1,
  \legato, 0.2,
  \out, 10
).trace.play;
)


x.set(\time, 10)

It’s clear x is both reading from and writing to the specified outbus, and doesn’t complain about duplicated controls

I’m basically trying to do the same thing, but adding a SynthDef rather than playing it directly. And when you do:

x = { |out, time = 2.5|
  var sig = In.ar(out, 2);
  NHHall.ar(sig, time);
}.asSynthDef.add

you get that warning again

That is because one is called out and the other outbus.

Generally, it isn’t a good idea to use play when maintaining a large piece — it does too much ‘extra’ stuff.

If your intention is to build reusable components, then use functions as I suggested and explicitly pass in the controls from inside the synthdef. This way, when you read the synthdef in a few years, you will know what all the controls are. SynthDef.wrap obscures this, meaning you have to jump around to find all the controls.

Still, what you ask is possible:

var sig = SynthDef.wrap(func, prependArgs: [out]);

A limitation of SynthDef.wrap is that it builds controls for arguments that are not supplied with values – passive, rather than explicit. As Jordan says, making it hard to trace, and hard to control if argument names are in common between multiple wrapped functions.

That issue sounds like: a function’s argument list isn’t sufficient for the problem of mapping input values and signals onto control slots.

So a real solution would be to assess the requirements for this, and design a proper interface for it:

  • Supplying values for some arbitrary inputs and omitting others, without requiring supplied arguments to be first and consecutive (i.e. prependArgs is a hack that handles some simple cases but doesn’t scale).
  • Args whose inputs should be shared (in common) among multiple wrapped functions.
  • Args with the same name in multiple wrapped functions, but which should receive distinct names in the final SynthDef.

There are a lot of permutations – I think this isn’t an easy problem to get right. So @Eric_Sluyter don’t feel bad that SynthDef.wrap isn’t doing everything you wanted – it’s a tricky problem and SynthDef.wrap is a quick-and-dirty partial solution only.

FWIW I eventually quit trying to modularize my SynthDefs.

hjh

That’s not exactly true, while one is called out and the other i_out they boil down to the same argument:

(
{ |in = 10|
  In.ar(in, 2);
}.play(outbus: 0);

x = { |out, time = 2.5|
  var sig = In.ar(out.poll, 2);
  NHHall.ar(sig, time);
}.play(outbus: 10);

Pbind(
  \dur, 1,
  \legato, 0.2,
  \out, 10
).trace.play;
)

prints a series of

UGen(OutputProxy): 10
( 'out': 10, 'legato': 0.2, 'dur': 1 )

and this behavior works the same when you check the node tree and make new synths with the same defname, e.g.

(
Synth(\temp__7);
Synth(\temp__8, [out: 10]);
Pbind(
  \dur, 1,
  \legato, 0.2,
  \out, 10
).trace.play;
)

I’ve narrowed it down to:

(
var func = { |out|
  var sig = In.ar(out, 2);
  NHHall.ar(sig, 10);
};
var def = func.asSynthDef(outClass: \ReplaceOut, name: \test);
def.doSend(s);
)

(
{ |in = 10|
  In.ar(in, 2);
}.play(outbus: 0, addAction: \addToTail);

x = Synth(\test, [out: 10]);

Pbind(
  \dur, 1,
  \legato, 0.2,
  \out, 10
).trace.play;
)

but when you def.add instead of def.doSend you get

ERROR: Function argument 'out' already declared in Interpreter:functionCompileContext
  in interpreted text
  line 1 char 16:

  #{ arg out, out;
                  
  	var	x99D2319C = Array.new(4);
-----------------------------------

(I’ve come to peace with this, but still would be interested to know why…)

That asSynthDef method is responsible for the output, so it creates out for you. Therefore you should not create your own. Especially, note that your function declares out and then doesn’t use it. Extraneous code elements don’t give you any benefits, but they do create opportunities for problems. So it’s usually a good idea to delete superfluous bits from the code.

Now the kicker is – if your function created var out = \out.kr or the long form var out = NamedControl.kr(\out), then asSynthDef would share the name and I think the duplicate argument error would disappear. This is because NamedControl can collapse duplicate definitions down to one control input – but, your function didn’t use this mechanism, so the duplicate isn’t detected. (It’s difficult to include function arguments in the dup-removal logic.)

In any case:

  • If you wish to have control over every aspect of your SynthDef, then use SynthDef and avoid convenience methods like asSynthDef (which is mainly for internal use anyway).
  • If you really want to use asSynthDef, then let it manage the output and don’t step on its toes by writing unnecessary arguments.

hjh

My function does use it – but as an input bus. I got the pattern from an example from the ddwMixerChannel documentation:

~dly.playfx({ |out| DelayN.ar(In.ar(out, 2), 1, 1, 0.98) });

I was hoping for the same pattern, but saved in the SynthDescLib. But this pattern as I say only works when you .doSend and not .add

Wow, so I was wrong about everything in that post! :laughing:

Going back and checking the code (for GraphBuilder:wrapOut) – basically, because of the use of i_out in this method, there’s no way to use asSynthDef with .add in the way that you want here.

Probably best if you roll your own wrapper, then, e.g.

// assumes 'out' is the first func arg
(
~addDefFromFunction = { |name, func|
	SynthDef(name, {
		var out = NamedControl.kr(\out, 0);
		var graph = SynthDef.wrap(func, prependArgs: [out]);
		Out.perform(UGen.methodSelectorForRate(graph.rate), out, graph);
	}).add;
};
)

(
~addDefFromFunction.(\test, { |out|
	var in = In.ar(out, 2);
	in * 0.1
});
)

SynthDescLib.at(\test)  // *one* 'out' control, good

hjh