Overview

Here are some notes on the most basic topics of making Sounds in SuperCollider.

Basics of making a Sound

UGens and synth functions

The sound generators in SuperCollider are called UGens (unit generators). These can receive messages like ar, which tells the UGen to operate at audio rate. This means the UGen calculates a new value for every sample.

There is also the kr message, which tells a UGen to operate at control rate. This means it calculates a new value only once per control block. For example, if the block size is 64, the UGen generates a new value every 64 samples.

Most UGens can receive both ar and kr messages, but not every UGen supports both. It depends on the UGen’s implementation.

This example creates a function containing a SinOsc UGen, which receives the ar message with some arguments. The function then receives the play message. The play message evaluates the function and sends the resulting UGen graph to the server.

{ SinOsc.ar(440, 0, 0.2) }.play;

There are some common arguments to UGens which you will encounter frequently:

  • freq – the frequency at which the UGen should operate
  • phase – the initial phase of the UGen
  • mul – a factor that multiplies the output of the UGen
  • add – an offset value added to the output of the UGen

Here is a example which shows how to patch UGens together:

(
{ var ampOsc;
    ampOsc = SinOsc.kr(0.5, 1.5pi, 0.5, 0.5);
    SinOsc.ar(440, 0, ampOsc);
}.play;
)

A very common way of starting and stopping individual synths is to assign them to a variable and apply the play method at the same time. After this you can apply the free method to the variables.

// Run one line at a time (don't stop the sound in between):
a = { Saw.ar(LFNoise2.kr(8).range(1000, 2000), mul: 0.2) }.play;
b = { Saw.ar(LFNoise2.kr(7).range(100, 1000), mul: 0.2) }.play;
c = { Saw.ar(LFNoise0.kr(15).range(2000, 3000), mul: 0.1) }.play;
// Stop synths individually:
a.free;
b.free;
c.free;

When you create a synth function, you can also use arguments within these, like in every function. With the set method you can change the values of these arguments after starting your synth function. It’s good practice to provide default values for these arguments.

x = {arg freq = 440, amp = 0.1; SinOsc.ar(freq, 0, amp)}.play;
x.set(\freq, 778);
x.set(\amp, 0.5);
x.set(\freq, 920, \amp, 0.2);
x.free;

When a Synth is created by playing a function with .play, it includes a hidden \gate and \fadeTime parameter. These allow graceful stopping of the synth via e.g. x.set(\gate, 0) or x.set(\gate, 0, \fadeTime, 2). The envelope and cleanup behavior are added automatically, even though they are not explicitly written in the code.

Basic UGens and Oscillators

You can find an overview on the most basic UGens in the Help Document: Tour of UGens.

Here is a short overview of the most common oscillators in SuperCollider.

// standard oscillators
{ SinOsc.ar }.plot; // sine wave
{ Saw.ar }.plot;    // sawtooth wave
{ Pulse.ar }.plot;  // square wave, pulse wave
{ LFTri.ar }.plot;  // triangle wave
{ VarSaw.ar(width: 0.75 )}.plot; // variable saw and triangle wave
{ Blip.ar }.plot; // bandlimted impulse oscillator
{ WhiteNoise.ar }.plot; // white noise
{ PinkNoise.ar }.plot;  // pink noise
{ LFNoise0.ar }.plot; // stepped noise
{ LFNoise1.ar }.plot; // linear interpolated noise
{ LFNoise2.ar }.plot; // cubic interpolated noise

There are UGens that generate Unipolar and Bipolar signals per default. With the plot or poll message, you can check what your chosen UGens is outputting. poll is printing 10 numbers per second to the post window.

{ SinOsc.kr(1).poll }.play;
{ LFNoise0.ar(1).poll }.play;
{ MouseX.kr(minval: 300, maxval: 2500, lag: 10).poll }.play;

You can also use scope instead of play to play a signal and put it on an oscilloscope at the same time.

{ Pulse.ar(freq: MouseX.kr(100, 600), width: MouseX.kr(0, 1), mul: MouseY.kr(0, 1) )}.scope;

Scale Numbers and Signals

The range mehtod scales an output signal to a predefined range, between a minimum and maximum. It expects, that you use the default output signal. That means you should not use it in conjunction with add and mul.

