SynthDef vs. Ndef : overview

I’m building a new piece, and while I was starting to write some Ndef I realised this was not that obvious to decide when to go for Ndef and when to go for SynthDef, which were the use cases they were best suited for, what were their strengths and their limitations.

I tried to make this quick review of my knowledge and experience of both concepts on the following axes:

  • Amount of Synth instances possible
  • Composition/reusability of the definition
  • Live modification
  • Triggering (e.g. pattern, …)
  • Routing and organisation

Comparison

SynthDef

Amount of Synth instances:

∞, but :warning: Risk of loosing the reference to the running Synth’s if not stored correctly in variables

Composition/reusability:

No.
A SynthDef is a finite piece. One cannot use a SynthDef neither within a SynthDef nor within an Ndef.

Mitigation: work with functions and build SynthDef by wrapping these functions.

Live modification:

Yes/No
The SynthDef source can be modified live but the modification won’t apply to already playing Synth occurrences deriving from this SynthDef.

Triggering (e.g. pattern, …):

Can be controlled by a Pbind triggering as many Synth as required by the pattern

Routing and organisation:

The routing of the Synth’s is managed by the groups to which the Synth’s are added and the different addAction (\addToTail, …)

Ndef

Amount of Synth instances:

1
More can be made by “duplicating” the Ndef (Ndef(\x).copy(\y))

SynthDef composition/reusability:

Yes.
One can build one Ndef composed of different Ndef using Ndef(\x).ar

:warning: All the Ndef are running independently. Playing the “composed” Ndef does not start playing the “composing” Ndef’s. They must started independently.

Live modification:

Yes

Triggering (e.g. pattern, …):

Cannot be controlled by an external Pbind as a valid \instrument.

But can be controlled by an internal pattern defined as part of the Ndef, that controls its parameters, but not when it is playing or not.

Rem : a Ndef is more or less always running in background

Routing and organisation:

The routing of the Ndef can managed in different ways :

  • by the groups and the addAction’s

  • by chaining them with <<>

  • by stacking up different SynthDef in the same Ndef and by using NodeProxy roles

Conclusion

I would say SynthDef are more suited when/for

  • multiple occurrences are of a same definition are required
  • the Synths have explicit start and releases
  • fine and changing control by Patterns (e.g.) is required

I would say Ndef are more suited when/for

  • Live modification of the Synth definition is required
  • Complex routing or Synth organisation

What do you think of this overview ?
Have I not missed too many things ?

Not quite true… Take a look at SynthDef.wrap.

I think this mixes up the idea of order of execution which groups help with, and routing, which is about sending signals over busses somewhere.

As I far as I know, once built, a SynthDef cannot be used within another SynthDef. I mentioned SynthDef.wrap as a mitigation of that limitation :

So you cannot write:

SynthDef(\fx1,{...});
SynthDef(\fx2,{...});

SynthDef(\fx3,{ | in | }
    var sig=SynthDef(\fx1).ar(\in, in);
    sig=SynthDef(\fx2).ar(\in, sig);
    ReplaceOut.ar(\out.kr(0),sig);
);

Indeed. I’ll correct my post with this.

To further react on this one. I tried to wrap SynthDef within another SynthDef.

(
d=SynthDef(\sine, { |freq=440, out=0, amp=0.5|
	var sig=SinOsc.ar(440)*amp;
	ReplaceOut.ar(out,sig!2);
}).add;

e=SynthDef(\wave, { |rate = 1, deep=0.2|
	var trem=SinOsc.kr(rate,mul:deep,add: 1-deep)*EnvGate.new;
	var sig;
	var out=\out.kr(0);
	SynthDef.wrap(d.func,prependArgs:[\out,2]);  // would need to foreseen some extra bus allocation
	sig=In.ar(2,2);
	// sig=SinOsc.ar(440)*\amp.kr(0.5);
	sig=sig*trem;
	ReplaceOut.ar(out,sig);
}).add;
)

This only works if one have an explicit reference to SynthDef to wrap within:

d=SynthDef(\sine, ...
SynthDef.wrap(d.func, ...

This doesn’t work:

SynthDef(\sine, ...
SynthDef.wrap(SynthDef(\sine).func, ...

So at the level of Synth composition, Ndef remains superior to SynthDef, isn’t it ?

Your code example is slightly wrong, you don’t need the input output thing as you can get an output from SynthDef.wrap, it just returns the last line like any function.

~insert = SynthDef(\ringmod_insert, { |ins|
	ins * SinOsc.ar(\ringmod_freq.kr(220))
})

SynthDef(\ex, {
	var signal = SinOsc.ar(342);
	var rm = SynthDef.wrap(~insert.func, prependArgs: [signal]);
	Out.ar(0, rm * 0.2).poll;
}).add;

But your second bit is absolutely right. My mistake, I forgot I wrote a wrapper class to do this for me. Basically SynthMixinDef(\name, {}) and SynthMixin(\name, ...args).

Also since you can use a function where ever, I don’t think its quite right to say a flat ‘No’ to synthdef’s composition and re-usability.

I would agree the Ndef is more ergonomic here (think that is a better word that superior), but it depends how often you need to do this. Personally, I only use it for analysis stuff - like getting a bunch of spectral descriptions from many synths at once, so its easier to stick with synthdef.

You could also spin…

… as a positive, it doesn’t take up language resources, might be useful if you make many at once.

Perhaps this should be performance-time changes are more ergonomic in Ndef. As both can be routed or organised however. You can technically move synth’s around too (they are just nodes) but its annoying to do.

By the way, mostly I agree with your assessment, there are just a few areas that need considering if some one else is to take this as a learning resource.

Ndef and SynthDef are often talked about as separate paradigms, not generally intermingled, but one cool thing about Ndef that I have discovered recently is that, since it has its own private bus, it can be mapped to any control input. In other words, it can be used as an argument in a Synth or a Pbind, which makes it great as an LFO. Any time I find myself creating busses and groups to control signal flow, I have to ask myself if it would be better to just use an Ndef. Compare these usages:

(
SynthDef(\freq, {|out|
    Out.kr(out, SinOsc.kr(6).exprange(380, 400));
}).add;
)
~vib_bus = Bus.control(s);
~syn = Synth(\default);
Synth(\freq, [out: ~vib_bus]);
~syn.map(\freq, ~vib_bus);
~syn.free;

// vs.

Ndef(\freq, {SinOsc.kr(6).exprange(380, 400)});
~syn = Synth(\default);
~syn.map(\freq, Ndef(\freq));
~syn.free;

// or

(
Pbind(
    \dur, Pwhite(0.2, 1),
    \legato, 0.3,
    \freq, Ndef(\freq)
).play;
)

// add a source

Ndef(\freq).add({LFNoise0.kr(3).exprange(20, 400)});

Anyone else using Ndefs like this? Do you see drawbacks to this approach? I’m also curious if anyone is aware of other “non-standard” uses of Ndef.

3 Likes