SynthDef.wrap

Can someone please explain the use of SynthDef.wrap for developing more modular code? I’ve seen the method reference and I understand how I could use it to more conveniently add effects, but I feel like I’m not really able to take full advantage its use. I’ve seen Mads’ example but I still don’t really understand the rationale for its use.

// Filter functions organized in a dictionary (Event)
// The signal of our synth will be passed in as the first argument
f = (
    hpf: { |in, cutoff=1000, rq=1|
        RHPF.ar(in, cutoff, rq)
    },
    bpf: { |in, cutoff=1000, rq=1|
        BPF.ar(in, cutoff, rq)
    },
    lpf: { |in, cutoff=1000, rq=1|
        RLPF.ar(in, cutoff, rq)
    }
);

// Iterate over all the filters we defined above and use them in a SynthDef
f.keysValuesDo{|filtername, filterfunction| 
    var synthdefname = "saw" ++ filtername.asString;

    SynthDef.new(synthdefname, { |freq=220, out=0|
        var sig = Saw.ar(freq, mul:0.1);

        sig = SynthDef.wrap(
            filterfunction,  
            prependArgs: [sig] // Pass signal in to the filter
            // NOTE: prependArgs HAVE to be inside of []
        ); 

        Out.ar(out, sig)
    }).add;
};

)

So I tried to use this design pattern to make a simple subtractive synth:

(
var osc, filt, defName;

osc = (
	sin: { |freq = 261.63|
		SinOsc.ar(freq);
	},
	tri: { |freq = 261.63|
		LFTri.ar(freq);
	},
	sqr: { |freq = 261.63|
		LFPulse.ar(freq).unipolar;
	},
	saw: { |freq = 261.63|
		LFSaw.ar(freq);
	}
);

filt = (
	lpf: { |in, cutoff = 1000, rq = 1|
		RLPF.ar(in, cutoff, rq);
	},
	bpf: { |in, cutoff = 1000, rq = 1|
		BPF.ar(in, cutoff, rq);
	},
	hpf: { |in, cutoff = 1000, rq = 1|
		RHPF.ar(in, cutoff, rq);
	}
);

osc.keysValuesDo{|oscName, oscFunc|
	filt.keysValuesDo{|filtName, filtFunc|
		var defName = oscName.asString ++ filtName.asString[0].toUpper ++ filtName.asString[1..];
		SynthDef(defName, {
			var sig = SynthDef.wrap(oscFunc);
			sig = SynthDef.wrap(filtFunc, prependArgs:[sig]);
			Out.ar(\out.kr(0), sig!2);
		}).add;
	};
}
)

But why do this? Now I just have a bunch of SynthDefs that I can access with \osc_filt. I could just as easily do this:

~build = {|osc, freq, filt, cutoff|
	{
		var sig = osc.ar(freq:freq);
		sig = filt.ar(in: sig, freq: cutoff);
	}.play;
}

How do others use SynthDef.wrap, when, and why? It seems like it could be incredibly useful, but I feel like I’m missing something.

wrap converts the function arguments into controls. If you insist on using function arguments rather than NamedControl'then there is a use… otherwise as Nathan said.

I don’t normally use argument style. Sometimes I will wrap a function in a SynthDef and use arguments there (something beyond an iteration function), but this seems… disappointing. Sounds like I was probably hoping for way more than I could’ve ever possibly gotten from it

FWIW after a decade and a half of trying to modularize my SynthDef components, eventually (a few years ago) I found myself just writing flat SynthDefs and getting more done that way.

OTOH… I have had for awhile a way of using JITLib to break up synthesis into modules: one module = one NodeProxy, and a whole ProxySpace is one synth (separate ProxySpaces for separate instruments), with the ability to save and restore of the patch. This summer I started on a decompiler for these “JITModular” patches – hit a button and it merges the modules into sclang code for one SynthDef. The resulting code is ugly! But it could end up being a way to experiment with synthesis in modular terms and then get a usable SynthDef (usable, not especially readable). I’ve got working examples, though I’m sure I haven’t touched all the UGens that need special handling.

Could I ask… what’s the meaning that this wording is intended to convey?

hjh

Just to follow up on these, because I wasn’t clear and I don’t think the actually benifits of SynthDef.wrap have been mentioned.

— This ended up longer than I hoped —

You might want to use a function or wrap when you need to inject code into a synthdef…
but there is complexity when it comes to Controls and IO.

The ideal use case, which nathan mentioned, is like this…

1
~f = { |input, amp| input * amp_c }; // something useful

SynthDef(\Fixed, { // no controls
	~f.(input: SinOsc.ar, amp: -10.dbamp) 
});
SynthDef(\Control, { 
	~f.(input: SinOsc.ar, amp: \amp.kr(0.1)) 
});

Here, each synthdef’s controls are easy to see because they are written where the definition of the synthdef is, not nested (perhaps deeply) inside another function, and by not using In or Out it is obvious what is being processed.

These functions can themselves be nested and composed quite nicely, here is what one of the SynthDef.wrap examples looks like written in this style keeping all controls at the top:

2
~mkBusEffect = { |bus, numChannels, wet, gate, fx|
	var env = Linen.kr(gate, 2, 1, 2, 2);
	In.ar(bus, numChannels) |> fx |> XOut.ar(bus, wet * env, _)
};

