DEV Community

Noah Hradek
Noah Hradek

Posted on

A Quick Introduction to Supercollider

Setup

I'm going to introduce a audio synthesis and music programming tool called Supercollider. It contains both an audio server and a programming language called sclang. The audio server generates audio and can be used from any programming language using the OSC protocol. Sclang is a ruby like language that is purely object oriented and behaves a bit like smalltalk. You send messages to objects which can then execute various code blocks or pass in parameters. It should be familiar enough if you know C like languages, ruby, or python but I will go over the syntax as well. You should at least know what functions, objects, variables, and arguments are. It's also helpful if you have a basic understanding of mathematics like sine waves and some music knowledge.

First download Supercollider from the website. Use the installer for Mac and Windows, for linux there's a source tarball or repo packages that can be used. Next startup supercollider and you should see a code window with a console window to the right and documentation. The documentation window just contains all the documentation which you can browse and the console window contains the server status and standard output.

Since Supercollider uses a client server architecture you must boot the server which sclang uses. Click on Server->Boot Server in the menu on top. Since I'm on a Mac I'm going to use the key combination Cmd-B which also starts the server. You can also use the message s.boot; which boots the default sever defined in s.

You should see the items on the bottom of the console window turn green. That indicates the server booted along with some messages. You might get some error message, maybe relating to the audio rate, lookup any errors you get to fix them.

Variables and Functions

First I'm going to introduce some language features. Since Supercollider is almost purely object oriented it behaves a bit like Ruby where you can send messages to objects. For example I can type "Hello Supercollider".postln;. To run this select the code block and use the key combination Shift-Enter. You can also click on Language->Evaluate Selection, Line, or Region in the menu.

You should see the text Hello Supercollider in the console window at the bottom right. In this example we sent the message postln to the string "Hello Supercollider". Statements end with a semicolon but if we only have one statement we can omit them. We can also do arithmetic like we can in other C based languages like so. ((4+5)/2).postln;

We should get the response 4.5
There's some more interesting messages that can be applied to numbers. For example finding the first 5 fibonacci numbers 5.fib or finding the square root 5.sqrt. In Supercollider we have the same standard datatypes of strings, numbers, arrays (lists), we also have new datatypes like signals which we will go into later.

Variables are the cornerstone of any kind of procedural programming and we have two types in Supercollider. Global and local variables. Global variables are defined throughout the file in any scope. If the variable name is just one letter long, you can just use it directly like this a = 3

For longer global variable names you prefix it with ~ like this ~myGlobal = 2

Local variables are defined with the var keyword and exist within a certain scope defined by code blocks or in a function. Code blocks are scoped blocks of code which execute all at once. To do this we wrap the code in parenthesis like this.

(
var x = 2;
y = 3
)

(
x.postln;
y.postln;
)
Enter fullscreen mode Exit fullscreen mode

When you click on the first parenthesis and do Shift-Enter the block runs and assigns the variables. The second code block when executed gives us nil and 3. Nil is the null value and it occurs because we are outside the scope of x however since y is a global variable it can exist in any scope.

We can use code blocks to execute code at once, but we also might want to name our blocks and pass arguments. We use functions for this, defined with curly braces like in C. However the functions work a bit differently since they are untyped.

(
var add = { arg x, y; x + y };
add.value(1, 2);
)
Enter fullscreen mode Exit fullscreen mode

The function add is defined with the braces and the argument list after the arg keyword. The last line in a function is what is returned.
To find the value of a function executed with arguments we can use the value message. Functions are higher-order as well meaning we can pass them as arguments too.

(
var applyTwice = { arg f, x; f.value(f.value(x)) };
applyTwice.value({arg x; 2 * x}, 1);
)
Enter fullscreen mode Exit fullscreen mode

There's a lot of interesting functional programming techniques that can be used here but it's too complex for this tutorial.

Function arguments can be defined in multiple ways. We can use the arg keyword like this.
var sq = { arg n; n*n }
Or with the || operator and the argument list inside.
var sq = { |n| n * n };

Most of the algorithms in sclang are implemented with functions and functional programming techniques combined with object-oriented programming. The traditional procedural techniques such as while loops exist but aren't as readily used.

Loops and Conditionals

Now we get onto looping which is usually done a bit differently than in many C like languages. With looping in supercollider we tend to use message based loops similar to Javascript or Ruby. For example I can use the do message to loop a number of times.

(
5.do {
    arg n;
    (n*n).postln;
}
)
Enter fullscreen mode Exit fullscreen mode

To 5 the message do is passed which executes the function five times. The passed in argument is the current iteration which is then squared.

We can also do conditionals like if statements. Although a bit differently than in other languages. We use if like a function call with a condition, the then clause, and finally the else clause passed in as functions. Since the condition doesn't change we don't pass it in as a function.

(
var x = 4;
if(x < 5, {"X is less than 5".postln}, {"X is greater than or equal to 5".postln});
)
Enter fullscreen mode Exit fullscreen mode

We can also do while loops which look similar but since the condition changes we need to use a function.

