Skip to main content

SuperCollider Tips

Here are some coding tips that may be of use for people using the SuperCollider language. Mostly a “stuff I wish I knew years earlier” post.

For some softer advice concerning broader technical and artistic use of SC, I have a new blog post called “Opinionated Advice for SuperCollider Beginners.”

Updated 2023-03-16.

Levels management and volume safety

Added 2023-03-16.

I’m putting this one first because it’s very, very important!

SC is notorious for producing sounds that can damage your hearing. Luckily, accidents are preventable, and it’s 100% possible to practice safe SuperColliding by getting into good habits. I have written the article “Levels and volume safety” on scsynth.org as an extensive guide to managing levels in SC.

A quick summary:

  1. If you are on macOS, upgrade to at least SC 3.12 right dang now and add Server.default.options.safetyClipThreshold = 1 to your startup file so the audio output clips.

  2. Work at low levels in your system’s volume control, but high levels in SC. If your audio is at a comfortable level and peaks in SC at -6 dBFS, the loudest sounds can only peak at 6 dB louder than that, so a synthesis accident can be startling, but unlikely to be dangerous.

  3. As a corollary: if SC produces a quiet signal, do not turn up the volume using your computer’s volume control! Instead, turn it up in SC.

  4. Use a Limiter on the master bus.

  5. Type in gain amounts as e.g. * -60.dbamp instead of * 0.001.

EnvGen, Env, and done actions

Here is a standard way to write an envelope generator:

env = EnvGen.kr(Env.adsr(0.01, 0.03, 0.7, 0.1), gate, doneAction: 2);
snd = snd * env;

There is nothing wrong with this, but there are nicer ways to write it. In 3.9 (?), SuperCollider added a bunch of constants like Done.freeSelf to improve readability:

env = EnvGen.kr(Env.adsr(0.01, 0.03, 0.7, 0.1), gate, doneAction: Done.freeSelf);
snd = snd * env;

Of course Done.freeSelf is just a constant 2, but I much prefer spelling it out to make the intentions more clear. In practice, I only use Done.freeSelf and Done.none. Check the Done helpfile for the full list.

In 3.7, a particularly nice shortcut was added, the instance method Env:-kr:

env = Env.adsr(0.01, 0.03, 0.7, 0.1).kr(Done.freeSelf, gate)
snd = snd * env;

Note the order of arguments – done action first, then gate. Internally, this creates an EnvGen and does the exact thing as above, but saves you typing and looks nicer. After I started writing envelope generators this way, I’ve never looked back.

One final bit of advice I have for envelope generators: always default to writing them in audio rate (ar) and not control rate (kr). Many real-life envelopes, such as percussive signals or Ryoji Ikeda, can get fast enough that the downsampling audibly loses sharpness. I write this:

env = Env.adsr(0.01, 0.03, 0.7, 0.1).ar(Done.freeSelf, gate);
snd = snd * env;

kr technically saves you some CPU, but unless you’re actually running into performance issues with your SynthDefs, this isn’t something I’d worry about. Get into the habit of using ar, only use kr when you need it.

Alternate symbol syntax in arrays

The code [a: 3, b: 4] does the same thing as [\a, 3, \b, 4]. This is particularly nice when writing arguments Synth:

synth = Synth(\foo, [\freq, 440, \amp, 0.1]);
synth = Synth(\foo, [freq: 440, amp: 0.1]);

You can run sclang from the command line

SC can be run without the IDE like a “normal” programming language. Just run sclang script.scd at the command line.

script.scd must be formatted in a special way. Rather than the usual floating chunks of code delimited by parentheses, the script must be a single code block executed all at once. This is equivalent in the IDE to booting the interpreter and immediately running Language → Evaluate File.

Let’s look at an example. If you have IDE code like this (with apologies for the terrible “music”):

// Run this first:
Server.default.boot;

// Run this second:
(
SynthDef(\sine, {
    var snd;
    snd = SinOsc.ar(\freq.kr(440));
    snd = snd * Env.perc(0.01, 0.3).ar(Done.freeSelf);
    Out.ar(0, snd);
}).add;
)

