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.

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.

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.