Overview

My notes on learning and remembering SuperCollider.

Content

SuperCollider - a short overview

SuperCollider is a object oriented programming language for soundsynthesis and manipulation. The SuperCollider software actually consists of three programs:

  • the text editor or IDE
  • the language (sclang) or interpreter, or client
  • and the server (scsynth), where actually the sound is produced and calculated

The IDE is communicating with the server, which is running as UNIX programm in the background. The user is writing messages in the supercollider language and sending these via the IDE over OSC to the server. These are then interpreted as synthesis modules and programms.

IDE

post window

  • like a console
  • the place where informations and errors are printed

keyboard commands

  • shift+return - evaluate selection or line
  • cmd+return - evaluate selection, line or region
  • cmd+b - boot server
  • cmd+. - free all synths on server
  • cmd+d - while having the cursor on a class, this opens the manual entry for this class
  • cmd+shift+d - look up for documentation
  • cmd+ß - show / hide help browser
  • cmd+a - select all
  • cmd+shift+p - clear the post window
  • cmd+shift+space - show tooltips for current method
  • cmd+m - show the meter window

the helpfull section

  • s.volume.gui; - creates a graphical slider to control the output volume
  • s.makeWindow; - creates a sever window, which allows booting, recording, and volume control

The Server

The server is a Programm, that runs in the Background of your computer. Every sound of SuperCollider is made on the server. The server is the sound engine, so to speak. To create sounds you need to start / boot the server. After this you can send messages made up of the sclang from your IDE to the server.

Your localhost server has the reserved chracter s. The server is also an object and you can boot and quit your server with these methods:

s.boot;
s.quit;

Change the SR and Blocksize

(
Server.local.options.sampleRate = 48000;
// sample rate can be checked in post window
 
Server.local.options.blockSize = 16;
// block size can be checked via
// s.options.blockSize; after boot
s.boot;
)
 
// check the blocksize
s.options.blockSize;
// end your test
s.quit

Choose Input and Output Device

SuperCollider can show you the avaiable input and output devices without starting the server with these commands:

ServerOptions.devices; // all devices
ServerOptions.inDevices; // input devices
ServerOptions.outDevices; // output devices

After this you can choose the devices with these commands:

Server.default.options.inDevice_("Built-in Microph");
Server.default.options.outDevice_("Built-in Output");
// for in and ouput
Server.default.options.device_("MOTU 828");

UGen Graphs

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).

SuperCollider - sclang

SuperCollider - Basics of making a Sound

SuperCollider - SynthDefs and Synths

Busses

Busses are used to route signals from one place to another — either between Synths or to/from the audio hardware. There are two types of busses in SuperCollider: audio busses and control busses.

Control busses are used for sending control-rate signals. By default, their indices start at 0 and go up to 4095, but this number can be changed in the server options before booting.

Audio busses are used for audio-rate signals and are divided into three categories:

  • audio outputs
  • audio inputs
  • private (internal) audio busses

For example, imagine your audio interface has 4 outputs and 2 inputs. Then:

  • Audio bus indices 0 to 3 are reserved for the outputs,
  • indices 4 and 5 for the inputs,
  • and the remaining busses (index 6 to 127) are available as private audio busses, used for internal routing between Synths. These are not connected to physical I/O by default.

Writing to or Reading from Busses

Via the Out UGen, you can write signals to a bus. The rate method (ar or kr) determines whether the signal is written to an audio or a control bus.

The first argument specifies the bus index. The second argument must be a UGen or an Array of UGens. If you pass an array, SuperCollider will perform multichannel expansion: the first element will be written to the specified bus, the second to the next bus, and so on. Each array element is mapped to a contiguous channel starting from the given bus index.

// write a control signal to the control signal bus '0'
{ Out.kr(0, SinOsc.kr) }.play
 
// write an audio signal to the audio signal bus '3'
{ Out.ar(1, SinOsc.ar) }.play
 