SynthDef(\effectA, {
	var fx = { |in|
		var lfo = LFNoise1.kr(\rate.kr(0.7), \depth.kr(0.8) * \ffreq.kr(1200), \ffreq.kr);
		RLPF.ar(in, lfp, \rq.kr(0.1), 10).distort * 0.15
	};
	~mkBusEffect.(\bus.kr, 2, \wet.kr(0), \gate.kr(1), fx)
});

And here is the example stripped down a bit, note how it hides the controls i_bus, gate and wet:

3
~makeEffect = { |name, numChannels, func|
	SynthDef(name, { | i_bus = 0, gate = 1, wet = 1|
		var in = In.ar(i_bus, numChannels);
		var env = Linen.kr(gate, 2, 1, 2, 2); 
		var sound = SynthDef.wrap(func, prependArgs: [in, env]);
		XOut.ar(i_bus, wet * env, sound);
	}).add;
};

~makeEffect.value(\wah, numChannels: 2,
	func: { |in, env, rate = 0.7, ffreq = 1200, depth = 0.8, rq = 0.1|
		var lfo = LFNoise1.kr(rate, depth * ffreq, ffreq);
		RLPF.ar(in, lfo, rq, 10).distort * 0.15;
	}
);

However, if you need to have many synths all with the same control names so they have the same interface, it would be nice to have the function create the controls. With NameControl this is easy to solve, but SynthDef.wrap is required for arguments-as-controls. Generally it isn’t a good idea to mix the actually function definition with the controls as you can’t separate them later (SynthDef.wrap does let you remove controls though, see next example), so its better the wrap them in another function.

4
~fWithControls = { |sig| 
	~f.(input: sig, amp: \uniformly_named_amp.kr(0)) 
};

SynthDef(\funcMixedIn, { ~fWithControls.(SinOsc.ar) });

The final case I can think of, is when you are trying to take existing synthdef and inject them into a new synthdef. But this is almost always impossible because most of the time In and Out are used and these cannot be overriden. It is possible to map a bus to a control, which could be overrided, but there is no way to do this for the output, instead SynthDef.wrap returns the last line, but assuming the synthdef was written with that in mind, the different way of specifying the controls could look like this…

5
SynthDef(\existingDef, { |input, amp| 
	input * amp 
}).add

~synthDef2Func = { |name| SynthDescLib.global.at(name).def.func }; 

SynthDef(\wrapFixed, {
	SynthDef.wrap(~synthDef2Func.(\existingDef), prependArgs: [sig, 0]) 
});

SynthDef(\wrapArg, {
	SynthDef.wrap(~synthDef2Func.(\existingDef), prependArgs: [sig, \amp.kr(0)])
});

SynthDef(\wrapMixedIn, {
	// automatically creates control names
	SynthDef.wrap(~synthDef2Func.(\existingDef), prependArgs: [sig]) 
});

TLDR: for modular code use function without any controls, or write your own pseudo ugen classes.
SynthDef.wrap does two things, promotes arguments to controls implicitly (probably a bad thing) and lets you remove a control and replace it with a value/ugen.

1 Like

Thanks for the clarification – so the intended meaning was really “in the context of modularized synthesis functions, arguments promoted to controls introduce a handful of technical problems and are probably best avoided.”

It sounded to me a bit like a general pejorative aimed at any use of function arguments for SynthDef inputs. If that were the case, it would be unnecessary and unhelpful. As stated, it was not clear to me – the double-emphasis (with eye roll?) for me introduced some confusion into the discussion.

FWIW SynthDef.wrap (IIRC) is used for example to wrap { ... }.play in an envelope while preserving arguments as controls – so it needs to be there. That doesn’t mean it’s a good way in general to modularize synthesis (where I’d agree with Jordan’s conclusion that pseudo-UGens are likely to be more successful – I did exactly that for a ConstantGainDistortion thingy that I found myself copy/pasting too many times).

hjh

My opinion remains unchanged but I’m glad I have a better sense of how it works now. I don’t like the promotion of argument names to the outer def. It seems like more work remembering the argument names you’ve used between nested SynthDefs as opposed to any of the many other (IMO better and I’d be inclined to say “more conventional”) ways to achieve the same result.

I also found something interesting that might need to be fixed while playing around with .wrap.

Bad SynthDef:

(
x = SynthDef(\foo, {
	var sig = LFSaw.ar(\freq.kr(100).poll(label: \osc));
	var filt = SynthDef.wrap({|in, freq = 1000|
		LPF.ar(in, freq.poll(label: \filt));
	}, [\ar, \kr], sig);
	Out.ar(\out.kr(0), sig!2);
}).play;
)

x.set(\freq, 300) //sets osc frequency only

If you add this SynthDef instead of playing it, it throws a warning:

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

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).

So you could fix it with:

(
SynthDef(\foo, {
	var sig = LFSaw.ar(\freq.kr(100).poll(label: \osc));
	var filt = SynthDef.wrap({|in, freq|
		LPF.ar(in, freq.poll(label: \filt));
	}, [\ar, \kr], [sig, \fFreq.kr(1000)]);
	Out.ar(\out.kr(0), sig!2);
}).add;
)
)

but then why not just:

(
SynthDef(\foo, {
	var sig = LFSaw.ar(\freq.kr(100).poll(label: \osc));
	sig = LPF.ar(sig, \fFreq.kr(1000).poll(label: \filt));
	Out.ar(\out.kr(0), sig!2);
}).add;
)

Or use a bus.

My current approach to modularity and reusability consists of keeping a file with SynthDefs I add to the global desclib or writing pseudo-UGens (like a sine sweep I was tired of retyping).