{ SinOsc.ar(freq: LFNoise0.kr(10).range(500, 1500), mul: 0.1) }.play;
{ LFPulse.ar(freq: 200, width: MouseY.kr(0, 1)).range(-0.5, 0.5) }.scope;

With methods like linlin, explin, expexp and linexp you can convert a linear or exponential range to another linear or exponential range.

a = [1,2,3,4,5,6,7];
// Rescale to 0−127, linear to linear
a.linlin(1, 7, 0, 127).round(1);
// Rescale to 0−127, linear to exponential
a.linexp(1, 7, 0.01, 127).round(1); // don't use zero for an exponential range

Multichannel Expansion

When you use an Array as argument to an UGen like SinOsc.ar, this will cause multichannel expansion. This means, when you plug an Array into one of a UGen’s arguments, you get an Array of that UGen instead of a single one.

// multichannel expansion to stereo
{ SinOsc.ar([440, 442], 0, 0.2) }.play;

By default a monophonic signal is send to the first output channel (left) respectively to the first output bus. When you create an array of UGens, these are send to consecutive output busses, starting at the first. But this also means, when you create an array which size is larger then the size of your output busses, you will just hear the signals going in to you busses, that are currently avaible.

The position of the multichannel expansion is very important. It describes either a process is duplicated or just a signal. Compare these two examples.

// compare these two functions
(
{
  var numOfSignals = 128;
  var noise = PinkNoise.ar(1/2 ! numOfSignals);
  var cf = LFNoise1.kr(1 ! numOfSignals).exprange(160, 1600);
  var filter = BPF.ar(noise, cf, 0.01);
  Splay.ar(filter);
}.play;
)
 
(
{
  var numOfSignals = 128;
  var noise = PinkNoise.ar(1/2 ! numOfSignals);
  var cf = LFNoise1.kr(1).exprange(160, 640) ! numOfSignals;
  var filter = BPF.ar(noise, cf, 0.01);
  Splay.ar(filter);
}.play;
)

From time to time you can run into this error message: "exceeded number of interconnect buffers" . There is a default limit of allowed interconnections in a UGen Buffer of 64, you can rise this with s.options.numWireBufs_(NUM).

With the choose message on an Array, everytime the function is called, it will choose randomly one of it’s data slots. So, when you use an Array with mixed data, like this one [[660, 880], [440, 660], 1320, 880], as input, you will get random stereo expansion or a monophonic signal.

(
{ var freq;
    freq = [[660, 880], [440, 660], 1320, 880].choose;
    SinOsc.ar(freq, 0, 0.2);
}.play;
)

Panning

Stereo Panning is made easy with the Pan2.ar UGen. It expects a monophonic signal and a panning Signal between -1 (hard left) and 1 (hard right).

// the PinkNoise is panned between -0.5 and 0.5
{ Pan2.ar(PinkNoise.ar(0.2), SinOsc.kr(0.5)) }.play;

This is a more complex example.

(
x = {
  var lfn = LFNoise2.kr(1);
  var saw = Saw.ar(
    freq: 30,
    mul: LFPulse.kr(
      freq: LFNoise1.kr(1).range(1, 10),
      width: 0.1));
  var bpf = BPF.ar(in: saw, freq: lfn.range(500, 2500), rq: 0.01, mul: 20);
  Pan2.ar(in: bpf, pos: lfn);
}.play;
)
x.free;

When you work of with a stereo signal you can also use Balance2.ar, which allows balancing the two signal channels.

// balancing between PinkNoise and a Sine wave
(
{
  var sine = SinOsc.ar(220, mul: 0.1);
  var noise = PinkNoise.ar(0.1);
  var signals = [sine, noise];
  var balance = LFNoise1.kr(1).range(-1, 1);
  Balance2.ar(signals[0], signals[1], balance);
}.play;
)

Mix Down / Signal Summing

You can mix signals with a simple addition:

{ PinkNoise.ar(0.2) + SinOsc.ar(440, 0, 0.2) + Saw.ar(660, 0.2) }.play;

There is also the Mix class, which allows you to mix down an array of channels down to a single channel, or arrays of channels down to a single array of channels.

// one channel
{ Mix.new([SinOsc.ar(440, 0, 0.2), Saw.ar(660, 0.2)]).postln }.play;
 
// combine two stereo arrays
(
{
    var a, b;
    a = [SinOsc.ar(440, 0, 0.2), Saw.ar(662, 0.2)];
    b = [SinOsc.ar(442, 0, 0.2), Saw.ar(660, 0.2)];
    Mix([a, b]).postln;
}.play;
)

