Overview
These are notes on Functions, Routines, and Tasks in SuperCollider, how they are defined, used, and when to use them.
Content
Functions, Routines, and Tasks
A Function executes its code immediately and completely and returns only one value. A Function cannot be interrupted. A Routine and a Task can be interrupted and can return multiple values. Tasks in contrast to Routines can be paused, and remembers their current state when they’re paused.
Functions and Function-Objects
Functions are defined via using curly brackets and can contain one or more commands. Everything enclosed in the brackets becomes a Function object. This object is an instance of SuperCollider’s Function class, which has several methods implemented.
When a Function is evaluated via the .value method it returns the value of its last command.
{ 3 + 4 }.value; // returns 7
(
{
3 + 4;
2 + 2;
}.value; // returns 4
)Functions can also be played via 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;There is also a shortcut for .value:
f.();Functions can have parameters that receive Arguments when the Function 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 errorRoutines
A Routine is a function-like class that can encapsulate a series of tasks, which are executed step by step. 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. If you want to play the routine again, you need to .reset it.
The Routine class takes a function as input. The .wait method let’s the routine wait for a given duration.
(
f = {
"Hello,".postln;
1.wait;
"World".postln;
1.wait;
"!".postln;
1.wait;
};
)
r = Routine(f);
r.play;
r.stop;
r.reset;The .wait method blocks the routine for a given time and the routine waits for a given time until the next part is executed.
You can insert the function directly as an input argument to Routine.
(
r = Routine({
"Hello,".postln;
1.wait;
"World".postln;
1.wait;
"!".postln;
1.wait;
});
)
r.play;
r.stop;
r.reset;The method .yield interrupts the execution and, in contrast to .wait, 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 do and loop. do duplicates it’s content by a given number or inf.
(
r = Routine({
3.do {
Synth(\tri, [freq: 60.midicps]);
0.2.wait;
Synth(\tri, [freq: 65.midicps]);
0.2.wait;
Synth(\tri, [freq: 67.midicps]);
0.2.wait;
};
});
)
r.play;
r.stop;
r.reset;loop is similar to a while loop, but it does not require a condition, since it always evaluates to true.
The Routine class has a single letter shortcut: you can replace the class symbol Routine() by r().
(
f = {
loop {
Synth(\tri, [freq: 60.midicps]);
0.2.wait;
Synth(\tri, [freq: 65.midicps]);
0.2.wait;
Synth(\tri, [freq: 67.midicps]);
0.2.wait;
};
};
)
r = r(f).play;
r.stop;
r.reset;You can also randomise the time delta between calls. But this only works with .play.
(
r = Routine({
var delta;
loop {
delta = rrand(1, 3) * 0.1;
Synth(\tri, [freq: exprand(54, 60).midicps]);
delta.wait;
};
});
)
r.play;
r.stop;Here is a more complex example, where two Routines are working together:
// using a function as a note generating routine
(
~notesfunc = {
var num = 50;
while { num <= 108 } {
num.yield; // this returns the note value on .next
num = num + rrand(2, 5);
if (num > 108) {
num = 50; // resets to 50
};
};
};
~notes = r(~notesfunc);
~synthsfunc = {
var note;
note = ~notes.next; // call first generated notes by ~notesfunc
while {note != nil} {
Synth(\tri, [freq: note.midicps]);
0.2.yield;
note = ~notes.next.postln; // call next generated notes by ~notesfunc
};
};
~synths = r(~synthsfunc);
)
~synths.play;
~synths.stop;
~synths.reset;
~synths.next;
Task
A Task 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(\default, [freq: midi.midicps, amp: 0.2, dur: 0.001]);
0.125.wait;
});
}
}).play;
)
t.stop; // probably stops in the middle of the scale
t.play; // should pick up with the next noteStart 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);
)