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

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");

The Language

Some Basics

A valid statement

To end a valid statement you have to use a semicolon ;.

Comments

In SuperCollider line comments start with a double slash //. A comment block is made of text in between slashes and asterisks /* comment text */.

Precedence

SuperCollider does not know any arithmetic precedence rules. Operations are executed from left to right. You have to use parenthesis to encapsulate your operations in the right way. When combining messages and binary operations, messages take precedence.

Evaluation

The last evaluated statement get’s allways postet in the Post window. This is true to single line statements and also code blocks.

Code Blocks

To build semantic code blocks, that should evaluate together, you can enclose these in parenthesis.

Variables

Variables in SuperCollider

In SuperCollider, we differentiate between local and global variables. Every object can be assigned to a variable.

The single-letter variables from a to z are already declared as global variables. This means they are accessible throughout your whole SuperCollider program. Some of them are already assigned to specific objects — for example, the variable s refers to the default local server.

If you want to use more descriptive names for global variables, you can define them by prefixing the name with a tilde ( ~ ), like ~myBuffer. They are also called envrionment variables.

Local variables, on the other hand, are only visible within a local scope. A local scope could be a SynthDef, a Function, or a code block. You declare local variables using the keyword var.

// Environment variables / global variables
~galaApples = 4;
~bloodOranges = 5;
~limes = 2;
~plantains = 1;
 
["Citrus", ~bloodOranges + ~limes];
["Non−citrus", ~plantains + ~galaApples];
 
// Local variables: valid only within the code block.
// Evaluate the block once and watch the Post window:
(
var apples = 4, oranges = 3, lemons = 8, bananas = 10;
["Citrus fruits", oranges + lemons].postln;
["Non−citrus fruits", bananas + apples].postln;
"End".postln;
)
 
~galaApples; // still exists
apples; // gone

Variables can always be reassigned.

Classes, Objects and Messages

SuperCollider is an object-oriented programming language. To perform an action on an object, you send a message to it. This message invokes a method that defines how the object should respond.

For example, if you want to print a string (which is also an object) to the post window, you send it the postln message by appending .postln to the string. The line is terminated with a semicolon:

"Hello World!".postln;

An object is an instance of a class. A class could be something like Student. Every student has unique properties such as a name, an age, a gender, or a favorite subject. For example, Jane would be an instance of the Student class with her own specific values for these properties.

In SuperCollider, everything is an object: an integer number, a UGen, a string, or an array. That means everything can receive messages and has methods.

Classes can have subclasses, which inherit properties and methods from their superclass. For example, the Float and Integer classes are child classes of the Number class, which in turn is a child of the Object class. Classes always starts with an uppercase letter.

Each class has its own methods that can act on its data. These methods are also inherited by their child classes. Because of this, sometimes you can’t find information on some methods of subclasses in the manual. These are then described in their superclasses. Here are some examples how to find out about realtionships and methods.

Group.superclass;                 // this will return 'Node'
Group.superclass.help;
Group.findRespondingMethodFor('set');        // Node-set
Group.findRespondingMethodFor('postln');    // Object-postln;
Group.helpFileForMethod('postln');         // opens class Object help file

Functions and Function-Objects

You can define functions using curly brackets. Everything enclosed in the brackets becomes a Function object. This object is an instance of SuperCollider’s Function class, which has several methods implemented. These methods can be invoked by sending messages to the Function object — for example, the play method:

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

By assigning your function to an instance variable, you can reuse it and send messages to the variable:

f = { "Function evaluated".postln };
f.value;

When you call the value method on a function it will evaluate and return the result of its last line of code:

(
f = {
    "Evaluating...".postln;
    2 + 3
};
f.value;
)
// this will return '5'

Functions can also have arguments. These are values which are passed into the function when it is evaluated:

(
f = { arg a, b;
    a - b
};
f.value(5, 3);
)

You can pass arguments also via keyword arguments:

(
f = { arg a, b;
    a / b
};
f.value(10, 2);         // regular style
f.value(b: 2, a: 10);   // keyword style
)

And also mix regular and keyword style:

(
f = { arg a, b, c, d;
    (a + b) * c - d
};
f.value(2, c:3, b:4, d: 1); // (2 + 4) * 3 - 1
)

You can also set default values for arguments:

