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:

  • SystemClock tracks the current running time of the SuperCollider program and is suitable for precise scheduling.
  • AppClock is similar to SystemClock, 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 (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 bar

Quantizing Events

When you schedule things on a clock, you can quantize these events with the Quant class.

The Quant class has 3 arguments:

  • .quant describes the beat, when your Routine should start playing. 1 means on the next avaiable beat, 4 means on the next-multiplied-by-4 avaiable beat
  • .phase let’s you add a phase offset. 1 would mean do create an offset of 1 beat
  • .timingOffset is 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.