Overview
Here are some notes on scheduling events, routines and clocks. Some information are interconnect. To understand everything you need to work through this document.
Scheduling Events and Sequencing
Scheduling with Routines
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 = {
Synth(\tri, [freq: 60.midicps]);
1.wait;
Synth(\tri, [freq: 65.midicps]);
1.wait;
Synth(\tri, [freq: 67.midicps]);
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({
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;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;
Clocks
In SuperCollider, scheduling of events (not to be confused with the class Event) 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:
SystemClocktracks the current running time of the SuperCollider program and is suitable for precise scheduling.AppClockis similar toSystemClock, but it has a lower system priority. This makes it more appropriate for GUI updates, where exact timing is less critical.TempoClockis 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 (in this case a function call) will happen in 5 seconds.
.sched is for scheduling an event at a relative time to the function call. .schedAbs can schedule an event to an absolute time, e.g. with TempoClock.nextbar.
(
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 });
)The scheduled function can return a time or beat value (depending on the used clock) on which the scheduling process should be repeated. This is relative to the first scheduled event.
If you want the function to run only once, make sure to en the function with ‘nil’.
// runs only once
TempoClock.default.sched(1, { rrand(1, 3).postln; nil });
// this function returns =1= to =.schedAbs=, which means, it will be rescheduled in 1 beat
~postln = { "hello".postln; 1; };
TempoClock.default.schedAbs(TempoClock.default.nextBar, ~postln);Be aware of scheduling events by a negative number or 0 you can get run into infinite loops or other problems.
Scheduling with TempoClock
You can create as many TempoClock 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.
TempoClock expects it’s argument as beats per seconds. To translate this to beats per minute you need to divide the number by 60. Via using the .permanent_(true) message on the TempoClock, the TempoClock will survive when you use Cmd+. and does not need to be reevaluated.
You can define a TempoClock like this:
~t = TempoClock(141/60); // is interpreting it's argument as beats per SECOND
~t = TempoClock(141/60).permanent_(true); // the TempoClock will survie Cmd+.Query information from the clock:
~t.tempo; // query the current tempo in bps
~t.tempo * 60; // query the current tempo in bpm
~t.beats; // query the current beat
~t.beatDur; // query the duration of one beat in seconds
~t.nextBar; // query the next start of a bar
~t.beatsPerBar; // query the number of beats in a barQuantizing Events
When you schedule things on a clock, you can quantize these events with the Quant class.
The Quant class has 3 arguments:
.quantdescribes the beat, when your Routine should start playing.1means on the next avaiable beat,4means on the next-multiplied-by-4 avaiable beat.phaselet’s you add a phase offset.1would mean do create an offset of 1 beat.timingOffsetis for ordering the calculation of threads (not used very often)
~r = r(~f).play(~t, Quant(4, 0, 0)); // insert a Quant object for quantized scheduling
// this can be shorten to
~r = r(~f).play(~t, Quant(4));
// and even shorter
~r = r(~f).play(~t, 4);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(\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);
)Patterns
Precision in Scheduling
Problems with language-side Synth calls
Scheduling events from the language side of SuperCollider, e.g. from loops inside a routine, is not very precise, since sclang sends OSC messages to the server and this takes time and depends on other processes that happen at the same time on your computer.
When scheduling things on a clock, you can make it more precise by nesting your Synth calls into s.bind().
This bundles the OSC message with information about the default server latency (s.latency ← OSC latency, not audio latency) and gives the Synth call precise timing.
But this is still not as precise as it could be. The scheduling still happens on the boundaries of a control cycle. The scheduled event always starts at the beginning of a control cycle, even when the scheduling message wants it to start partway into a control cycle. To take this into account, you can use OffsetOut instead of Out. This lets the event still schedule at the beginning of a control cycle, but adds an offset to the audio vector, which makes the audio production start between the boundaries of a control cycle.