(
var x = 0;
while({x < 10}, {x = x + 1; x.postln});
)
Enter fullscreen mode Exit fullscreen mode

We can also do for loops like in Python or Ruby.

for(1, 10, { |x| x.postln})
Enter fullscreen mode Exit fullscreen mode

Arrays

Arrays are the sequential datatype in supercollider. They behave a bit like lists in Python.
You can run through the items of an array.

[1,2,3,4].do { |x| x.postln; }
Enter fullscreen mode Exit fullscreen mode

We can create a new array and really any new object with the new message.

(
var a = Array.new;
a = a.add(5);
a = a.add(6);
a.postln;
)
Enter fullscreen mode Exit fullscreen mode

Arrays must be reassigned whenever a new item is added. We can get the size with the size message.

(
var a = [1,2,3,4];
a.size.postln;
)
Enter fullscreen mode Exit fullscreen mode

We can get a specific item from an array with at.

[1,2,3,4].at(0);
Enter fullscreen mode Exit fullscreen mode

We can use the fill message to fill an array with a function at a specific size.

(
var a = Array.fill(5, { |x| 2**x });
a.postln;
)
Enter fullscreen mode Exit fullscreen mode

Oscillators

The basis of Supercollider is sound synthesis. In essence generating sounds which are basically signals varying over time. You can think of this as an array of floats, usually two arrays because audio output has two channels. The height of the signal is the amplitude and the rate at which it moves is called the frequency. A higher frequency makes a higher pitched sound and a lower frequency makes a lower pitched sound. The lowest frequency which is usually highest in amplitude is called the fundamental frequency and every frequency afterwards is called a partial.

Image description

Sound is generated via oscillators which allow you to create dynamic time varying signals. We can play these using the play message for a function like this.

{ SinOsc.ar(440) }.play;
Enter fullscreen mode Exit fullscreen mode

This plays a sine wave at 440Hz. To stop the sound you can use the key combination Cmd - . on mac. There's other parameters we can manipulate such as phase and mul which is the amplitude factor. I recommend watching videos on signal processing where they explain frequency, phase, etc. to get an understanding of it.

We can plot the signal with the plot message.

{ SinOsc.ar(440, ) }.plot;
Enter fullscreen mode Exit fullscreen mode

Or view an oscilloscope while playing the sound.

{ SinOsc.ar(440, ) }.scope;
Enter fullscreen mode Exit fullscreen mode

Mul attenuates the signal multiplying it by the factor and add adds to the signal.

{ SinOsc.ar(freq: 440, mul: 0.5, add: 0.1) }.scope;
Enter fullscreen mode Exit fullscreen mode

We can think of the signal as being modified like this.
mul*Sin(freq*x + phase) + add

We can output multiple signals to different channels by passing in an array.

{ SinOsc.ar([440,660]) }.scope;
Enter fullscreen mode Exit fullscreen mode

There's some other oscillators we can use as well like Saw and Pulse. I would look into them and see how they differ from SinOsc. Oscillators are a type of object called Unit Generators which form the basis of sound synthesis in supercollider. There are many UGens which can produce sound, filter sound, etc. To give you an example we'll use a filter to filter out the harsh sounds of a saw oscillator.


(
{
    var sig = Saw.ar(440);
    LPF.ar(sig, freq: 500);
}.scope;
)
Enter fullscreen mode Exit fullscreen mode

You may have noticed I am using the message ar on the UGens. This stands for audio rate and is used for signals that represent output sound, we can also use the message kr (control rate) for signals that control other signals like controlling the frequency of a signal.

(
{
    var control = SinOsc.kr(2).range(220, 440);
    var sig = Saw.ar(control);
    LPF.ar(sig, freq: 500);
}.scope;
)
Enter fullscreen mode Exit fullscreen mode

This creates a control signal running two times per second which goes between 220 and 440 and controls the frequency of our saw signal.

We can use some interesting UGens like Mix to combine signals. The fill message behaves a bit like it does for an array except it adds all the signals together to create one signal with the fundamental at 220 and 4 other partials afterwards. We can view this with the freq scope by running FreqScope.new or just going to Server->Show FreqScope in the menu. This displays the frequencies that make up the currently running signal. Try running this code and see what happens.

(
FreqScope.new;
{
    Mix.fill(5, { |x| SinOsc.ar(220*x) })
}.scope;
)
Enter fullscreen mode Exit fullscreen mode

We can create more interesting sounds by increasing the number of partials or changing the wave.

Signals can be added, subtracted, multiplied, everything we can do to numbers we can also do with signals.

(
{
    var fund = 220;
    SinOsc.ar(fund) + SinOsc.ar(fund*2) + SinOsc.ar(fund*4);
}.scope;
)
Enter fullscreen mode Exit fullscreen mode

When you look at the freqscope you can see how we now have three frequencies with partials at 440 and 880. Any sound can be made up of a series of Sine wave frequencies and all we have to do is add them up to form a sound. This is called additive synthesis. Subtractive synthesis is when we take away from a sound like using filters to sculpt it into what we need it to be like this where we use a low pass filter to only allow frequencies below a certain range to pass through. There's also high pass which only allows higher signals to pass through

