Overview

Here you can find notes on what SynthDefs are, how they are used and how Synths are working withing the server.

SynthDefs and Synths

A SynthDef is one type of server abstraction in SuperCollider. There are several others (like Buffer, Group, or Bus), which all allow the client ( SuperCollider - sclang ) to control objects running on the audio server (scsynth).

A SynthDef is similar to a user-defined class. Once defined, it can be instantiated on the server as a Synth object and controlled from the client.

Compared to functions (which are evaluated and played immediately), SynthDef instances can be created, modified, and controlled over time. You can send messages to a Synth (an instance of a SynthDef) to change its parameters while it’s running. A function, on the other hand, must be re-evaluated entirely to change its behavior.

You can define a SynthDef via the new method. Here is an example that describes how to translate a simple function to a SynthDef:

//first the Function
{ SinOsc.ar(440, 0, 0.2) }.play;
 
// now here's an equivalent SynthDef
SynthDef.new("tutorial-SinOsc", { |out| Out.ar(out, SinOsc.ar(440, 0, 0.2)) }).play;

SynthDef.new takes a number of arguments. The first is a name, usually in the form of a String as above. But nowadays it’s more common to use a symbol (this is a symbol \symbol). The second is in fact a Function. This argument is called a UGen Graph Function, as it tells the server how to connect together its various UGens.

NOTE

Within the function braces, the |out| argument defines a SynthDef control input, which is then used as the first input to Out.ar. It is a good habit to provide an out control in every SynthDef.

Here is an example with multichannel expansion in the Out.ar object.

(
SynthDef.new("tutorial-SinOsc-stereo", { |out|
    var outArray;
    outArray = [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)];
    Out.ar(out, outArray)
}).play;
)

Both, Function-Play and SynthDef-Play return an Object, a Synth, which represents a synth on the server. But since it is yet not stored in variable, we lose some control over it. When it is stored in a variable we can apply some methods on it, e.g. free a specific synth to free some resources and stop it from making sounds.

x = { SinOsc.ar(660, 0, 0.2) }.play;
y = SynthDef.new("tutorial-SinOsc", { |out| Out.ar(out, SinOsc.ar(440, 0, 0.2)) }).play;
x.free;    // free just x
y.free;    // free just y

It is more common to add the SynthDef ‘class’ to the server, before making a instance of the SynthDef. This saves ressources and time, since the UGen Graph Function is evaluated only once. This also allows assigning multiple instances of the Synth to a unique variable, which allows control over this specific instance.

// execute first, by itself
SynthDef.new("tutorial-PinkNoise", { |out| Out.ar(out, PinkNoise.ar(0.3)) }).add;
 
// then:
x = Synth.new("tutorial-PinkNoise");
y = Synth.new("tutorial-PinkNoise");
x.free; y.free;

But since you are evaluating the SynthDef only once, there comes some limitations in comparision with using functions. Compare these two examples.

// first with a Function. Note the random frequency each time 'play' is called.
f = { SinOsc.ar(440 + 200.rand, 0, 0.2) };
x = f.play;
y = f.play;
z = f.play;
x.free; y.free; z.free;
 
// Now with a SynthDef. No randomness!
SynthDef("tutorial-NoRand", { |out| Out.ar(out, SinOsc.ar(440 + 200.rand, 0, 0.2)) }).add;
x = Synth("tutorial-NoRand");
y = Synth("tutorial-NoRand");
z = Synth("tutorial-NoRand");
x.free; y.free; z.free;

The rand message in the SynthDef is only evaluated once, so there is only one random value generated, shared by all future instances.

There are solution for this, e.g. the Rand UGen. But the most common way would be to create an argument and set the value for the instance.

(
SynthDef("tutorial-args", { arg freq = 440, out = 0;
    Out.ar(out, SinOsc.ar(freq, 0, 0.2));
}).add;
)
x = Synth("tutorial-args");                // no args, so default values
y = Synth("tutorial-args", ["freq", 660]);        // change freq
z = Synth("tutorial-args", ["freq", 880, "out", 1]);    // change freq and output channel
x.free; y.free; z.free;

When you evaluate a Function-Play statement like { SinOsc.ar }.play SuperCollider unpacks this process into (1.) the creation of a SynthDef, (2.) sends this SynthDef to the server, and (3.) the immediate playback of this SynthDef by creating a Synth instance. Basically evaluating a Function-Play statement is a shortcurt for creeating a SynthDef and creating a Synth Object.

Complex Example

Here is an complex example which makes extensive use of multichannel expansion, and Array.fill mechanism to create a 6 note chord, and a SystemClock.sched to set a new frequency at a random point in time.

// Create SynthDef
(
SynthDef(\wow, {
  arg freq = 60, amp = 0.1, gate = 1, wowrelease = 3;
  var chorus, source, filtermod, env, snd;
 
  chorus = Lag.kr(freq, 2) * LFNoise2.kr([0.4, 0.5, 0.7, 1, 2, 5, 10]).range(1, 1.02);
  source = LFSaw.ar(chorus) * 0.5;
  filtermod = SinOsc.kr(1/16).range(1, 10);
  env = Env.asr(1, amp, wowrelease).kr(2, gate);
  snd = LPF.ar(in: source, freq: freq * filtermod, mul: env);
  Out.ar(0, Splay.ar(snd))
}).add;
)
 
// Watch the Node Tree
s.plotTree;
 
// Create a 6−note chord
a = Array.fill(6, {Synth(\wow, [\freq, rrand(40, 70).midicps, \amp, rrand(0.1, 0.5)])});
 
// Release notes one by one
a[0].set(\gate, 0);
a[1].set(\gate, 0);
a[2].set(\gate, 0);
a[3].set(\gate, 0);
a[4].set(\gate, 0);
a[5].set(\gate, 0);
 
// ADVANCED: run 6−note chord again, then evaluate this line
SystemClock.sched(0, { a[5.rand].set(\freq, rrand(40, 70).midicps); rrand(3, 10) });

Playing SynthDefs with Pbinds

With the key argument \instrument you can call every SynthDef you defined before. It is also possible to access every argument you introduced in your SynthDef, just call the proper key argument and assigne a value to it. If you want to use the pitch conversion facilities of Pbind you have to use freq as an argument in your SynthDef, Pbind will do the math for you. When using a sustained envelope like Env.adsr you need to set a default argument gate = 1, Pbind will access this argument and send automatically a 0 to the gate argument. If you not use a sustained envelope, make sure to use doneAction: 2 in your SynthDef.

(
SynthDef(\pluck, {arg amp = 0.1, freq = 440, decay = 5, mutedString = 0.1;
  var env, snd;
  env = Env.linen(0, decay, 0).kr(doneAction: 2);
  snd = Pluck.ar(
    in: WhiteNoise.ar(amp),
    trig: Impulse.kr(0),
    maxdelaytime: 0.1,
    delaytime: freq.reciprocal,
    decaytime: decay,
    coef: mutedString);
    Out.ar(0, [snd, snd]);
}).add;
)
 
(
Pbind(
  \instrument, \pluck,
  \amp, Pwhite(0.4, 0.6, inf),
  \scale, Scale.dorian,
  \degree, Pwhite(-12, 12),
  \decay, Pseq((10..1), inf),
  \mutedString, Pwhite(0.05, 0.2),
  \dur, 0.2
).play;
)