(
f = { arg a, b = 2;
    a + b
};
f.value(2);        // 2 + 2
)

And also use pipes to specify arguments:

(
f = { arg a, b;
    a + b
};
 
g = { |a, b|
    a + b
};
 
f.value(2, 2);
g.value(2, 2);
)

You can also use variables inside of functions. These are local to the function scope:

(
f = { arg a, b;
    var firstResult, finalResult;  // declare the variables via 'var' keyword
    firstResult = a + b;
    finalResult = firstResult * 2;
    finalResult
};
f.value(2, 3);    // this will return (2 + 3) * 2 = 10
)
 
(
g = {
    var foo;
    foo = 3;
    foo
};
g.value;
foo;            // this will cause an error as "foo" is only valid within f.
)

You can also declare variables outside of functions. These are then local to the scope of the block (defined by parenthesis):

(
var myFunc;
myFunc = { |input| input.postln };
myFunc.value("foo");    // arg is a String
myFunc.value("bar");
)
 
myFunc;            // throws an error

The letters a to z are what are called interpreter variables. These are pre-declared when you start up SC, and have an unlimited, or “global”, scope. This makes them useful for quick tests or examples.

Objects in SuperCollider (and many other object-oriented languages) are polymorphic. This means that different objects can receive the same message but may respond differently, because they implement different methods for that message. So when you send the same message to different objects, you may get different results.

An example for polymorphism (check carefully to get it completly):

(
f = { arg a;
  a.value + 3        // call "value" on a; polymorphism awaits!
};
)
f.value(3);          // a.value is 3, so this returns 3 + 3 = 6
g = { 3.0.rand };
f.value(g);          // here the arg is a Function. a.value evaluates 3.0.rand
f.value(g);          // try it again, different result

Syntax Shortcuts

There a few shorthand forms or alternate syntaxes for doing things withing SuperCollider. E.g. :

// this
{ Mix.new([SinOsc.ar(440, 0, 0.2), Saw.ar(660, 0.2)]).postln }.play;
// and this
{ Mix([SinOsc.ar(440, 0, 0.2), Saw.ar(660, 0.2)]).postln }.play;
// are equivalent

You can also switch between Functional and receiver notation:

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

Basics of making a Sound with 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;
)

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 decalre an argument in your function, you can make use of the n variable. N i 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;
)

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 (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. Review 04. Functions and Other Functionality for more about function arguments.

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;

Environment Variables

Placing a ~ in front of a variable creates an environment variable. These variables do not need to be explicitly declared and persist within the current environment. You can think of them as global variables within your current workspace.

In most cases, you work within a single environment. However, there are situations where using multiple environments becomes useful — for example, when encapsulating state or isolating parts of a program.

(
~sources = Group.new;
~effects = Group.after(~sources);
~bus = Bus.audio(s, 2);
)
// to be sure, create a new Environment:
Environment.new.push;
 
// some code..
 
// restore old environment
currentEnvironment.pop;

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 (starting at index 6) 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

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.

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.

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]);

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.

Buffers

Buffers represent buffers on the sever, which are ordered arrays of floating point numbers. They can be single or multichannel buffers and are the usual way to store data on the server. Any data can be represented (Soundfiles, Waveforms, etc.).

Buffers, like Busses, are numbered, starting at 0. Using the Buffer class takes care of allocating numbers, and avoids conflicts. Before using a Buffer you need to allocate memory for this, which is an asynchronous process. The size of Buffers is meassured in frames.

A server’s buffers are global, which is to say that they can be accessed by any synth, and by more than one at a time. They can be written to or even changed in size, while they are being read from.

Making a Buffer Object and Allocating Memory

Making a Buffer object and allocating the necessary memory in the server app is quite easy. You can do it all in one step with Buffer’s alloc method:

s.boot;
b = Buffer.alloc(s, 100, 2);    // allocate 2 channels, and 100 frames
b.free;                // free the memory (when you're finished using it)

The acutal size of the Buffer is the number of channels multiplied with the number of frames.

If you’d like to allocate in terms of seconds, rather than frames, you can do so like this:

b = Buffer.alloc(s, s.sampleRate * 8.0, 2); // an 8 second stereo buffer
b.free;

Buffer’s ‘free’ method frees the memory on the server, and returns the Buffer’s number for reallocation. You should not use a Buffer object after doing this.

