Routines and clocks
Session and Pbind are convenience layers over three plain objects you can also drive yourself:
- a
Routine— what happens over time, written as a Python generator thatyields how long to wait; - a
TempoClock— when it happens: it schedules the routine and keeps musical time in beats; - a
Server— where the sound goes: it owns the communication interface and plays events on it.
This page works at that level: write a routine by hand and play notes from it through a Server, on the high-level API throughout — you build Events and call play, never hand-assemble OSC bundles. For the different ways a clock can keep time — the default wall clock, locked to a server's sample clock, or a shared transport — and how to observe each, see Timing models.
Logical time vs physical time
This is the idea that makes routines worth using, inherited from SuperCollider.
A routine yields numbers, and each one is a wait in beats before the clock resumes it. Those waits play out in physical time — the actual wall-clock seconds the OS sleeps — and under load they jitter. But a routine also keeps a logical time: the running sum of everything it has yielded so far, relative to when it started and the clock's tempo. The logical time has no jitter — a routine that yields 0.5 four times is at logical beats 0, 0.5, 1.0, 1.5 exactly, whatever the scheduler did in between.
The Server stamps every event from the routine's logical time, not from "now". So even though the routine is woken at slightly irregular physical instants, the timing it asks the server for is precise. That is the only way to get jitter-free rhythmic sequences in real time, and every timing model builds on it.
A routine by hand
An Event is a dict of note parameters that knows how to play itself: Event(...).play(server) creates a synth at the routine's current logical beat and schedules its release after the note's sustain. Build it with keywords or from a plain dict — an Event is a dict — and play it from inside a routine, yielding the gap to the next note:
from clausters.base import TempoClock, Routine
from clausters.seq import Event
from clausters.defs import Server
server = Server("127.0.0.1", 57110, latency=0.1) # a running server, on UDP
clock = TempoClock(tempo=2.0) # 2 beats per second
def melody():
for note in [60, 62, 64, 67, 69]: # MIDI notes
e = Event(midinote=note, amp=0.2, dur=0.5) # half-beat note
e.play(server)
yield e.delta() # advance to the next note
clock.play(Routine(melody)) # schedule it to start now
clock.run(3.0) # advance the clock 3 s, then stop
server.close()
A few things worth knowing:
- An
Eventcarries musical defaults (see the API reference):midinote(ordegree, or an explicitfreq) sets pitch,ampthe level,instrumentthe def (the server has a built-indefaultsine). Timing comes fromdur: the note'sdelta(beats to the next event) isdur, and itssustain(how long it sounds) isdur * legato(legatodefaults to0.8). As in SuperCollider, an explicitdeltaorsustainkey overrides that calculation —Event(..., dur=0.5, sustain=0.4)sounds for exactly 0.4 beats. A dict works just as well —Event({"midinote": 60, "amp": 0.2, "dur": 0.5}).play(server). clock.run(seconds)starts the real-time driver, waits, and stops it. Useclock.start()/clock.stop()to keep one clock running across several routines.- A routine optionally receives the clock as its argument (
def melody(clock):) if it needs it, but for playing events you rarely do — the Server finds the logical beat itself. - This clock paces against wall-clock OSC time, the default. To make the same routine drift-free and sample-accurate, or to phase-align several clients, lock it to the server — see Timing models.
The one rule
A routine must never block the clock thread. It runs on that thread, so a time.sleep, a blocking server.sync(), or any wait=True def send freezes every other routine and the whole timeline. Cede time with yield instead. To load a def from within a routine, send it asynchronously — server.add_synthdef(sdef, wait=False) — and yield enough time before the first note that uses it.
The random context: one seed for a whole script
Everything random — Pwhite, Prand, and the module functions
clausters.next_f64() / uniform(lo, hi) / next_below(n) / choice(items)
— draws from one seedable context, the sclang model:
main.seed(n)seeds the root generator. Called before you build and play, it makes every random value in the script reproducible, end to end.- Every routine gets its own generator when it is created, seeded from the context creating it. Same root seed + same creation order = the same music; and because each routine draws from its own stream, concurrent routines (several clocks, live RT next to an NRT render) stay reproducible per routine no matter how their wakes interleave.
- A draw always uses the generator of the routine running right now, falling back to the root outside any routine.
There are no per-pattern seeds — independent seeds would break whole-script consistency. To isolate some material, play it inside its own routine: it gets its own derived stream by construction. The generator itself lives in the shared native core, so the same seed replays the same values in every Clausters client language.
from clausters import main, uniform
from clausters.seq import Pbind, Pwhite
main.seed(2026) # the whole script is now reproducible
detune = uniform(-3.0, 3.0) # a one-off draw from the same context
pattern = Pbind(degree=Pwhite(0, 7), dur=0.25) # draws when played
Offline, with the same code
Everything here works unchanged offline. Build the Server with an OscNrtInterface, drive the clock with clock.render() instead of run(), and the routine's play calls accumulate a timed score the bundled renderer turns into samples — no server, no audio device. That swap is the client's central seam, covered in Sessions and The client, layer by layer.
See also
- Timing models — the ways a clock keeps time (wall-clock, sample-locked, shared transport) and how to observe each.
- Sessions — the ergonomic handle that bundles a clock and a server, for when you do not need this level of control.
- The client, layer by layer — where routines, clocks and the server sit in the whole client.
- API reference —
TempoClock,Routine,Eventand theServermethods used here.