The Mix class also has another class method called fill, which is acting similiar to a loop. A Function will be evaluated n times, whereby n is the first argument to Mix.fill();.

// this will generate 8 sinetones, each with a unique random offset on the frequencie
// the amplitude is tamed by passing n to the mul argument
(
    var n = 8;
    { Mix.fill(n, { SinOsc.ar(500 + 500.0.rand, 0, 1 / n) }) }.play;
)

When you declare an argument in your function, you can make use of the n variable. N is counted up from 0 to n-1 and passed to the function.

// Look at the post window for frequencies and indices
(
    var n = 8;
    {
        Mix.fill(n, { arg index;
            var freq;
            index.postln;
            freq = 440 + index;
            freq.postln;
            SinOsc.ar(freq , 0, 1 / n)
        })
    }.play;
)

With Splay you can spread an array of signals evenly from left to right. It’s basically a downmix of a multichannel array to a stereo signal.

// Mix it down to stereo (spread evenly from left to right)
c = { Splay.ar(SinOsc.ar([100, 300, 500, 700, 900], mul: 0.1)) }.scope;
 
// Fun with Splay:
(
d = { arg fundamental = 110;
  var harmonics = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  var snd = BPF.ar(
    in: Saw.ar(32, LFPulse.ar(harmonics, width: 0.1)),
    freq: harmonics * fundamental,
    rq: 0.01,
    mul: 20);
  Splay.ar(snd);
}.play;
)
 
d.set(\fundamental, 50);
d.free;

Playing Soundfiles

Play a Buffer

For playing a Buffer with a Soundfile, you have to create a buffer on the server and read a soundfile in it. Both can be accomplished with Buffer.read(). After this you can play the buffer with the UGen PlayBuf.

// Load files into buffers:
~buf1 = Buffer.read(s, "/path/to/sndfl1.wav"); // one sound file
~buf2 = Buffer.read(s, "/path/to/sndfl2.wav"); // another sound file
 
// Playback:
{ PlayBuf.ar(1, ~buf1) }.play; // number of channels and buffer
{ PlayBuf.ar(1, ~buf2) }.play;
 
// Get some info about the files:
[~buf1.bufnum, ~buf1.numChannels, ~buf1.path, ~buf1.numFrames];
[~buf2.bufnum, ~buf2.numChannels, ~buf2.path, ~buf2.numFrames];
 
// Changing playback speed with 'rate'
{ PlayBuf.ar(numChannels: 1, bufnum: ~buf1, rate: 2, loop: 1) }.play;
{ PlayBuf.ar(1, ~buf1, 0.5, loop: 1) }.play; // play at half the speed
{ PlayBuf.ar(1, ~buf1, Line.kr(0.5, 2, 10), loop: 1) }.play; // speeding up
{ PlayBuf.ar(1, ~buf1, MouseY.kr(0.5, 3), loop: 1) }.play; // mouse control
 
// Changing direction (reverse)
{ PlayBuf.ar(1, ~buf2, −1, loop: 1) }.play; // reverse sound
{ PlayBuf.ar(1, ~buf2, −0.5, loop: 1) }.play; // play at half the speed AND
 

Envelopes and the Env Object

A simple Envelope in the shape of a line can be created with the Line Object. If you need something more then a envelope with two breakpoints in a linear distance you can work with the Env object, which can define breakpoint envelopes in several ways. You should also know, that these breakpoint envelopes are played back with the EnvGen UGen. Most of the time you don’t need this EnvGen, but it’s good to know, that under the hood there is something hidden.

Look at this:

// This:
{ SinOsc.ar * Env.perc.kr(doneAction: 2) }.play;
// ... is a shortcut for this:
{ SinOsc.ar * EnvGen.kr(Env.perc, doneAction: 2) }.play;

Env

Env allows the biggest flexibility of all envelope generators in SuperCollider. It takes an Array of N levels and needs a corresponding times Array of size N-1. The times values describe the distance in seconds between the levels values. With the key-value releaseNode you can introduce a sustain value. When the envelope reaches this value it will hold this value while gate is equal 1. For this you need to use the key-value gate in the EnvGen. When you set the gate to 0, the envelope will finish it’s process from the releaseNode. With the key-value loopNode you can introduce a loop cycle in cooperation with the releaseNode. The envelope will cycle from the releaseNode to the loopNode+1 in the time from loopNode to loopNode+1. Check the documentation for more info.