// write an array of audio signals to the audio signal busses '4' to '6'
{ Out.ar(4, SinOsc.ar([220, 225, 227])) }.play

An audio-rate signal can be downsampled to control rate by writing it to a Out.kr UGen. This effectively converts the signal to control rate.

In some cases, it is also possible to upsample a control-rate signal to audio rate via interpolation. However, not all UGens support this kind of automatic rate conversion.

// This throws an error. Can't write a control rate signal to an audio rate bus
{ |out| Out.ar(out, SinOsc.kr) }.play;
 
// This will work as the audio rate signal is downsampled to control rate
{ |out| Out.kr(out, SinOsc.ar) }.scope;

When multiple synths write their signals to the same bus, the signals will be summed and downmixed.

(
SynthDef("tutorial-args", { arg freq = 440, out = 0;
    Out.ar(out, SinOsc.ar(freq, 0, 0.2));
}).add;
)
// both write to bus 1, and their output is mixed
x = Synth("tutorial-args", ["out", 1, "freq", 660]);
y = Synth("tutorial-args", ["out", 1, "freq", 770]);

To read from a bus, you can use the In UGen. Its first argument is the bus index, and the second argument specifies the number of channels to read. If the second argument is greater than one, the result will be an array of audio signals.

In returns an OutputProxy, which acts as a placeholder for future audio inputs and supports multichannel expansion.

In.ar(0, 1); // this will return 'an OutputProxy'
In.ar(0, 4); // this will return an Array of 4 OutputProxies
  • Example with a Control Bus

    You can always scale one control signal to several ranges. Here is an example how to use a Control Bus to control different paramters.

    ~myControl = Bus.control(s, 1);
     
    c = { Out.kr(~myControl, Pulse.kr(freq: MouseX.kr(1, 10), mul: MouseY.kr(0, 1)))}.play;
     
    (
    {
      Blip.ar(
        freq: LFNoise0.kr([1/2, 1/3]).range(50, 60),
        numharm: In.kr(~myControl).range(10, 100),
        mul: LFTri.kr([1/4, 1/6]).range(0, 0.1)
      )
    }.play;
     
    {
      Splay.ar(
        Pulse.ar(
          freq: LFNoise0.kr([1.4, 1, 1/2, 1/3]).range(100, 1000) * In.kr(~myControl).range(0.9, 1.1),
          mul: SinOsc.ar([1/3, 1/2, 1/4, 1/8]).range(0, 0.03)
        )
      )
    }.play;
    )
     
    c.free;
  • asMap

    You can also assign a busses output a a synth node, even wehen synth does not contain an In arg.

    // create a SynthDef
    SynthDef(\simple, {arg freq = 440; Out.ar(0, SinOsc.ar(freq, mul: 0.2))}).add;
     
    // create control buses
    ~oneBus = Bus.control(s, 1);
    ~anotherBus = Bus.control(s, 1);
     
    // start controls
    { Out.kr(~oneBus, LFSaw.kr(1).range(100, 1000)) }.play;
    { Out.kr(~anotherBus, LFSaw.kr(2, mul: -1).range(500, 2000)) }.play;
     
    // start a note
    x = Synth(\simple, [\freq, 800]);
    x.set(\freq, ~oneBus.asMap);
    x.set(\freq, ~anotherBus.asMap);
    x.free;

Microphone Input

Microphone Input (or any input channel of your audio interface) can be received via the SoundIn UGen. This is only different from the In.ar in the way, it is counting channels. SoundIn starts indexing the input channels with 0 - In.ar() is counting the overall channels.

// Warning: use headphones to avoid feedback
{SoundIn.ar(0)}.play; // same as In.ar(8): takes sound from the first input bus
 
// Stereo version
{SoundIn.ar([0, 1])}.play; // first and second inputs
 
// Some reverb just for fun?
{FreeVerb.ar(SoundIn.ar([0, 1]), mix: 0.5, room: 0.9)}.play;

Private Busses

Private busses are used for internal communication between Synths within the SuperCollider server. They allow you to send audio or control signals from one Synth to another without interfering with hardware input or output busses.