// Run this third:
(
Routine({
    10.do {
        Synth(\sine, [freq: exprand(100, 8000), amp: 0.1]);
        rrand(0.1, 0.5).yield;
    };
}).play;
)

To run this from the command line, condense all code blocks into a single block that is the equivalent of running all in sequence:

Server.default.waitForBoot {
    SynthDef(\sine, {
        var snd;
        snd = SinOsc.ar(\freq.kr(440));
        snd = snd * Env.perc(0.01, 0.3).ar(Done.freeSelf);
        Out.ar(0, snd);
    }).add;
    Server.default.sync;
    Routine({
        10.do {
            Synth(\sine, [freq: exprand(100, 8000), amp: 0.1]);
            rrand(0.1, 0.5).yield;
        };
        0.exit;
    }).play;
};

The method Server:-sync, in a Routine, is necessary since SynthDef:-add works asynchronously. If your code consists of SynthDef definitions and then Synth instantiations, you should put a Server:-sync in between them to avoid race conditions. Note that Server:-sync must take place in a Routine (in this case, waitForBoot already wraps our code in a Routine).

You have to write 0.exit manually at the completion of your piece. sclang doesn’t do the node.js thing where it exits automatically when there are no more outstanding asynchronous tasks.

This refactoring process might seem like an enormous pain in the neck, and a complete upheaval of the standard workflow in SuperCollider. I’d make a case that the first code example is “wrong” from a software engineering standpoint. Splitting a linear program across multiple, interactively executed code blocks and guessing the timing of asynchronous tasks simply isn’t a good programming practice, and proper asynchronous programming is needed if you want to run sclang as a command-line application.

That said, instantaneous feedback on your code (in particular, without having to restart sclang and scsynth) is tremendously helpful in a music programming environment, and a big reason why the IDE’s nonlinear execution style is so dominant. Using the IDE trades off proper coding structure for the creative benefits of interactivity. I tend to write in the nonlinear style when making music, and the more structured linear style when designing something more robust and application-like.

NamedControl

I wrote a post on the scsynth.org forums about NamedControls, why they are superior in multiple ways to the typical way to write SynthDefs. Instead of this:

// Old way
SynthDef(\sine, {
    |freq = 440, amp = 0.1, out = 0|
    Out.ar(out, SinOsc.ar(freq) * amp);
}).add;

Write this:

// New way
SynthDef(\sine, {
    Out.ar(\out.kr(0), SinOsc.ar(\freq.kr(440)) * \amp.kr(0.1));
}).add;

This seems like a purely surface-level change, and maybe even a step backwards since you no longer automatically have a nice summary of the arguments at the top of the SynthDef. However, NamedControls have tremendous advantages in readability over argument-style SynthDefs when you start using rate specifiers:

// Old way 1
// WARNING: names of actual arguments are "freq", "amp", and "out"!
// Also, don't accidentally name any argument starting with "a_", "k_", "i_", or "t_" or
// you'll experience difficult-to-diagnose bugs!
SynthDef(\sine, {
    |a_freq = 440, k_amp = 0.1, i_out = 0|
    Out.ar(i_out, SinOsc.ar(a_freq) * k_amp);
}).add;

// Old way 2
// WARNING: remember to update the "rates" array if you add, remove, or reorder arguments!
SynthDef(\sine, {
    |freq = 440, amp = 0.1, out = 0|
    Out.ar(out, SinOsc.ar(freq) * amp);
}, rates: [\ar, nil, \ir]).add;

// New way
SynthDef(\sine, {
    Out.ar(\out.kr(0), SinOsc.ar(\freq.kr(440)) * \amp.kr(0.1));
}).add;

Or multichannel arguments:

// Old way
SynthDef(\sine, {
    |freqs = #[440, 440, 440, 440, 440, 440, 440, 440]|
    ...
}).add;
// WARNING: attempting to abbreviate as "freqs = (440.dup(8))" will cause silent failure!