Using Buffers with Sound Files

Buffer has another class method called ‘read’, which reads a sound file from disk into memory, and returns a Buffer object. Using the UGen PlayBuf, we can play the file.

// read a soundfile
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
 
// now play it
(
x = SynthDef("tutorial-PlayBuf",{ arg out = 0, bufnum;
    Out.ar( out,
        PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum))
    )
}).play(s,[\bufnum, b]);
)
x.free; b.free;

PlayBuf.ar has a number of arguments which allow you to control various aspects of how it works.

PlayBuf.ar(
    1,                // number of channels
    bufnum,             // number of buffer to play
    BufRateScale.kr(bufnum)        // rate of playback
    )

Streaming a File in From Disk

In some cases, for instance when working with very large files, you might not want to load a sound completely into memory. Instead, you can stream it in from disk a bit at a time, using the UGen DiskIn, and Buffer’s ‘cueSoundFile’ method:

(
SynthDef("tutorial-Buffer-cue",{ arg out=0,bufnum;
    Out.ar(out,
        DiskIn.ar( 1, bufnum )
    )
}).add;
)
 
b = Buffer.cueSoundFile(s,Platform.resourceDir +/+ "sounds/a11wlk01-44_1.aiff", 0, 1);
y = Synth.new("tutorial-Buffer-cue", [\bufnum,b], s);
 
b.free; y.free;

This is not as flexible as PlayBuf (no rate control), but can save memory.

Getter Methods on Buffer Objects

When you save an object — for example, a Buffer — to an instance variable, you gain access to several getter and setter methods. In particular, the getter methods for Buffer instances are very useful, because you often don’t have all information about a sound file available at the time of loading.

// watch the post window
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
b.bufnum;
b.numFrames;
b.numChannels;
b.sampleRate;
b.free;

Since there is a latency between loading something into a buffer on the server and being able to use it on the client side, many Buffer methods accept an action function as an argument. This function is executed once the client receives confirmation from the server that the buffer is ready.

// with an action function
// note that the vars are not immediately up-to-date
(
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav", action: { arg buffer;
    ("numFrames after update:" + buffer.numFrames).postln;
    x = { PlayBuf.ar(1, buffer, BufRateScale.kr(buffer)) }.play;
});
 
// Note that the next line will execute BEFORE the action function
("numFrames before update:" + b.numFrames).postln;
)
x.free; b.free;

Recording into Buffers

In addition to PlayBuf, there’s a UGen called RecordBuf, which lets you record into a buffer.

b = Buffer.alloc(s, s.sampleRate * 5, 1); // a 5 second 1 channel Buffer
 
// record for four seconds
(
x = SynthDef("tutorial-RecordBuf",{ arg out=0,bufnum=0;
    var noise;
    noise = PinkNoise.ar(0.3);    // record some PinkNoise
    RecordBuf.ar(noise, bufnum);     // by default this loops
}).play(s,[\out, 0, \bufnum, b]);
)
 
// free the record synth after a few seconds
x.free;
 
// play it back
(
SynthDef("tutorial-playback",{ arg out=0,bufnum=0;
    var playbuf;
    playbuf = PlayBuf.ar(1,bufnum);
    FreeSelfWhenDone.kr(playbuf); // frees the synth when the PlayBuf has played through once
    Out.ar(out, playbuf);
}).play(s,[\out, 0, \bufnum, b]);
)
b.free;

Accessing Buffer Data

The Buffer class allows easy access to the buffer values for writing or reading. Buffer-set just takes an index and a value to write. Buffer-get takes an index and a action-function. Multichannel Buffer are interleaving their data, so for a two channel buffer index 0 = frame1-chan1, index 1 = frame1-chan2, index 2 = frame2-chan1, and so on.

b = Buffer.alloc(s, 8, 1);
b.set(7, 0.5);             // set the value at 7 to 0.5
b.get(7, {|msg| msg.postln});    // get the value at 7 and post it when the reply is received
b.free;

The methods ‘getn’ and ‘setn’ allow you to get and set ranges of adjacent values. ‘setn’ takes a starting index and an array of values to set, ‘getn’ takes a starting index, the number of values to get, and an action function.