You can create private busses and reserve bus channels for them using the Bus class. This class handles dynamic allocation of available bus indices.

For example, suppose you are working on a system with 2 output channels and 1 input channel. In this case, the first two audio busses (indices 0 and 1) are reserved for audio output, and the next one (index 2) for audio input. When you create a private audio bus using b = Bus.audio(s);, SuperCollider will automatically allocate the next free bus index (e.g., index 3).

If you later change your hardware configuration (e.g., adding more input or output channels), the Bus class ensures that your private busses still refer to valid, non-overlapping indices. This makes your code more portable and robust.

The Bus class has also some more convenient features.

s.reboot; // this will restart the server and thus reset the bus allocators
 
b = Bus.control(s, 2);    // a 2 channel control Bus
b.index;         // this should be zero
b.numChannels         // Bus also has a numChannels method
c = Bus.control(s);
c.numChannels;        // the default number of channels is 1
c.index;        // note that this is 2; b uses 0 and 1
  • Some Examples with Private Busses

    Check carefully to understand these.

    (
    SynthDef("tutorial-Infreq", { arg bus, freqOffset = 0, out;
        // this will add freqOffset to whatever is read in from the bus
        Out.ar(out, SinOsc.ar(In.kr(bus) + freqOffset, 0, 0.5));
    }).add;
     
    SynthDef("tutorial-Outfreq", { arg freq = 400, bus;
        Out.kr(bus, SinOsc.kr(1, 0, freq/40, freq));
    }).add;
     
    b = Bus.control(s,1);
    )
     
    (
    x = Synth.new("tutorial-Outfreq", [\bus, b]);
    y = Synth.after(x, "tutorial-Infreq", [\bus, b]);
    z = Synth.after(x, "tutorial-Infreq", [\bus, b, \freqOffset, 1000]);
    )
    x.free; y.free; z.free; b.free;

    This examples shows how to work with effect busses.

    (
    // the arg direct will control the proportion of direct to processed signal
    SynthDef("tutorial-DecayPink", { arg outBus = 0, effectBus, direct = 0.5;
        var source;
        // Decaying pulses of PinkNoise. We'll add reverb later.
        source = Decay2.ar(Impulse.ar(1, 0.25), 0.01, 0.2, PinkNoise.ar);
        // this will be our main output
        Out.ar(outBus, source * direct);
        // this will be our effects output
        Out.ar(effectBus, source * (1 - direct));
    }).add;
     
    SynthDef("tutorial-DecaySin", { arg outBus = 0, effectBus, direct = 0.5;
        var source;
        // Decaying pulses of a modulating sine wave. We'll add reverb later.
        source = Decay2.ar(Impulse.ar(0.3, 0.25), 0.3, 1, SinOsc.ar(SinOsc.kr(0.2, 0, 110, 440)));
        // this will be our main output
        Out.ar(outBus, source * direct);
        // this will be our effects output
        Out.ar(effectBus, source * (1 - direct));
    }).add;
     
    SynthDef("tutorial-Reverb", { arg outBus = 0, inBus;
        var input;
        input = In.ar(inBus, 1);
     
        // a low-rent reverb
        // aNumber.do will evaluate its function argument a corresponding number of times
        // {}.dup(n) will evaluate the function n times, and return an Array of the results
        // The default for n is 2, so this makes a stereo reverb
        16.do({ input = AllpassC.ar(input, 0.04, { Rand(0.001,0.04) }.dup, 3)});
     
        Out.ar(outBus, input);
    }).add;
     
    b = Bus.audio(s,1); // this will be our effects bus
    )
     
    (
    x = Synth.new("tutorial-Reverb", [\inBus, b]);
    y = Synth.before(x, "tutorial-DecayPink", [\effectBus, b]);
    z = Synth.before(x, "tutorial-DecaySin", [\effectBus, b, \outBus, 1]);
    )
     
    // Change the balance of wet to dry
    y.set(\direct, 1); // only direct PinkNoise
    z.set(\direct, 1); // only direct Sine wave
    y.set(\direct, 0); // only reverberated PinkNoise
    z.set(\direct, 0); // only reverberated Sine wave
    x.free; y.free; z.free; b.free;

    Here is another example how to set and unset control busses. Control busses hold their last value until something new is written.

    (
    // make two control rate busses and set their values to 880 and 884.
    b = Bus.control(s, 1); b.set(880);
    c = Bus.control(s, 1); c.set(884);
    // and make a synth with two frequency arguments
    x = SynthDef("tutorial-map", { arg freq1 = 440, freq2 = 440, out;
        Out.ar(out, SinOsc.ar([freq1, freq2], 0, 0.1));
    }).play(s);
    )
    // Now map freq1 and freq2 to read from the two busses
    x.map(\freq1, b, \freq2, c);
     
    // Now make a Synth to write to the one of the busses
    y = {Out.kr(b, SinOsc.kr(1, 0, 50, 880))}.play(addAction: \addToHead);
     
    // free y, and b holds its last value
    y.free;
     
    // use Bus-get to see what the value is. Watch the post window
    b.get({ arg val; val.postln; f = val; });
     
    // set the freq2, this 'unmaps' it from c
    x.set(\freq2, f / 2);
     
    // freq2 is no longer mapped, so setting c to a different value has no effect
    c.set(200);
     
    x.free; b.free; c.free;

    Look carefully at this line: b.get({ arg val; val.postln; f = val; }); This line has a callback function, which is executed, when the serve has delievered the value, that is called by b.get(). Since the server has some latency, it is important to understand, that this

    (
    f = nil; // just to be sure
    b.get({ arg val; f = val; });
    f.postln;
    )

    would not work.