// New way
SynthDef(\sine, {
    var freqs = \freqs.kr(440.dup(8));
    ...
}).add;

As you can see with the warnings I’ve peppered around here, the old style has some nasty surprises lying in wait. I go into more detail on these arguments in the forum post, so go check it out.

mul/add

The mul and add arguments on UGens exist entirely for historical reasons, back when the compile-time optimization of SynthDefs was less smart. Nowadays, you can simply use the * and + binary operators, at no CPU penalty:

SinOsc.ar(440) * mul + add

This seems much more readable to me than

SinOsc.ar(440, mul, add)

since you don’t need to memorize the number of arguments that SinOsc expects. Ah hell, I forgot the phase argument. I meant SinOsc.ar(440, 0, mul, add).

mul and add also have a hidden surprise. Their behavior in multichannel expansion is inconsistent with regular UGen arguments:

{ LFNoise0.ar(freq: [100, 100], mul: 1) }.plot(0.2); // two different noise signals
{ LFNoise0.ar(freq: 100, mul: [1, 1]) }.plot(0.2); // two of the same noise signal

Internally, they are expanding differently:

{ LFNoise0.multiNew(\audio, [100, 100]) * 1 }.plot(0.2);
{ LFNoise0.multiNew(\audio, 100) * [1, 1] }.plot(0.2);

To avoid this trap, and improve the readability of your code, I recommend always using the binary operators.

Amplitude

Ever tried to use the Amplitude UGen for analysis, vocoding, compressor design, or whatever, and gotten weird or unexpected results?

env = Amplitude.kr(sig);

Amplitude has poor default settings, with both attack and release coefficients at 0.01. In most practical applications of envelope followers, you will want release significantly longer than attack. A good start would be:

env = Amplitude.ar(sig, 0.01, 0.1);

You will have to adjust these coefficients to fit your specific application. For analysis, you may want to crank the attack all the way down to 0 to maximize sensitivity and responsiveness. For vocoding, you may want slow, long-term attack/release like 0.1/1.0. Adjust until it sounds good.

Also, .ar vs .kr. There is generally no need to write Amplitude.kr and throw away sharp transient information. Use Amplitude.ar unless the optimization is specifically needed.

Always wrap Synth.new and Synth:set in Server.default.bind

Added 2023-03-16.

This tip was originally a thread on scsynth.org. This is an abridged version. For more discussion, see the thread.

If you venture outside the confines of the Patterns system, there is an important gotcha rarely covered in SC tutorials. Run these examples:

(
var s;
s = Server.default;
Routine({
    SynthDef(\ping, { Out.ar(\out.kr(0), (SinOsc.ar(440) * -5.dbamp * Env.perc(0.001, 0.1).ar(Done.freeSelf)) ! 2) }).add;
    s.sync;
    loop {
        Synth(\ping);
        0.05.wait;
    };
}).play;
)

(
var s;
s = Server.default;
Routine({
    SynthDef(\ping, { Out.ar(\out.kr(0), (SinOsc.ar(440) * -5.dbamp * Env.perc(0.001, 0.1).ar(Done.freeSelf)) ! 2) }).add;
    s.sync;
    loop {
        s.bind { Synth(\ping); };
        0.05.wait;
    };
}).play;
)

The first sounds jittery and uneven, but the second one sounds fine. The same is true of Synth:set:

(
var s;
s = Server.default;
Routine({
    var synth;
    SynthDef(\ping2, { Out.ar(\out.kr(0), (SinOsc.ar(440) * -5.dbamp * Env.perc(0.001, 0.1).ar(Done.none, \trigger.tr)) ! 2) }).add;
    s.sync;
    synth = Synth(\ping2);
    loop {
        synth.set(\trigger, 1);
        0.05.wait;
    };
}).play;
)

