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 linecmd+return
- evaluate selection, line or regioncmd+b
- boot servercmd+.
- free all synths on servercmd+d
- while having the cursor on a class, this opens the manual entry for this classcmd+shift+d
- look up for documentationcmd+ß
- show / hide help browsercmd+a
- select allcmd+shift+p
- clear the post window
the helpfull section
s.volume.gui;
- creates a graphical slider to control the output volumes.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
to3
are reserved for the outputs, - indices
4
and5
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 byb.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 valuesPseq
- takes a list as input and cycles over itPrand
- picks random values from a listPwhite
- creates random values between a lower and higher borderPser
- likePseq
but describes the total number of values you get when cycling over the listPxrand
- likePrand
but without repitition of valuesPshuf
- shuffles the list and cycles over the shuffled listPslide
- makes segments out of a list and steps through the segmentsPgeom
- creates a series of values similiar to a geometric seriesPn
- 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 toSystemClock
, 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; )
Footer
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