Groups and the Node Tree

The Node Tree

Synth instances on the SuperCollider server are executed in a specific order, determined by their position in the node tree. Every Synth themself is a node. This order affects how and when audio or control signals are written to and read from buses. s.plotTree; creates a graphical representation of all you synth nodes.

For example: if one Synth reads from a bus, and another Synth writes to it, the writer must be placed before the reader in the execution order. Otherwise, the reader might access stale or uninitialized data. The most recent synths get added to the top by default.

To control the execution order, you can use methods like Synth.new with the optional addAction argument, or you can explicitly place a Synth after another with Synth.after.

These methods allow precise control over when each Synth is executed relative to others, which is essential when working with buses.

Synth-new has two arguments which allow you to specify where in the order a synth is added. The first is a target, and the second is an addAction. The latter specifies the new synth’s position in relation to the target. There is \addAfter and \addBefore, and the (rarely) used \addReplace.

x = Synth("default", [\freq, 300]);
// add a second synth immediately after x
y = Synth("default", [\freq, 450], x, \addAfter);
x.free; y.free;

Methods like Synth-after are simply convenient ways of doing the same thing, the difference being that they take a target as their first argument.

// These two lines of code are equivalent
y = Synth.new("default", [\freq, 450], x, \addAfter);
y = Synth.after(x, "default", [\freq, 450]);

To remove a synth from the server you have a few options:

  • synth.free - like in the examples before
  • Cmd+. - to kill all synths on the server
  • doneAction: 2 - like with UGens as Line. This frees the synth after Line done it’s job

Here is an example with Line and it’s doneAction: 2. Watch the node tree with these two.

// without doneAction: 2
{WhiteNoise.ar(Line.kr(0.2, 0, 2))}.play;
 
// with doneAction: 2
{WhiteNoise.ar(Line.kr(0.2, 0, 2, doneAction: 2))}.play;

Groups as Collection of Nodes

Groups are, alongside Synths, the other type of Nodes in SuperCollider. A Group is a collection of Nodes and can contain Synths, other Groups, or both.

Groups are useful in two main ways:

  • They help to control the execution order of nodes on the server.
  • They allow you to group nodes together and send them messages collectively (e.g. free, set, or move).

