Overview
Here are some notes on the most basic topics of making Sounds in SuperCollider.
Basics of making a Sound
UGens and synth functions
The sound generators in SuperCollider are called UGens (unit generators). These can receive messages like ar
, which tells the UGen to operate at audio rate. This means the UGen calculates a new value for every sample.
There is also the kr
message, which tells a UGen to operate at control rate. This means it calculates a new value only once per control block. For example, if the block size is 64, the UGen generates a new value every 64 samples.
Most UGens can receive both ar
and kr
messages, but not every UGen supports both. It depends on the UGen’s implementation.
This example creates a function containing a SinOsc
UGen, which receives the ar
message with some arguments.
The function then receives the play
message.
The play
message evaluates the function and sends the resulting UGen graph to the server.
{ SinOsc.ar(440, 0, 0.2) }.play;
There are some common arguments to UGens which you will encounter frequently:
- freq – the frequency at which the UGen should operate
- phase – the initial phase of the UGen
- mul – a factor that multiplies the output of the UGen
- add – an offset value added to the output of the UGen
Here is a example which shows how to patch UGens together:
(
{ var ampOsc;
ampOsc = SinOsc.kr(0.5, 1.5pi, 0.5, 0.5);
SinOsc.ar(440, 0, ampOsc);
}.play;
)
A very common way of starting and stopping individual synths is to assign them to a variable and apply the play
method at the same time. After this you can apply the free
method to the variables.
// Run one line at a time (don't stop the sound in between):
a = { Saw.ar(LFNoise2.kr(8).range(1000, 2000), mul: 0.2) }.play;
b = { Saw.ar(LFNoise2.kr(7).range(100, 1000), mul: 0.2) }.play;
c = { Saw.ar(LFNoise0.kr(15).range(2000, 3000), mul: 0.1) }.play;
// Stop synths individually:
a.free;
b.free;
c.free;
When you create a synth function, you can also use arguments within these, like in every function. With the set
method you can change the values of these arguments after starting your synth function. It’s good practice to provide default values for these arguments.
x = {arg freq = 440, amp = 0.1; SinOsc.ar(freq, 0, amp)}.play;
x.set(\freq, 778);
x.set(\amp, 0.5);
x.set(\freq, 920, \amp, 0.2);
x.free;
When a Synth is created by playing a function with .play
, it includes a hidden \gate
and \fadeTime
parameter. These allow graceful stopping of the synth via e.g. x.set(\gate, 0)
or x.set(\gate, 0, \fadeTime, 2)
. The envelope and cleanup behavior are added automatically, even though they are not explicitly written in the code.
Basic UGens and Oscillators
You can find an overview on the most basic UGens in the Help Document: Tour of UGens.
Here is a short overview of the most common oscillators in SuperCollider.
// standard oscillators
{ SinOsc.ar }.plot; // sine wave
{ Saw.ar }.plot; // sawtooth wave
{ Pulse.ar }.plot; // square wave, pulse wave
{ LFTri.ar }.plot; // triangle wave
{ VarSaw.ar(width: 0.75 )}.plot; // variable saw and triangle wave
{ Blip.ar }.plot; // bandlimted impulse oscillator
{ WhiteNoise.ar }.plot; // white noise
{ PinkNoise.ar }.plot; // pink noise
{ LFNoise0.ar }.plot; // stepped noise
{ LFNoise1.ar }.plot; // linear interpolated noise
{ LFNoise2.ar }.plot; // cubic interpolated noise
There are UGens that generate Unipolar and Bipolar signals per default. With the plot
or poll
message, you can check what your chosen UGens is outputting. poll
is printing 10 numbers per second to the post window.
{ SinOsc.kr(1).poll }.play;
{ LFNoise0.ar(1).poll }.play;
{ MouseX.kr(minval: 300, maxval: 2500, lag: 10).poll }.play;
You can also use scope
instead of play
to play a signal and put it on an oscilloscope at the same time.
{ Pulse.ar(freq: MouseX.kr(100, 600), width: MouseX.kr(0, 1), mul: MouseY.kr(0, 1) )}.scope;
Scale Numbers and Signals
The range
mehtod scales an output signal to a predefined range, between a minimum and maximum. It expects, that you use the default output signal. That means you should not use it in conjunction with add
and mul
.
{ SinOsc.ar(freq: LFNoise0.kr(10).range(500, 1500), mul: 0.1) }.play;
{ LFPulse.ar(freq: 200, width: MouseY.kr(0, 1)).range(-0.5, 0.5) }.scope;
With methods like linlin
, explin
, expexp
and linexp
you can convert a linear or exponential range to another linear or exponential range.
a = [1,2,3,4,5,6,7];
// Rescale to 0−127, linear to linear
a.linlin(1, 7, 0, 127).round(1);
// Rescale to 0−127, linear to exponential
a.linexp(1, 7, 0.01, 127).round(1); // don't use zero for an exponential range
Multichannel Expansion
When you use an Array as argument to an UGen like SinOsc.ar
, this will cause multichannel expansion. This means, when you plug an Array into one of a UGen’s arguments, you get an Array of that UGen instead of a single one.
// multichannel expansion to stereo
{ SinOsc.ar([440, 442], 0, 0.2) }.play;
By default a monophonic signal is send to the first output channel (left) respectively to the first output bus. When you create an array of UGens, these are send to consecutive output busses, starting at the first. But this also means, when you create an array which size is larger then the size of your output busses, you will just hear the signals going in to you busses, that are currently avaible.
The position of the multichannel expansion is very important. It describes either a process is duplicated or just a signal. Compare these two examples.
// compare these two functions
(
{
var numOfSignals = 128;
var noise = PinkNoise.ar(1/2 ! numOfSignals);
var cf = LFNoise1.kr(1 ! numOfSignals).exprange(160, 1600);
var filter = BPF.ar(noise, cf, 0.01);
Splay.ar(filter);
}.play;
)
(
{
var numOfSignals = 128;
var noise = PinkNoise.ar(1/2 ! numOfSignals);
var cf = LFNoise1.kr(1).exprange(160, 640) ! numOfSignals;
var filter = BPF.ar(noise, cf, 0.01);
Splay.ar(filter);
}.play;
)
From time to time you can run into this error message: "exceeded number of interconnect buffers"
. There is a default limit of allowed interconnections in a UGen Buffer of 64, you can rise this with s.options.numWireBufs_(NUM)
.
With the choose
message on an Array, everytime the function is called, it will choose randomly one of it’s data slots. So, when you use an Array with mixed data, like this one [[660, 880], [440, 660], 1320, 880]
, as input, you will get random stereo expansion or a monophonic signal.
(
{ var freq;
freq = [[660, 880], [440, 660], 1320, 880].choose;
SinOsc.ar(freq, 0, 0.2);
}.play;
)
Panning
Stereo Panning is made easy with the Pan2.ar
UGen. It expects a monophonic signal and a panning Signal between -1 (hard left) and 1 (hard right).
// the PinkNoise is panned between -0.5 and 0.5
{ Pan2.ar(PinkNoise.ar(0.2), SinOsc.kr(0.5)) }.play;
This is a more complex example.
(
x = {
var lfn = LFNoise2.kr(1);
var saw = Saw.ar(
freq: 30,
mul: LFPulse.kr(
freq: LFNoise1.kr(1).range(1, 10),
width: 0.1));
var bpf = BPF.ar(in: saw, freq: lfn.range(500, 2500), rq: 0.01, mul: 20);
Pan2.ar(in: bpf, pos: lfn);
}.play;
)
x.free;
When you work of with a stereo signal you can also use Balance2.ar
, which allows balancing the two signal channels.
// balancing between PinkNoise and a Sine wave
(
{
var sine = SinOsc.ar(220, mul: 0.1);
var noise = PinkNoise.ar(0.1);
var signals = [sine, noise];
var balance = LFNoise1.kr(1).range(-1, 1);
Balance2.ar(signals[0], signals[1], balance);
}.play;
)
Mix Down / Signal Summing
You can mix signals with a simple addition:
{ PinkNoise.ar(0.2) + SinOsc.ar(440, 0, 0.2) + Saw.ar(660, 0.2) }.play;
There is also the Mix
class, which allows you to mix down an array of channels down to a single channel, or arrays of channels down to a single array of channels.
// one channel
{ Mix.new([SinOsc.ar(440, 0, 0.2), Saw.ar(660, 0.2)]).postln }.play;
// combine two stereo arrays
(
{
var a, b;
a = [SinOsc.ar(440, 0, 0.2), Saw.ar(662, 0.2)];
b = [SinOsc.ar(442, 0, 0.2), Saw.ar(660, 0.2)];
Mix([a, b]).postln;
}.play;
)
The Mix
class also has another class method called fill
, which is acting similiar to a loop. A Function will be evaluated n times, whereby n is the first argument to Mix.fill();
.
// this will generate 8 sinetones, each with a unique random offset on the frequencie
// the amplitude is tamed by passing n to the mul argument
(
var n = 8;
{ Mix.fill(n, { SinOsc.ar(500 + 500.0.rand, 0, 1 / n) }) }.play;
)
When you declare an argument in your function, you can make use of the n variable. N is counted up from 0 to n-1 and passed to the function.
// Look at the post window for frequencies and indices
(
var n = 8;
{
Mix.fill(n, { arg index;
var freq;
index.postln;
freq = 440 + index;
freq.postln;
SinOsc.ar(freq , 0, 1 / n)
})
}.play;
)
With Splay
you can spread an array of signals evenly from left to right. It’s basically a downmix of a multichannel array to a stereo signal.
// Mix it down to stereo (spread evenly from left to right)
c = { Splay.ar(SinOsc.ar([100, 300, 500, 700, 900], mul: 0.1)) }.scope;
// Fun with Splay:
(
d = { arg fundamental = 110;
var harmonics = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var snd = BPF.ar(
in: Saw.ar(32, LFPulse.ar(harmonics, width: 0.1)),
freq: harmonics * fundamental,
rq: 0.01,
mul: 20);
Splay.ar(snd);
}.play;
)
d.set(\fundamental, 50);
d.free;
Playing Soundfiles
Play a Buffer
For playing a Buffer with a Soundfile, you have to create a buffer on the server and read a soundfile in it. Both can be accomplished with Buffer.read()
. After this you can play the buffer with the UGen PlayBuf
.
// Load files into buffers:
~buf1 = Buffer.read(s, "/path/to/sndfl1.wav"); // one sound file
~buf2 = Buffer.read(s, "/path/to/sndfl2.wav"); // another sound file
// Playback:
{ PlayBuf.ar(1, ~buf1) }.play; // number of channels and buffer
{ PlayBuf.ar(1, ~buf2) }.play;
// Get some info about the files:
[~buf1.bufnum, ~buf1.numChannels, ~buf1.path, ~buf1.numFrames];
[~buf2.bufnum, ~buf2.numChannels, ~buf2.path, ~buf2.numFrames];
// Changing playback speed with 'rate'
{ PlayBuf.ar(numChannels: 1, bufnum: ~buf1, rate: 2, loop: 1) }.play;
{ PlayBuf.ar(1, ~buf1, 0.5, loop: 1) }.play; // play at half the speed
{ PlayBuf.ar(1, ~buf1, Line.kr(0.5, 2, 10), loop: 1) }.play; // speeding up
{ PlayBuf.ar(1, ~buf1, MouseY.kr(0.5, 3), loop: 1) }.play; // mouse control
// Changing direction (reverse)
{ PlayBuf.ar(1, ~buf2, −1, loop: 1) }.play; // reverse sound
{ PlayBuf.ar(1, ~buf2, −0.5, loop: 1) }.play; // play at half the speed AND
Envelopes and the Env Object
A simple Envelope in the shape of a line can be created with the Line
Object. If you need something more then a envelope with two breakpoints in a linear distance you can work with the Env
object, which can define breakpoint envelopes in several ways.
You should also know, that these breakpoint envelopes are played back with the EnvGen
UGen. Most of the time you don’t need this EnvGen
, but it’s good to know, that under the hood there is something hidden.
Look at this:
// This:
{ SinOsc.ar * Env.perc.kr(doneAction: 2) }.play;
// ... is a shortcut for this:
{ SinOsc.ar * EnvGen.kr(Env.perc, doneAction: 2) }.play;
Env
Env
allows the biggest flexibility of all envelope generators in SuperCollider. It takes an Array of N levels and needs a corresponding times Array of size N-1. The times values describe the distance in seconds between the levels values.
With the key-value releaseNode
you can introduce a sustain value. When the envelope reaches this value it will hold this value while gate is equal 1. For this you need to use the key-value gate
in the EnvGen
. When you set the gate to 0, the envelope will finish it’s process from the releaseNode.
With the key-value loopNode
you can introduce a loop cycle in cooperation with the releaseNode
. The envelope will cycle from the releaseNode to the loopNode+1 in the time from loopNode to loopNode+1.
Check the documentation for more info.
(
SynthDef(\envExample, {
|freq = 80, gate = 1|
var env = EnvGen.ar(
Env(
levels: [0, 1, 0.125, 1, 0.125, 0],
times: [1, 0.5, 0.25, 0.1, 0.25],
curve: \wel,
releaseNode: 4,
loopNode: 0,
).plot,
gate: gate,
doneAction: 2
);
var sig = SinOsc.ar(freq * env.range(1, 2), 0, 0.1);
Out.ar(0, sig * env);
}).add;
)
~snd = Synth(\envExample);
~snd.set(\gate, 0);
You can always use the collect
method for a generative approach for the levels, times and curves arrays.
Env.perc
Env.perc
is a easy way to get a percussive envelope. It takes 4 arguments: attackTime, releaseTime, level and curve.
You also need to tell Env.perc
on which rate it should be executed. For this you append ar
or kr
to the Env.perc()
and you can also add a doneAction: 2
to free your synth after the envelope did it’s work.
Env.perc.plot; // using all default args
Env.perc(0.5).plot; // attackTime: 0.5
Env.perc(attackTime: 0.3, releaseTime: 2, level: 0.4).plot;
Env.perc(0.3, 2, 0.4, 0).plot; // same as above, but curve:0 means straight lines
{PinkNoise.ar(Env.perc.kr(doneAction: 2))}.play; // default Env.perc args
{PinkNoise.ar(Env.perc(0.5).kr(doneAction: 2))}.play;
{PinkNoise.ar(Env.perc(0.3, 2, 0.4).ar(2))}.play;
{PinkNoise.ar(Env.perc(0.3, 2, 0.4, 0).kr(2))}.play;
Env.triangle
The name explains itself. It only takes two arguments: duration, and level.
// See it:
Env.triangle.plot;
// Hear it:
{SinOsc.ar([440, 442], mul: Env.triangle.kr(2))}.play;
// By the way, an envelope can be a multiplier anywhere in your code
{SinOsc.ar([440, 442]) * Env.triangle.kr(2)}.play;
Env.linen
Env.linen
is a trapezoid formed like envelope, but it’s attack and release ramp can also be shaped by it’s curve argument.
You can specify attackTime, sustainTime, releaseTime, sustainLeve, and curve.
// See it:
Env.linen(1).plot;
// Hear it:
{SinOsc.ar([300, 350], mul: Env.linen(0.01, 2, 1, 0.2).kr(2))}.play;
Env.pairs
With Env.pairs
you can describe any envelope you wish. It takes breakpoint pairs as arguments, which consists of [time, level]
, and a type of curve.
(
{
var env = Env.pairs([[0, 0], [0.4, 1], [1, 0.2], [1.1, 0.5], [2, 0]], \lin);
env.plot;
SinOsc.ar([440, 442], mul: env.kr(2));
}.play;
)
ADSR and ASR Envelopes
Sometimes you need a envelope with a undefined duration, which is deppending on the duration of a other event, like pressing a key on a keyboard.
For this you can use the Env.adsr
and Env.asr
envelopes. They expect a gate
value, which triggers (1) the envelope a and also sets them on it’s release phase (0).
Check this example:
// ASR
// Play note ('press key')
// attackTime: 0.5 seconds, sustainLevel: 0.8, releaseTime: 3 seconds
x = {arg gate = 1, freq = 440; SinOsc.ar(freq: freq, mul: Env.asr(0.5, 0.8, 3).
kr(doneAction: 2, gate: gate))}.play;
// Stop note ('finger off the key' − activate release stage)
x.set(\gate, 0); // alternatively, x.release
// ADSR (attack, decay, sustain, release)
// Play note:
(
d = { arg gate = 1;
var snd, env;
env = Env.adsr(0.01, 0.4, 0.7, 2);
snd = Splay.ar(BPF.ar(Saw.ar((32.1, 32.2..33)), LFNoise2.kr(12).range(100, 1000), 0.05, 10));
Out.ar(0, snd * env.kr(doneAction: 2, gate: gate));
}.play;
)
// Stop
d.release; // this is equivalent to d.set(\gate, 0);