(
var s;
s = Server.default;
Routine({
    var synth;
    SynthDef(\ping2, { Out.ar(\out.kr(0), (SinOsc.ar(440) * -5.dbamp * Env.perc(0.001, 0.1).ar(Done.none, \trigger.tr)) ! 2) }).add;
    s.sync;
    s.bind { synth = Synth(\ping2); };
    loop {
        s.bind { synth.set(\trigger, 1); };
        0.05.wait;
    };
}).play;
)

The use of Server.default.bind is especially critical if you mix Pattern and non-Pattern code:

(
var s;
s = Server.default;
Routine({
    SynthDef(\ping, { Out.ar(\out.kr(0), (SinOsc.ar(\freq.kr(440)) * -5.dbamp * Env.perc(0.001, 0.1).ar(Done.freeSelf)) ! 2) }).add;
    s.sync;
    Synth(\ping);
    Pbind(\instrument, \ping, \freq, Pseq([660], 1)).play;
    (instrument: \ping, freq: 880).play;
}).play;
)

(
var s;
s = Server.default;
Routine({
    SynthDef(\ping, { Out.ar(\out.kr(0), (SinOsc.ar(\freq.kr(440)) * -5.dbamp * Env.perc(0.001, 0.1).ar(Done.freeSelf)) ! 2) }).add;
    s.sync;
    s.bind { Synth(\ping); };
    Pbind(\instrument, \ping, \freq, Pseq([660], 1)).play;
    (instrument: \ping, freq: 880).play;
}).play;
)

The first example attempts to play a Synth, a Pattern, and an Event at the same time. The Synth arrives early in the first example, while all are on time in the second example.

The client and server communicate by OSC. OSC messages, when in bundles, can be optionally adorned with a “time tag” that indicates the exact time when the message should be executed. If no time tag is specified, or the message is not in a bundle, the receiver must execute the OSC message as soon as it is received. A common use for time tags is to pre-empt OSC messages so their timing can be accurate instead of at the mercy of any inherent latency in OSC communication.

An unadorned Synth.new sends an /s_new message with no time tag, and so the server executes the OSC message whenever it’s received.

s.bind { ... } is shorthand for s.makeBundle(s.latency, { ... }). .makeBundle causes the Server object to temporarily change the behavior of sendMsg so that attempts to send new OSC messages instead add those OSC messages to a bundle. The function is immediately executed, and after it is completed, the OSC messages are scheduled s.latency seconds ahead. You can change s.latency if you want; the default of 0.2 is rather high. (s.latency is commonly misunderstood to be related to audio latency. It isn’t. In fact, the only place it is used is in OSC pre-emption, and scsynth isn’t even aware of it. Maybe it should have been called s.oscLatency?)

It is important to note that s.bind { ... }, despite having a callback function, is not asynchronous. The function is run immediately, and s.bind does not return until after the callback has completed execution.

The Patterns system – or more accurately, the default Event type – automatically runs .makeBundle. You can override this with the \latency key in the default Event type. Try setting it to nil in a pattern, which removes the time tag.

There is one case where you shouldn’t use s.bind { ... }: real-time interaction, such as from a MIDI controller, sensor, or another application. In such cases, it’s preferable to sacrifice timing accuracy for the sake of minimizing latency.

Preventing SC from automatically starting JACK on Linux

Added 2023-03-16.

On Linux using JACK, SC automatically tries to start a JACK server if none seem to be running, and that JACK server will shut down when SC does. This can be annoying if you’d rather start JACK yourself.

This isn’t SC’s fault since this behavior is the default when creating a client in the JACK API. Fortunately, JACK allows overriding this behavior by defining the environment variable JACK_NO_START_SERVER. If you want this to be the case for all JACK applications, you can add export JACK_NO_START_SERVER=1 to your ~/.profile. To make this setting SC-specific, "JACK_NO_START_SERVER".setenv("1"); in your startup file should do the trick.

Getting help

The longest-standing SC communities are the sc-users and sc-dev mailing lists. More recent, and less well known, are the web forum at scsynth.org and the Slack chat. If you are interested in participating in the community, I recommend checking them out.