Groups are represented by the class Group, which acts as a server-side abstraction for managing node hierarchies.

Creating a Group is done with these commands.

g = Group.new;
h = Group.before(g);
g.free; h.free;

This can be very helpful for things like keeping effects or processing separate from sound sources, and in the right order.

Here is an example.

(
// a stereo version
SynthDef(\tutorial_DecaySin2, { arg outBus = 0, effectBus, direct = 0.5, freq = 440;
    var source;
    // 1.0.rand2 returns a random number from -1 to 1, used here for a random pan
    source = Pan2.ar(Decay2.ar(Impulse.ar(Rand(0.3, 1), 0, 0.125), 0.3, 1,
        SinOsc.ar(SinOsc.kr(0.2, 0, 110, freq))), Rand(-1.0, 1.0));
    Out.ar(outBus, source * direct);
    Out.ar(effectBus, source * (1 - direct));
}).add;
 
SynthDef(\tutorial_Reverb2, { arg outBus = 0, inBus;
    var input;
    input = In.ar(inBus, 2);
    16.do({ input = AllpassC.ar(input, 0.04, Rand(0.001,0.04), 3)});
    Out.ar(outBus, input);
}).add;
)
 
// now we create groups for effects and synths
(
~sources = Group.new;
~effects = Group.after(~sources);     // make sure it's after
~bus = Bus.audio(s, 2);         // this will be our stereo effects bus
)
 
// now synths in the groups. The default addAction is \addToHead
(
x = Synth(\tutorial_Reverb2, [\inBus, ~bus], ~effects);
y = Synth(\tutorial_DecaySin2, [\effectBus, ~bus, \outBus, 0], ~sources);
z = Synth(\tutorial_DecaySin2, [\effectBus, ~bus, \outBus, 0, \freq, 660], ~sources);
)
 
// we could add other source and effects synths here
 
~sources.free; ~effects.free; // this frees their contents (x, y, z) as well
~bus.free;
 
// remove references to ~sources and ~effects environment variables:
currentEnvironment.clear;

There are a few add-actions to order you Groups.

g = Group.new;
h = Group.head(g);        // add h to the head of g
x = Synth.tail(h, \default);    // add x to the tail of h
s.queryAllNodes;        // this will post a representation of the node hierarchy
x.free; h.free; g.free;
  • Send Messages to Groups

    Groups make it convenient to send messages to all synths inside of one group

    g = Group.new;
     
    // make 4 synths in g
    // 1.0.rand2 returns a random number from -1 to 1.
    4.do({ { arg amp = 0.1; Pan2.ar(SinOsc.ar(440 + 110.rand, 0, amp), 1.0.rand2) }.play(g); });
     
    g.set(\amp, 0.005); // turn them all down
     
    g.free;

Node Hierachy

Server has a method called queryAllNodes which will post a representation of the server’s node tree with the corresponding ID’s of the nodes.

When a server app is booted there is a special group created with a node ID of 0. This represents the top of the server’s node tree. There is a special server abstraction object to represent this, called RootNode. In addition there is another group created with an ID of 1, called the default group. This is the default target for all Nodes and is what you will get if you supply a Server as a target. If you don’t specify a target or pass in nil, you will get the default group of the default Server.

s.boot;
a = Synth.new(\default); // creates a synth in the default group of the default Server
a.group; // Returns a Group object. Note the ID of 1 (the default group) in the post window

The default group serves an important purpose: It provides a predictable basic Node tree so that methods such as Server-scope and Server-record (which create nodes which must come after everything else) can function without running into order of execution problems. In the example below the scoping node will come after the default group.

{ SinOsc.ar(mul: 0.2) }.scope(1);
 
// watch the post window;
s.queryAllNodes;
 
// our SinOsc synth is within the default group (ID 1)
// the scope node ('stethoscope') comes after the default group, so no problems

