A DAW-style transport
A DAW has a transport: a shared timeline with a tempo and a bar grid. Everything you arm starts aligned to bars, and moving the tempo moves the whole arrangement with it. Clausters offers that same idea, but across independent clients: a server hosts one shared beat grid, several clients join it, and a routine on each starts on the same bar. This page is the practical guide to that workflow — being the conductor, joining as a follower, starting together on a bar, and following a tempo change live.
It builds on two other pages. Timing models explains why the alignment is beat-accurate or sample-exact (the time reference a clock paces against); this page is the how of the transport itself. Receiving OSC and MIDI is the input layer the live-change section uses.
The shared grid
The transport is deliberately small: an origin sample (the sample position of beat 0) and a tempo (beats per second). Together they are a grid — beat b is sample origin + b·rate/tempo — that the server stores under /transport and any client can read. That is the whole of it: the server hosts the grid but never plays from it. There is no server-side playhead rolling forward; each client's TempoClock is its own playhead, and the grid is the common ruler they all measure bars against.
One client is the conductor: it defines the grid once.
from clausters.defs import Server
server = Server() # UDP to 127.0.0.1:57110
server.set_transport(origin_sample=0, tempo=2.0) # beat 0 at sample 0, 2 beats/s
Every other client is a follower: it adopts that grid as its own tempo and origin.
from clausters.base import TempoClock
clock = TempoClock() # its own tempo is about to be overwritten
clock.join_transport(server) # adopt the shared tempo + origin
With a Session, both sides are one call — session.server.set_transport(...) to conduct, session.join_transport() to follow. clock.leave_transport() (or never joining) returns a clock to its own private grid.
Starting together on a bar
A DAW starts a clip on the next bar, not the instant you click. The client's equivalent is quant: the beat boundary a routine's start snaps to.
clock.start() # the playhead must be running first
clock.play(routine, quant=4) # start on the next 4-beat bar
session.play(pattern, quant=4) # the Session form
quant=4 snaps the start to the next beat that is a multiple of 4 — a bar in 4/4. quant=1 is the next beat; None or 0 starts immediately. Because every follower's quant snaps to the same shared grid, they all land on the same bar, so independent clients begin in phase. Start the clock before playing the quantized routine, so quant measures against the running grid rather than a stopped one.
quant works without a transport too — then it snaps to the clock's own elapsed beats, which is the clean way for a single client to drop a new voice in on the next bar. Joining a transport is what makes that bar the same bar across clients.
Beat-accurate or sample-exact
How tightly the clients align depends on the time reference each clock paces against — the subject of Timing models, in one paragraph here:
- Plain (wall-clock) followers align to the beat, drift-bounded: the grid's sample origin is mapped to OSC time through the server's
/clockanchor, so everyone agrees on the bar to within the wall-vs-audio drift. - Followers that also
lock_to(server)align to the sample: the grid lives on the master's sample axis, so the shared bar is one exact sample for all of them.
clock.lock_to(server) # sample-exact timing (drift-free; the master clock)
clock.join_transport(server) # ...then phase-align on the shared bar
Order does not matter much, but lock first and join second reads well: choose the reference, then align on it.
Following a tempo change live
This is where the transport behaves most like a DAW's: when the conductor changes the tempo (or origin), every follower should move with it. Setting /transport again pushes the new grid to every client registered for notifications, so followers do not have to poll. A follower reacts with an OSC responder on /transport.reply:
from clausters.base import OscReceiver
from clausters.responders import OscFunc
recv = OscReceiver().start()
recv.send(server.target.addr(), "/notify", 1) # subscribe on this socket
def follow_transport(msg, time, src):
# msg == ["/transport.reply", origin_sample, tempo, defined]
if msg[3]: # defined
clock.join_transport(server) # re-adopt the new grid
OscFunc(follow_transport, "/transport.reply", recv=recv)
Now the conductor doing server.set_transport(0, 3.0) later in the session re-tempos every follower at once — the bar grid they quantize against moves together. (Register /notify from the receiver's socket, as above, so the push lands where the responder is listening.) The shipped osc_responder.py example wires exactly this reaction; see Examples.
Rolling the transport: a conductor with play / stop / locate
The shared transport also carries a DAW-style rolling state — whether it is playing and the song position — that a conductor drives and every client's playhead obeys. The server holds the state and broadcasts each change; it still never schedules audio, so each client rolls its own playhead on the shared grid.
A conductor (any client) drives it through the Server:
server.set_transport(0, 2.0) # define the grid (stopped at position 0)
server.transport_play(0.0) # roll from beat 0 -- every follower starts
server.transport_locate(16.0) # seek the song position to beat 16
server.transport_stop() # halt every follower
A follower binds a Playhead to the transport with follow_transport, and from then on the playhead mirrors the conductor — it rolls on transport_play, halts on transport_stop, and seeks on transport_locate:
from clausters.seq import Playhead
head = Playhead(timeline, clock, server)
clock.start()
head.follow_transport(server, quant=4) # obey the transport; start on a bar
follow_transport registers /notify and an OscFunc on /transport.reply (the responder layer) so it reacts to the broadcast, then applies the current state once. Because every follower computes from the same broadcast state, they roll in lockstep: beat-aligned in plain wall-clock mode, and sample-exact when each clock is also lock_to the server. The everyone-is-symmetric design makes this simple — every client (including the one issuing the commands, if it follows too) reacts to the same broadcast identically. transport_conductor.py (Examples) shows two followers rolling together; unfollow_transport() releases it.
A worked example: two clients, one bar
transport_sync.py runs two completely independent client pairs — each its own Server and TempoClock, the state two separate programs would hold — and lands a note from each on the same bar. The check uses public state only, so any client on the same transport computes the same number:
import math
def next_bar_sample(server, clock, quant=4):
origin, tempo = server.transport()
rate = clock.timebase.sample_rate
grid_beat = (clock.timebase.current_sample() - origin) * tempo / rate
target = math.ceil(grid_beat / quant) * quant
return round(origin + target * rate / tempo)
Sampled back to back, the two clients return the same next-bar sample — that equality is the phase alignment. Each then play(..., quant=4)s a note, and the two sound together. The example is in Examples.
What it is, and what it is not
The analogy to a DAW transport is the bar grid and tempo plus a play/stop/position state — enough to lock clients to the same bars, tempo, and rolling playhead. The rest of a DAW's transport is intentionally not here:
- The server broadcasts transport control, it does not schedule audio. It holds the grid and the rolling state (playing + position) and pushes changes; the actual rolling — which note sounds when — is each client's own
Playheadon the shared grid (see Timelines and the playhead). The audio scheduling stays per-client (via/sched), so the server never becomes an audio clock. - One grid per server, last-writer-wins. There is a single shared transport; whoever calls
set_transportmost recently defines it. Several conductors are a coordination choice you make, not something the server arbitrates. (Multiple independently named transports on one server were considered and deferred.) - Tempo and origin only — no meter object. A "bar" is whatever beat multiple you pass as
quant; there is no separate time-signature the server stores. Pick aquantthat matches your meter (4 for 4/4, 3 for 3/4). - No server-side recording or arrangement. The timeline a playhead rolls lives in the client; the server holds only the shared position and tempo, not the notes.
These are the honest edges of a small, composable feature: shared bars, a shared tempo, and a shared play/stop/position that several clients phase-align on, with each client owning its own playhead and arrangement.
Cheat-sheet
| You want to… | Do this |
|---|---|
| Define the shared grid (conductor) | server.set_transport(origin_sample, tempo) |
| Read the current grid | server.transport() → (origin_sample, tempo) or None |
| Join the grid (follower) | clock.join_transport(server) / Session.join_transport() |
| Leave it | clock.leave_transport() |
| Start on the next bar | clock.play(routine, quant=4) / session.play(pattern, quant=4) |
| Align to the sample, not just the beat | clock.lock_to(server) as well (see Timing models) |
| Follow live tempo changes | an OscFunc("/transport.reply", …) that re-join_transports (see Receiving OSC and MIDI) |
| Roll a playhead from a conductor | server.transport_play() / transport_stop() / transport_locate(beat); followers playhead.follow_transport(server, quant=4) |
| Read the rolling state | server.transport_state() → {tempo, playing, position, …} |
See also
- Timing models — the time reference behind beat-accurate vs sample-exact alignment.
- Routines and clocks — the playhead (
TempoClock) and the routines you start on the bar. - Receiving OSC and MIDI — the responder layer the live-change reaction uses.
- Sessions — the handle that bundles a clock and a server, with
join_transport. - Examples —
transport_sync.py(two clients on one bar) andosc_responder.py(the live transport reaction). - The Clausters server book —
/transportand/clockon the wire.