b = Buffer.alloc(s,16);
b.setn(0, [1, 2, 3]);                // set the first 3 values
b.getn(0, 3, {|msg| msg.postln});        // get them
b.setn(0, Array.fill(b.numFrames, {1.0.rand}));    // fill the buffer with random values
b.getn(0, b.numFrames, {|msg| msg.postln});    // get them
b.free;

There is an upper limit on the number of values you can get or set at a time (usually 1633 when using UDP, the default). This is because of a limit on network packet size. To overcome this Buffer has two methods, ‘loadCollection’ and ‘loadToFloatArray’ which allow you to set or get large amounts of data by writing it to disk and then loading to client or server as appropriate.

(
// make some white noise
v = FloatArray.fill(44100, {1.0.rand2});
b = Buffer.alloc(s, 44100);
)
(
// load the FloatArray into b, then play it
b.loadCollection(v, action: {|buf|
    x = { PlayBuf.ar(buf.numChannels, buf, BufRateScale.kr(buf), loop: 1)
        * 0.2 }.play;
});
)
x.free;
 
// now get the FloatArray back, and compare it to v; this posts 'true'
// the args 0, -1 mean start from the beginning and load the whole buffer
b.loadToFloatArray(0, -1, {|floatArray| (floatArray == v).postln });
b.free;

A FloatArray is just a subclass of Array which can only contain floats.

Plotting and Playing

Buffer has two useful convenience methods: ‘plot’ and ‘play’.

// see the waveform
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
b.plot;
 
// play the contents
// this takes one arg: loop. If false (the default) the resulting synth is
// freed automatically
b.play;             // frees itself
x = b.play(true);        // loops so doesn't free
x.free; b.free;

Waveshaping

Buffers can also be used for holding a transferfunction for waveshaping. Here is an example with a chebyshev polynom.

b = Buffer.alloc(s, 512, 1);
b.cheby([1,0,1,1,0,1]);
(
x = play({
    Shaper.ar(
        b,
        SinOsc.ar(300, 0, Line.kr(0,1,6)),
        0.5
    )
});
)
x.free; b.free;

Patterns

Patterns in SuperCollider are similar to a musical score. They are used to create one or several event streams, which in turn trigger one or more SynthDef instances.

The class Pbind can combine multiple key-value patterns into a single event stream. A key-value pattern consists of a key, which refers to an argument of the SynthDef the values will be sent to, and a value, which is typically generated using a pattern class like Pseries or Pseq.

If no \instrument key is specified, Pbind will use the default SynthDef, which is usually named default.

A basic example:

(
Pbind(
  \degree, Pseq([-12, 3, 4, 5], inf),
  \dur, Pseq([0.2, 0.1, 0.1, 0.1, 0.5], inf),
  \legato, 0.5
).play;
)

The Event class defines a set of reserved keys that are interpreted in a special way, such as \freq, \amp, or \dur. Besides those, you can freely use any argument name that you’ve defined in your own SynthDef.

The pitch slot can be described with different key arguments, which all aspect a different pitch description:

  • \freq - value as frequencie
  • \degree - value as scale degree, with default scale major, while 0 is middle C
  • \note - value as note, while 0 is middle C
  • \midinote - value as midinote

There are a few helpfull Pattern Objects to create value patterns. Every Pattern Object class begins with a P. Here are a few examples:

  • Pseries - creates a artihmetic like series of values
  • Pseq - takes a list as input and cycles over it
  • Prand - picks random values from a list
  • Pwhite - creates random values between a lower and higher border
  • Pser - like Pseq but describes the total number of values you get when cycling over the list
  • Pxrand - like Prand but without repitition of values
  • Pshuf - shuffles the list and cycles over the shuffled list
  • Pslide - makes segments out of a list and steps through the segments
  • Pgeom - creates a series of values similiar to a geometric series
  • Pn - creates a Pattern out of the repetition of another Pattern

Chords with Patterns

You can write Chord Events via encapsulating them as a sublist in square brackets. It is also possible to to introduce the \srtum argument, which allows arpeggiating the chord.

(
Pbind(
  \note, Pseq([[0, 3, 7], [2, 5, 8], [3, 7, 10], [5, 8, 12]], 3),
  \dur, 0.15
).play;
)
 
(
Pbind(
  \note, Pseq([[-7, 3, 7, 10], [0, 3, 5, 8]], 2),
  \dur, 1,
  \legato, 0.4,
  \strum, 0.4
).play;
)