(
SynthDef(\envExample, {
  |freq = 80, gate = 1|
 
  var env = EnvGen.ar(
    Env(
      levels: [0, 1, 0.125, 1, 0.125, 0],
      times: [1, 0.5, 0.25, 0.1, 0.25],
      curve: \wel,
      releaseNode: 4,
      loopNode: 0,
    ).plot,
    gate: gate,
    doneAction: 2
  );
  var sig = SinOsc.ar(freq * env.range(1, 2), 0, 0.1);
  Out.ar(0, sig * env);
}).add;
)
 
~snd = Synth(\envExample);
~snd.set(\gate, 0);

You can always use the collect method for a generative approach for the levels, times and curves arrays.

Env.perc

Env.perc is a easy way to get a percussive envelope. It takes 4 arguments: attackTime, releaseTime, level and curve. You also need to tell Env.perc on which rate it should be executed. For this you append ar or kr to the Env.perc() and you can also add a doneAction: 2 to free your synth after the envelope did it’s work.

Env.perc.plot; // using all default args
Env.perc(0.5).plot; // attackTime: 0.5
Env.perc(attackTime: 0.3, releaseTime: 2, level: 0.4).plot;
Env.perc(0.3, 2, 0.4, 0).plot; // same as above, but curve:0 means straight lines
 
{PinkNoise.ar(Env.perc.kr(doneAction: 2))}.play; // default Env.perc args
{PinkNoise.ar(Env.perc(0.5).kr(doneAction: 2))}.play;
{PinkNoise.ar(Env.perc(0.3, 2, 0.4).ar(2))}.play;
{PinkNoise.ar(Env.perc(0.3, 2, 0.4, 0).kr(2))}.play;

Env.triangle

The name explains itself. It only takes two arguments: duration, and level.

// See it:
Env.triangle.plot;
// Hear it:
{SinOsc.ar([440, 442], mul: Env.triangle.kr(2))}.play;
// By the way, an envelope can be a multiplier anywhere in your code
{SinOsc.ar([440, 442]) * Env.triangle.kr(2)}.play;

Env.linen

Env.linen is a trapezoid formed like envelope, but it’s attack and release ramp can also be shaped by it’s curve argument. You can specify attackTime, sustainTime, releaseTime, sustainLeve, and curve.

// See it:
Env.linen(1).plot;
// Hear it:
{SinOsc.ar([300, 350], mul: Env.linen(0.01, 2, 1, 0.2).kr(2))}.play;

Env.pairs

With Env.pairs you can describe any envelope you wish. It takes breakpoint pairs as arguments, which consists of [time, level], and a type of curve.

(
{
  var env = Env.pairs([[0, 0], [0.4, 1], [1, 0.2], [1.1, 0.5], [2, 0]], \lin);
  env.plot;
  SinOsc.ar([440, 442], mul: env.kr(2));
}.play;
)

ADSR and ASR Envelopes

Sometimes you need a envelope with a undefined duration, which is deppending on the duration of a other event, like pressing a key on a keyboard. For this you can use the Env.adsr and Env.asr envelopes. They expect a gate value, which triggers (1) the envelope a and also sets them on it’s release phase (0).

Check this example:

// ASR
// Play note ('press key')
// attackTime: 0.5 seconds, sustainLevel: 0.8, releaseTime: 3 seconds
x = {arg gate = 1, freq = 440; SinOsc.ar(freq: freq, mul: Env.asr(0.5, 0.8, 3).
  kr(doneAction: 2, gate: gate))}.play;
// Stop note ('finger off the key' − activate release stage)
x.set(\gate, 0); // alternatively, x.release
 
// ADSR (attack, decay, sustain, release)
// Play note:
(
d = { arg gate = 1;
  var snd, env;
  env = Env.adsr(0.01, 0.4, 0.7, 2);
  snd = Splay.ar(BPF.ar(Saw.ar((32.1, 32.2..33)), LFNoise2.kr(12).range(100, 1000), 0.05, 10));
  Out.ar(0, snd * env.kr(doneAction: 2, gate: gate));
}.play;
)
// Stop
d.release; // this is equivalent to d.set(\gate, 0);