(
{
    var fund = 440, cutoff=600;
    LPF.ar(Pulse.ar(fund), freq: cutoff);
}.scope;
)
Enter fullscreen mode Exit fullscreen mode

There's other types of synthesis but for now focus on those specific types.

SynthDefs

Sometimes we want to save our signals and be able to use them later. Like we would use a synthesizer to play different notes for example. We can store our signals in SynthDefs which are objects which can create and modify sound. To create a synthdef we pass in a symbol representing the name, and a function with arguments.

(
SynthDef(\saw, {
    arg freq=440, cutoff=1000;
    var sig;
    sig = Saw.ar(freq);
    sig = LPF.ar(sig, cutoff);
    Out.ar(0, sig);
}).add;
)

Synth(\saw, [\freq, 440, \cutoff, 300]);
Enter fullscreen mode Exit fullscreen mode

We can create the synth and then pass in the arguments when run. The add message is called on the SynthDef to add it to the server. The Out UGen is used to send the signal to the stereo output, use bus number 0 for now. Symbols start with a \ and are used to name synths among other things, for now use simple names.

We can then later set the arguments for the synth using the set message.

(
var asynth = Synth(\saw);
asynth.set(\freq, 550);
)
Enter fullscreen mode Exit fullscreen mode

Envelopes

This is great except our sound seems to run forever. We can make it last only a specific amount of time using envelopes. Envelopes can affect the amplitude, frequency or any other property of a signal but for making the signal decrease in volume we will affect the amplitude.

(
Env([1,0], [1]).plot;
SynthDef(\saw, {
    arg freq=440, cutoff=1000;
    var sig, env;
    env = EnvGen.kr(Env([1, 0], [1]));
    sig = Saw.ar(freq);
    sig = LPF.ar(sig, cutoff) * env;
    Out.ar(0, sig);
}).add;

Synth(\saw);
)
Enter fullscreen mode Exit fullscreen mode

We pass in Env to the EnvGen UGen which generates the envelope and uses it to control the amplitude of the signal. The parameters to Env are the levels which can vary from 0 to 1 and the times which represent the amount of time it takes in seconds. The times will be one less than the amount. We can see how the signal decreases linearly over time. We can create an envelope which increases and then decreases like this.

(
Env([0, 1, 0], [1, 1]).plot;
SynthDef(\saw, {
    arg freq=440, cutoff=1000;
    var sig, env;
    env = EnvGen.kr(Env([0, 1, 0], [1, 1]));
    sig = Saw.ar(freq);
    sig = LPF.ar(sig, cutoff) * env;
    Out.ar(0, sig);
}).add;

Synth(\saw);
)
Enter fullscreen mode Exit fullscreen mode

You notice how the signal fades in and fades out. This is because we start from 0 and go to 1 in one second and then go back to 0 in another second. Thus the sound takes 2 seconds to finish. There are other ways we can manipulate the envelope for example changing the curve. There are also other types of envelopes for example perc and asdr. I won't go into those now, just look them up and try using them.

Patterns

Now we get into the sequencing of sounds. This is what actually allows us to play music. The main way we do this in Supercollider is with patterns. Patterns are like arrays that can be manipulated, played, and used indefinitely. The main function we use to create patterns is Pbind. We use Pbind with modifiers like Pseq to create intricate patterns.

Pbind(\freq, 440).play;
Enter fullscreen mode Exit fullscreen mode

This runs a repeating note at 440Hz.

We can play a series of eighth notes infinitely with a sequence of increasing notes starting at middle C with random amplitudes.

(
Pbind(
    \dur, 0.125,
    \midinote, Pseq([0, 1, 2, 3, 4] + 60, inf),
    \amp, Prand([0.125, 0.5, 0.25], inf)
).play;
)
Enter fullscreen mode Exit fullscreen mode

We can change the instrument to the fist saw synth we defined earlier. Run that code again and then run this code, giving the sound a richer timbre.

(
Pbind(
    \dur, 0.25,
    \midinote, Pseq([0, 1, 2, 3, 4] + 60, inf),
    \amp, Prand([0.125, 0.5, 0.25], inf),
    \instrument, \saw
).play;
)
Enter fullscreen mode Exit fullscreen mode

We can select a series of random notes from C Major creating a random melody.

(
Pbind(
    \dur, 0.25,
    \midinote, Prand([0, 2, 4, 5, 7, 9] + 60, inf),
    \amp, Prand([0.125, 0.5, 0.25], inf),
    \instrument, \saw
).play;
)
Enter fullscreen mode Exit fullscreen mode

I will probably do an entire post on patterns, but for now this should suffice. Just play around with different patterns and look at the different types of pattern generators.

I hope you enjoyed this post, just play around with the examples and try creating more complicated SynthDefs. Changing parameters and manipulating how they interact.

Top comments (0)