Scale

When you use \degree for your pitches, you can introduce the Scale class, for selecting a Scale.

(
Pbind(
  \scale, Scale.prometheus,
  \degree, Pseq([0, 1, 2, 3, 4, 5, 6, 7], 1),
  \dur, 0.15
).play;
)
 
// get a list of avaible scales:
Scale.directory;
 
// chromatics can be introduced with this:
(
Pbind(
  \degree, Pseq([0, 1, 2, 3, 3.1, 4], 1),
).play;
)

Transposition

You can transpose your pitches (not frequencies) with the \ctranspose keyword.

(
Pbind(
  \note, Pser([0, 2, 3, 5, 7, 8, 11, 12], 11),
  \ctranspose, 12, // transpose an octave above (= 12 semitones)
  \dur, 0.15;
).play;
)
 

Microtones

Tempo

Rests

Multiple Pbinds

Controlling Pbinds

Scheduling Events and Sequencing

Clocks

In SuperCollider, scheduling of events happens on clocks. Clocks keep track of the current time and determine when the next event should occur. An event is either a Function, a Routine or a Task.

There are several types of clocks:

  • SystemClock tracks the current running time of the SuperCollider program and is suitable for precise scheduling.
  • AppClock is similar to SystemClock, but it has a lower system priority. This makes it more appropriate for GUI updates, where exact timing is less critical.
  • TempoClock is used for musical sequencing. It supports tempo and meter, and is therefore ideal for time-based musical events.

Scheduling Events

Scheduling means telling a clock to execute something at a specific time in the future. So you need two things: a time and an object (e.g., a function) to execute.

Scheduling is an asynchronous action. This means the scheduling call is evaluated immediately (it returns something), but the scheduled event itself will happen later in time.

SystemClock.sched(5, { "hello".postln });

This returns SystemClock, but the scheduled event will happen in 5 seconds.

sched is a scheduling an event at a relative time to the function call. schedAbs can schedule an event to an absolute time.

(
var timeNow = TempoClock.default.beats;
"Time is now: ".post; timeNow.postln;
"Scheduling for: ".post; (timeNow + 5).postln;
TempoClock.default.schedAbs(timeNow + 5,
    { "Time is later: ".post; thisThread.clock.beats.postln; nil });
)

You can create as many TempoClocks as you need. If you use only one, it will usually fall back to the default clock. When working with multiple clocks, it makes sense to assign each of them to a variable.

Here another example with time as a ‘beats / second’ value.

(
var timeNow;
TempoClock.default.tempo = 2;    // 2 beats/sec, or 120 BPM
timeNow = TempoClock.default.beats;
"Time is now: ".post; timeNow.postln;
"Scheduling for: ".post; (timeNow + 5).postln;
TempoClock.default.schedAbs(timeNow + 5,
    { "Time is later: ".post; thisThread.clock.beats.postln; nil });
)

What time is it?

These methods help to get the current clock time.

SystemClock.beats;
TempoClock.default.beats;
AppClock.beats;
thisThread.clock.beats;

Endless Scheduling

When you schedule a function that returns a number, the clock will treat that number as the amount of time before running the function again. If you want the function to run only once, make sure to en the function with ‘nil’.

// fires many times (but looks like it should fire just once)
TempoClock.default.sched(1, { rrand(1, 3).postln; });
 
// fires once
TempoClock.default.sched(1, { rrand(1, 3).postln; nil });

Be aware of, tat scheduling events by a negative number or 0 you can get run into infinite loops or other problems.

Sequencing with Routines and Tasks

Routines

A Routine is a function-like class that can encapsulate a series of tasks, which are executed step by step. The method yield interrupts the execution and returns the current value. When the Routine is resumed via the next method, it continues from where it was paused.

(
r = Routine({
    "abcde".yield;
    "fghij".yield;
    "klmno".yield;
    "pqrst".yield;
    "uvwxy".yield;
    "z{|}~".yield;
});
)
 
r.next;    // get the next value from the Routine
6.do({ r.next.postln });

One can also create more complex Routines using a subroutine like loop. This is similar to a while loop, but it does not require a condition, since it always evaluates to true.