In general you should add nodes to the default group, or groups contained within it, and not before or after it. When adding an ‘effects’ synth, for instance, one should resist the temptation to add it after the default group, and instead create a separate source group within the default group. This will prevent problems with scoping or recording.

Example of organizing Nodes and Groups

This is a example of how you could deal with audio busses and nodes in groups.

// keep watching everything in the NodeTree
s.plotTree;
 
// create some buses
~reverbBus = Bus.audio(s, 2);
~masterBus = Bus.audio(s, 2);
 
// define groups
(
~sources = Group.new;
~effects = Group.new(~sources, \addAfter);
~master = Group.new(~effects, \addAfter);
)
 
// run all synths at once
(
// one source sound
{
  Out.ar(~reverbBus, SinOsc.ar([800, 890]) * LFPulse.ar(2) * 0.1)
}.play(target: ~sources);
// another source sound
{
  Out.ar(~reverbBus, WhiteNoise.ar(LFPulse.ar(2, 1/2, width: 0.05) * 0.1))
}.play(target: ~sources);
 
// some reverb
{
  Out.ar(~masterBus, FreeVerb.ar(In.ar(~reverbBus, 2), mix: 0.5, room: 0.9))
}.play(target: ~effects);
 
// some silly master volume control with mouse
{
  Out.ar(0, In.ar(~masterBus, 2) * MouseY.kr(0, 1))
}.play(target: ~master);
)

SuperCollider - Buffers, Sampling, Waveshaping

Supercollider - Patterns and the Event Class

SuperCollider - Events, Routines, Tasks and Sequencing on Clocks

Multichannel Output

Fixed Outputs

When your Function returns an Array of UGens, each slot of the array correspons to an outputchannel (starting from 0 = physical outputchannel 1).

// stereo example
{ [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)] }.play;
 
// quad example
{ [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2), SinOsc.ar(438, 0, 0.2), SinOsc.ar(435, 0, 0.2)] }.play;

When you need a specific outputchannel you can use the Out.ar UGen, which expects the outputchannel as frist argument.

{
    Out.ar(3, SinOsc.ar(440, 0, 0.2));  // Channel 3 (4. physical output)
    Out.ar(7, SinOsc.ar(442, 0, 0.2));  // Channel 7 (8. physical output)
}.play;

Out.ar expects as frist argument the outputchannel number and as second argument a UGen or an array of UGens. If you provide an array, multichannel expansion is happening. That means, that the first argument defines the outputchannel for the first object of the argument and the following UGens of the array are played on following consecutive channels.

Plotting and Scoping

Function has two methods for plotting and scoping the audio output: plot and scope.

This makes a graph of the signal produced by the output of the Function. You can specify some arguments, such as the duration. The default is 0.01 seconds, but you can set it to anything you want.

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

This can be usefull for checking, if you get the output, you are expecting to get.

With scope you can get an oscilloscope-like display of the funtion’s output.

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

You can also get the server output as oscilloscope output.

{ [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)] }.play;
s.scope;

Recording

Here’s a quick way to record sound:

// QUICK RECORD
// start recording:
s.record;
 
// make some sounds
 
// stop recording:
s.stropRecording;
 
// optional: GUI with record button, volume control, mute button:
s.makeWindow;

Synthesis

Additiv Synthesis

var n = 28; { Mix.fill(n, { arg index; var freq; freq = 44 * (index + 1); freq.postln; SinOsc.ar(freq , 0, 1 / n) }) }.play; )

SuperCollider - MIDI

SuperCollider - OSC

Supercollider - Quarks

My Curriculum

What i need to learn:

  • SuperCollider Syntax
  • SuperCollider Programm Architecture
    • Abstractions
  • Controlling Synth Nodes
  • Controll Structures
  • Rendering
  • MIDI Messaging
  • OSC Messaging
  • Additiv Synthesis
  • Subtractive Synthesis
    • also with samples
    • also with live input
    • table reading
    • function tables?
  • FM Synthesis
    • complex signal flows and controll structures
  • Karplus Strong
  • FFT
  • Granular Synthesis