(
r = Routine({
    var delta;
    loop {
        delta = rrand(1, 3) * 0.5;
        "Will wait ".post; delta.postln;
        delta.yield;
    }
});
)
 
r.next;
 
TempoClock.default.sched(0, r);
 
r.stop;

You can also replace the statements with instructions to play a Synth.

(
SynthDef(\singrain, { |freq = 440, amp = 0.2, sustain = 1, out|
    var sig;
    sig = SinOsc.ar(freq, 0, amp) * EnvGen.kr(Env.perc(0.01, sustain), doneAction: Done.freeSelf);
    Out.ar(out, sig ! 2);    // sig ! 2 is the same as [sig, sig]
}).add;
 
r = Routine({
    var delta;
    loop {
        delta = rrand(1, 3) * 0.5;
        Synth(\singrain, [freq: exprand(200, 800), amp: rrand(0.1, 0.5), sustain: delta * 0.8]);
        delta.yield;
    }
});
)
 
r.play;
 
r.stop;

Routines cannot be paused. When you send a play message to it, it will start to play, when you send a stop message, it will stop, period.

TASK

A Task on the other hand can be paused and pick up the next value from where it stopped.

(
t = Task({
    loop {
        [60, 62, 64, 65, 67, 69, 71, 72].do({ |midi|
            Synth(\singrain, [freq: midi.midicps, amp: 0.2, sustain: 0.1]);
            0.125.wait;
        });
    }
}).play;
)
 
// probably stops in the middle of the scale
t.stop;
 
t.play;    // should pick up with the next note
 
t.stop;

Start Time

To start serveral Tasks at the same time you can use the quant parameter of the play method. quant corresponds roughly to bar length, while 0 is the beginning of the bar. An Array of two numbers tells SuperCollider the bar length and the phase.

(
f = {
    Task({
        loop {
            [60, 62, 64, 65, 67, 69, 71, 72].do({ |midi|
                Synth(\singrain, [freq: midi.midicps, amp: 0.2, sustain: 0.1]);
                0.25.wait;
            });
        }
    });
};
)
 
t = f.value.play(quant: 4);        // start on next 4-beat boundary
 
u = f.value.play(quant: [4, 0.5]);    // next 4-beat boundary + a half-beat
 
t.stop; u.stop;

Using data routines in note sequencing is very usefull. But take in mind, because of the while loop, the playing stops, when no new values are delivered.

(
var midi, dur;
midi = Routine({
    [60, 72, 71, 67, 69, 71, 72, 60, 69, 67].do({ |midi| midi.yield });
});
dur = Routine({
    [2, 2, 1, 0.5, 0.5, 1, 1, 2, 2, 3].do({ |dur| dur.yield });
});
 
SynthDef(\smooth, { |freq = 440, sustain = 1, amp = 0.5, out|
    var sig;
    sig = SinOsc.ar(freq, 0, amp) * EnvGen.kr(Env.linen(0.05, sustain, 0.1), doneAction: Done.freeSelf);
    Out.ar(out, sig ! 2)
}).add;
 
r = Task({
    var delta;
    while {
        delta = dur.next;
        delta.notNil
    } {
        Synth(\smooth, [freq: midi.next.midicps, sustain: delta]);
        delta.yield;
    }
}).play(quant: TempoClock.default.beats + 1.0);
)

A note on server messaging and timing

Patterns

Collection and Arrays

An Array is a type of a Collection, which is (surpise!) a collection of Objects. Collections are Objects themselves, and most types of Collections can hold any types of objects, mixed together, including other Collections.

An Array us an ordered collection of limited maximum size. You create an array by putting objects between two quare brackets, with commas in between:

a = ["foo", "bar"];    // "foo" is at index 0; "bar" is at index 1
a.at(0);
a.at(1);
a.at(2);        // returns "nil", as there is no object at index 2
 
// there's a shorthand for at that you'll see sometimes:
a[0];            // same as a.at(0);

In SupderCollider Arrays also have a special use: They are used to implement multichannel audio. When your Function resturns an Array of UGens, each slot of the Array correspond with an outputchannel (outputchannels in SuperCollider starts with 0).

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

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 on 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;

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

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.

Stereo 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;

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

My Curriculum

What i need to learn:

  • SuperCollider Syntax
  • SuperCollider Programm Architecture
    • Abstractions
  • 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