API reference

clausters.session

Session: ergonomic defaults without global state.

The client deliberately avoids global state, but sc3's ease of use came largely from its globals (Server.default, the default clock). A Session gives that ergonomics back explicitly: it bundles a Server and a TempoClock into one handle with play / render / run, and the nrt / live factories pick sensible defaults. Because it is an ordinary object, several sessions coexist — e.g. one offline NRT session for plotting next to a live RT one — in the same script, which globals make impossible.

s = Session.nrt(tempo=2.0)
s.play(Pbind(instrument="default", freq=Pseq([440, 550, 660]), dur=0.5))
samples, frames = s.render()        # drains the clock, renders the score

Session

class Session()

One Server plus one TempoClock, bundled into a single handle.

This is the client's ergonomic entry point. Rather than wiring a server, a clock and (optionally) a timebase together yourself, you take a Session that owns both and drives them as a unit -- play a pattern on it, render it offline, or run it for some seconds live.

Prefer the factories to the constructor: nrt builds an offline, score-accumulating session and live a real-time one over UDP, each with sensible defaults. The constructor is for the uncommon case of supplying your own Server and clock.

Which factory you call is the only thing that differs between an offline render and a live take: that difference lives in the Server's communication interface, not in the pattern or the clock. So the same play drives either kind, and an offline and a live session can run side by side in one script.

Arguments:

  • server - the Server to drive -- a live one, or one holding an OscNrtInterface for offline rendering.

  • clock - the TempoClock that sequences it; a fresh one at tempo 1.0 is created when omitted.

    Closing the session closes its server, so the common shape is a context manager:

with Session.live(tempo=2.0, latency=0.1) as s:
    s.play(Pbind(instrument="default", degree=Pseq([0, 2, 4]), dur=0.5))
    s.run(3.0)

Session.nrt

@classmethod
def nrt(cls, tempo: float = 1.0) -> "Session"

Build an offline (non-real-time) session.

Its Server holds an OscNrtInterface, so playing a pattern accumulates a timetagged score instead of sending anything; render then turns that score into samples through the bundled embedded renderer. No server process and no audio device are involved.

Arguments:

  • tempo - the clock's tempo, in beats per second.

Returns:

A Session whose render produces the audio.

Session.live

@classmethod
def live(cls,
         host: "str | None" = None,
         port: "int | None" = None,
         *,
         tempo: float = 1.0,
         latency: "float | None" = None,
         timebase=None,
         boot: bool = True,
         options=None,
         shm="auto",
         transport: "str | None" = None,
         verbose: int = 0,
         data_dir=None,
         server_args=(),
         ready_timeout: float = 10.0) -> "Session"

Build a real-time session, starting a server if none is up.

The probe (and the boot handshake) ride UDP — discovery stays zero-config — and the session's command interface then connects over TCP by default (transport="udp"/"ws" opt across), so defs and bulk reads are not bounded by a datagram.

This is the everyday live-coding entry point. By default (boot=True) it ensures a server the way nrt ensures a renderer: if one already answers at the target address it attaches to it, and if none does it launches a separate clausters process — choosing a shared-memory segment for you — and connects to that. Either way you get a session you drive the same. A server the session started is stopped when the session is closed or the interpreter exits, so a REPL or script leaves nothing running; a server it merely attached to is left alone.

Pass boot=False for the plain attach-only behavior (never start a process): connect to a server you launched yourself, possibly remote.

Arguments:

  • host - the server's host; None takes the config file's [client].host (default 127.0.0.1). Booting is local.
  • port - the server's UDP port; None takes [client].port (the Clausters default is 57110).
  • tempo - the clock's tempo, in beats per second.
  • latency - seconds added to each event's timetag so it reaches the server slightly ahead of its play time and sounds on time instead of late; a small value such as 0.1 is typical for a live take. None takes the config file's [client].latency.
  • timebase - the clock's pacing source. The default (monotonic) paces in wall-clock seconds; a SampleClockTimebase anchors timing to the server's sample clock for drift-free scheduling.
  • boot - start a server if none is already answering (default). False attaches only, never launching a process.
  • options - a clausters.defs.ServerOptions sizing a launched server and this client's allocators alike; None uses the defaults.
  • shm - the shared-memory segment for a launched server — "auto" picks one, a path forces it, None launches without one. The path is remembered so gui maps the same segment.
  • transport - the command carrier — "tcp" (default), "udp" or "ws"; None takes [client].transport from the config.
  • verbose - launched-server log verbosity (1/2/3 -> -v/ -vv/-vvv; negative -> -q).
  • data_dir - a launched server's --data-dir; None uses default.
  • server_args - extra CLI tokens for a launched server (e.g. ["--tcp"]).
  • ready_timeout - seconds to wait for a launched server to answer.

Returns:

A Session you drive with run (or start / stop).

Session.embed

@classmethod
def embed(cls,
          tempo: float = 1.0,
          latency: "float | None" = None,
          workers: int = 0,
          timebase=None,
          server=None) -> "Session"

Build a real-time session backed by an in-process embedded server.

The whole server — audio device and engine — runs in this process through the bundled native library; there is no socket and no separate server process. Otherwise it is identical to live: the same routines, patterns and defs drive it, because only the Server's communication interface differs (an OscEmbedInterface instead of UDP). So an embedded, a live and an offline session can run side by side in one script.

Arguments:

  • tempo - the clock's tempo, in beats per second.
  • latency - seconds added to each event's timetag so it lands a touch in the future and sounds on time; a small value such as 0.05 is typical even in-process.
  • workers - engine worker threads for parallel node processing (0 lets the server choose).
  • timebase - the clock's pacing source (default monotonic wall clock).
  • server - an existing clausters.Clausters handle to reuse; when omitted the session opens and owns a fresh embedded server and closes it on close.

Returns:

A Session you drive with run (or start / stop), exactly like live.

Session.gui

def gui(*,
        port: "int | None" = None,
        transport: str = "tcp",
        verbose: int = 0,
        data_dir=None,
        extra_args=(),
        ready_timeout: float = 10.0)

Launch (once) a clausters-gui visual server wired to this session's server, and return a clausters.gui.GuiHost connected to it.

The GUI parallel of live booting a server: one call and the visual server is up, its client leg pointed at this session's server and — when that server was launched with a shared-memory segment — mapping the same segment, so meters, scopes and playheads read the engine with no per-frame messages. You never spell out an address or a segment path: they come from the session. The host is owned by the session and stopped on close (or interpreter exit).

Idempotent: repeated calls return the same GuiHost (the port and other options of the first call stand).

Arguments:

  • port - the GUI host's own port (script -> host, UDP and TCP alike); None uses the host default (57210).
  • transport - the carrier this session talks to the host over — "tcp" (default; a /gui_def tree is not bounded by a datagram) or "udp".
  • verbose - host log verbosity (1/2/3 -> -v/-vv/-vvv).
  • data_dir - the host's --data-dir for its GuiDef store.
  • extra_args - extra host CLI tokens.
  • ready_timeout - seconds to wait for the host to answer.

Returns:

A started clausters.gui.GuiHost. Use clausters.gui.GuiHost.open to open a window, set to edit it and clausters.gui.GuiHost.close to close it.

Session.play

def play(pattern, quant=None)

Play an event pattern on this session's clock and server.

Arguments:

  • pattern - an event pattern, e.g. a Pbind.
  • quant - optional quantization handed to the player -- the beat grid the routine starts on; None starts immediately.

Returns:

The EventStreamPlayer driving the pattern.

Session.render

def render(sample_rate: float = 48_000.0, channels: int = 2)

Drain the clock and render the accumulated score (offline only).

Advances the clock logically with no real-time waiting, so every scheduled event lands in the score, then renders that score through the embedded renderer.

Arguments:

  • sample_rate - render sample rate, in Hz.
  • channels - number of interleaved output channels.

Returns:

(samples, frames) -- interleaved float32 in a stdlib array('f') and the frame count. Schedule a closing event (e.g. freeing the root group) so the render has a defined duration.

Session.lock_to_server

def lock_to_server()

Lock this session's clock to its server's sample clock — the sample-accurate, drift-free timebase, with the server as the master clock. Returns self, so it chains after a factory: Session.live(...).lock_to_server().

Safe when the server is not a reachable master (offline, or no server running): the clock simply stays on wall-clock OSC time. See TempoClock.lock_to.

Session.join_transport

def join_transport()

Join this session's server's shared transport, so a quant-ed pattern starts on the same beat as every other client on it (see TempoClock.join_transport). Returns self for chaining: Session.live(...).lock_to_server().join_transport(). No-op if the server has no transport defined.

Session.run

def run(seconds: float)

Run the clock in real time for seconds, then stop (live only).

Arguments:

  • seconds - how long to advance the clock, in wall-clock seconds.

Returns:

self, so calls chain.

Session.start

def start()

Start the clock so scheduled events fire in real time; returns self. Pair with stop, or use run to start, wait and stop in one call.

Session.stop

def stop()

Stop the clock; returns self. Events scheduled past the stop point do not fire.

Session.close

def close()

Close the underlying Server and release the clock's master-clock tracker (from lock_to_server), if any. Also stops the GUI host (gui) and, if live launched a server, that process too — so nothing is left running. Done automatically when the session is used as a context manager and, for launched processes, on interpreter exit.

clausters.responders

Responders: OscFunc / MidiFunc (port of sc3/base/responders.py).

The input half of the client. Until now the client was output-only — it built OSC/MIDI and sent it to the server. Responders add the receive path and the client's role as a general MIDI/OSC hub, mirroring sclang's OSCFunc/MIDIFunc: receive OSC and MIDI from any application, match and dispatch to a callback, and let that callback emit OSC/MIDI onward — to the Clausters server or to other apps.

A responder registers a self-filtering handler with a receiver (the transport + demux thread): clausters.base.OscReceiver for OSC, clausters.base.MidiReceiver for MIDI. Pass one explicitly, or rely on the lazily-created module defaults (default_osc_receiver / default_midi_receiver — opt-in convenience, the one bit of process-wide state here, in the spirit of main.default_clock).

Callbacks run on the receiver's thread (or, if the receiver has a clock, on the clock thread): keep them quick and non-blocking — the golden rule. To sequence in response to an event, schedule a routine on a clock (non-blocking) instead of looping inside the callback.

from clausters.responders import OscFunc, MidiFunc

# Relay an incoming /play to the server as a /s_new.
OscFunc(lambda msg, t, src: server.synth("default", {"freq": msg[1]}), "/play")

# Drive the server from a MIDI keyboard: note on -> /s_new, note off -> free.
notes = {}
def on(m, src):
    notes[m["note"]] = server.synth("default", {"freq": 440 * 2 ** ((m["note"] - 69) / 12)})
MidiFunc(on, "note_on")

default_osc_receiver

def default_osc_receiver() -> OscReceiver

The lazily-created, started default OscReceiver (ephemeral UDP port). Created on first use so importing this module opens no socket. Override it with set_default_osc_receiver (e.g. to bind a fixed port external apps can target, or to attach a clock).

set_default_osc_receiver

def set_default_osc_receiver(receiver: OscReceiver) -> OscReceiver

Install receiver as the module default returned by default_osc_receiver. Start it yourself first.

default_midi_receiver

def default_midi_receiver() -> MidiReceiver

The lazily-created, started default MidiReceiver (a virtual input port). Created on first use, so importing this module opens no MIDI port and needs no live-built clausters-midi. Override with set_default_midi_receiver.

set_default_midi_receiver

def set_default_midi_receiver(receiver: MidiReceiver) -> MidiReceiver

Install receiver as the module default returned by default_midi_receiver. Start it yourself first.

OscFunc

class OscFunc()

Responder for incoming OSC messages.

Registers func to fire when a message matching path arrives. The callback is called func(msg, time, src)msg the message as a list [addr, arg1, …], time the bundle's Unix time (or None for an immediate / bare message), src the (host, port) of the sender.

Arguments:

  • func - the callback func(msg, time, src).

  • path - the OSC address to match (a leading / is added if missing).

  • src - optional (host, port) — respond only to that sender. A port of None matches any port from that host.

  • arg_template - optional list matched against the message arguments by position; an entry is a literal (compared equal), a predicate callable, or None (matches anything). Shorter than the message is fine — only the listed positions are checked.

  • recv - the clausters.base.OscReceiver to register with; defaults to default_osc_receiver.

    The responder is enabled on creation. Call free (or disable) when done.

OscFunc.enable

def enable()

Start responding (registers the handler with the receiver).

OscFunc.disable

def disable()

Stop responding without discarding the object (re-enable-able).

OscFunc.free

def free()

Disable permanently; call when finished with this responder.

OscFunc.one_shot

def one_shot()

Free the responder after its first match — a one-time action.

MidiFunc

class MidiFunc()

Responder for incoming MIDI messages.

Registers func to fire on channel-voice messages of a given type. The callback is called func(message, src)message a dict ({'type', 'channel', …}, see clausters.base._midiinterface.parse_midi) and src the port name.

Arguments:

  • func - the callback func(message, src).

  • midi_msg - a message type ('note_on') or a list of types (['note_on', 'note_off']).

  • chan - optional channel (0..15) to restrict to.

  • arg_template - optional {field: matcher} dict matched against the message fields; a matcher is a literal, a predicate callable, or None (matches anything).

  • recv - the clausters.base.MidiReceiver to register with; defaults to default_midi_receiver.

    Enabled on creation; free (or disable) when done.

MidiFunc.enable

def enable()

Start responding (registers the handler with the receiver).

MidiFunc.disable

def disable()

Stop responding without discarding the object (re-enable-able).

MidiFunc.free

def free()

Disable permanently; call when finished with this responder.

MidiFunc.one_shot

def one_shot()

Free the responder after its first match — a one-time action.

oscfunc

def oscfunc(path, **kwargs)

Decorator building an OscFunc over a callback.

@oscfunc("/play")
def resp(msg, time, src):
    print(msg, time, src)

midifunc

def midifunc(midi_msg, **kwargs)

Decorator building a MidiFunc over a callback.

@midifunc(["note_on", "note_off"])
def resp(message, src):
    print(message, src)

clausters.ipc

Clausters Python bindings. Standard library only, by design.

Two ways in, both speaking ordinary OSC bytes (build them however you like; examples/json_client.py has stdlib helpers):

  • Clausters — the embedded server: loads the cdylib (build it with cargo build --release --features embed,realtime) and runs the whole server in-process. Commands are function calls, no network anywhere.
  • ShmClient — attaches to a separate server started with clausters --shm <path>: commands/replies travel through a shared-memory ring, and the data plane (sample clock, control buses) is read and written directly in mapped memory.

Plus render — the synchronous "scientific" call: hand it a binary score, get the interleaved float32 samples back, no server running at all.

Boundary rule (project-wide): only flat data crosses — bytes in, array('f')/floats/ints out. A numpy user can wrap the results without copying (numpy.frombuffer), but nothing here imports anything heavy.

Caveats of the pure-Python shm path: Python has no atomics, so cursor reads and writes rely on x86-TSO-style ordering of aligned 32-bit accesses — fine on the supported platforms, documented in docs/ipc.md.

ShmClient

class ShmClient()

Client of a clausters --shm <path> server (same machine).

ShmClient.clock

@property
def clock() -> int

The engine's sample counter, mirrored every block (64 samples).

ShmClient.ctl_set

def ctl_set(index: int, value: float)

Writes the very atomic the engine's InCtl reads next block.

ShmClient.request

def request(packet: bytes, timeout: float = 2.0) -> bytes

The synchronous facade: send, then block (the client blocks, never the server) until a reply arrives.

render

def render(score: bytes,
           sample_rate: float = 48000.0,
           channels: int = 2,
           workers: int = 0,
           lib_path: str | None = None) -> tuple[array, int]

Synchronous offline render: binary score in, (samples, frames) out — interleaved float32 in a stdlib array('f'). The whole call blocks the caller and nothing else; there is no server involved.

Clausters

class Clausters()

The embedded live server: audio device + engine in this process.

Clausters.request

def request(packet: bytes, timeout: float = 2.0) -> bytes

Synchronous facade: blocks this thread until the reply arrives.

Clausters.clock

@property
def clock() -> int

Block-accurate sample counter, straight from shared memory.

clausters.errors

Library-specific exception types.

A small hierarchy so callers can catch what went wrong concretely instead of guessing from a stray AttributeError or a generic OSError. Everything derives from ClaustersError, and each leaf also derives from the builtin it used to be raised as (OSError, RuntimeError, BufferError, TimeoutError, ValueError) — so existing except OSError: / except RuntimeError: code (and the test-suite skips) keep working unchanged while new code can be precise:

try:
    samples, frames = clausters.render(score)
except clausters.LibraryFeatureError as e:
    print(e.symbol, "needs", e.feature)   # build it with the right feature
except clausters.ClaustersError:
    ...                                    # any other library failure

ClaustersError

class ClaustersError(Exception)

Base class for every error this library raises on purpose.

LibraryError

class LibraryError(ClaustersError)

A problem with the native libclausters shared library itself.

LibraryNotFoundError

class LibraryNotFoundError(LibraryError, OSError)

The libclausters cdylib could not be located on disk.

LibraryFeatureError

class LibraryFeatureError(LibraryError, OSError)

The cdylib loaded but a required FFI symbol is missing — it was built without the Cargo feature that exports it.

The concrete cause behind the otherwise cryptic undefined symbol / AttributeError: a plain cargo build produces a libclausters.so with the FFI surface compiled out. symbol and feature say exactly what is missing and what to rebuild with.

AbiMismatchError

class AbiMismatchError(LibraryError, OSError)

The cdylib's ABI version does not match this binding's.

RenderError

class RenderError(ClaustersError, RuntimeError)

The offline render call failed (the core reported an error).

ServerError

class ServerError(ClaustersError, RuntimeError)

The embedded server could not be opened (no audio device, etc.).

CommandError

class CommandError(ClaustersError, RuntimeError)

A server command was answered with a /fail reply.

SegmentError

class SegmentError(ClaustersError, ValueError)

The shared-memory segment is not a valid clausters segment.

CommandRingFull

class CommandRingFull(ClaustersError, BufferError)

The command ring is full; the command was not sent (retry later).

ReplyTimeout

class ReplyTimeout(ClaustersError, TimeoutError)

No reply arrived within the timeout.

clausters.base.stream

Streams and routines (port of sc3/base/stream.py).

The coroutine layer. A Routine wraps a Python generator function; driving it resumes the generator, and the value it yields is a time to wait (in beats) before the next resumption. The thing that resumes routines on a schedule is the clock (clausters.base.clock); here we only define the protocol. This is the part that stays in the host language — yield is Python control flow and never moves to Rust.

StopStream

class StopStream(StopIteration)

Raised (and caught) to end a stream normally.

YieldAndReset

class YieldAndReset(Exception)

Raise from inside a routine to yield value and then reset the routine to its initial state, so its next resumption restarts the generator.

Stream

class Stream()

A lazy sequence: implements the iterator protocol over next.

Concrete streams carry their own random generator (rng), derived from the creating context at construction (see clausters.base.rand): random values drawn while a stream runs come from its stream, so one root seed (main.seed) reproduces a whole script and concurrent routines stay reproducible per routine.

rng

the stream's random generator; None falls back to the root context.

Stream.next

def next(inval=None)

Produce the next value, optionally fed inval; raise StopStream to end. Subclasses implement this -- the base raises NotImplementedError.

Stream.reset

def reset()

Return the stream to its initial state so iteration restarts. A no-op on the base; stateful subclasses override it.

FunctionStream

class FunctionStream(Stream)

Wraps a plain callable: each next calls it with inval.

Routine

class Routine(Stream)

Wraps a generator function into a resumable timeline.

The generator may take zero or one positional argument (the initial inval). Each next resumes it; a yielded number is the delay before the routine should be resumed again.

The generator must never block the thread — that is the user's responsibility. A routine runs on the clock thread (RT) or inside the render loop (NRT); blocking it (time.sleep, a blocking Server.sync/wait=True def send, any synchronous wait-for-reply) stalls every other routine and the whole timeline. Cede time with yield instead. In particular, to create a def from inside a routine use the asynchronous form -- server.add_faustdef(fdef, wait=False) (or add_synthdef(..., wait=False)) -- which only sends; do not call the blocking server.sync() here. A non-blocking, notification-driven barrier you can yield (OSCFunc) is future work; until then, send the def async and yield enough time before the /s_new that depends on it.

Routine.run

@classmethod
def run(cls, func)

Decorator/constructor sugar: @Routine.run over a genfunc.

Routine.reset

def reset()

Discard the running generator and return to the init state, so the next next or play starts the generator function afresh.

Routine.next

def next(inval=None)

Resume the generator once (sending it inval) and return the value it yields -- a delay in beats -- or raise StopStream when it finishes. The clock calls this on each wake; you rarely call it directly.

Routine.play

def play(clock=None, quant=None)

Schedule this routine to start on clock; returns self.

Arguments:

  • clock - the TempoClock to run on; when None it falls back to main.default_clock and raises if that is unset.
  • quant - start quantization, forwarded to the clock (see TempoClock.play; not yet implemented).

clausters.base.clock

TempoClock (port of sc3/base/clock.py, native-backed).

The seam between the native core and the host language. The clock owns the scheduling queue and the beat/second arithmetic — the latter delegated to clausters-core through clausters._native, so timing matches the server's sample clock. The queue holds routines (and one-shot callables); resuming a routine (the yield driver) stays in Python.

One clock, two drives:

  • run / start — real time: a background thread sleeps between events using a monotonic pacing clock; the logical beat still advances only by the routines' yields, so inter-event timing is exact and the OSC timetags (stamped from a separate wall clock) carry that exactness.
  • render — non-real time: drain the queue in beat order with no sleeping, advancing a logical clock; used to build a score.

The clock does not talk to the server: it only schedules and exposes the current time (beats, beats2secs, start_time). Sending events belongs to clausters.defs.server.Server, which owns the destination/communication interface and reads the time from the clock of the routine being resumed (the clock sets routine.clock and main.current_tt around each wake). Swapping that interface (RT/NRT/MIDI) is the seam — and it lives on the Server, not here.

TempoClock

class TempoClock()

A scheduler that keeps musical time in beats and resumes routines on it.

A clock has a tempo (beats per second) and a queue of scheduled items -- routines and one-shot callables. Two drives share that queue:

  • real time (start / run): a background thread sleeps between items, pacing against the timebase, and fires them live.
  • non-real time (render): the queue is drained in beat order with no sleeping, advancing a logical clock as fast as possible -- used to build a score offline.

The defining property is that the logical beat advances only by the routines' yields, never by wall-clock drift: a routine that yields 0.25 is resumed exactly a quarter-beat later, whichever drive is running and whatever the OS scheduler does. That is what makes inter-event timing exact -- and, with a SampleClockTimebase, sample-accurate.

The clock does not talk to the server. It only schedules and reports time (beats, beats2secs, start_time); a Server reads the clock of the routine it is resuming and emits from there. Choosing where events go (real time, offline, MIDI) is the Server's job, not the clock's.

Arguments:

  • tempo - beats per second.
  • timebase - the pacing source -- the default monotonic clock, or a SampleClockTimebase to anchor pacing and scheduling to the server's own sample clock.

TempoClock.beats2secs

def beats2secs(beats: float) -> float

Convert a beat position to seconds under the current tempo (computed in the native core, so it matches the server's own arithmetic).

TempoClock.secs2beats

def secs2beats(secs: float) -> float

Convert seconds to a beat position under the current tempo (native core, server-matching).

TempoClock.beats

def beats() -> float

The clock's current beat: the yield-driven logical beat while rendering or being woken, else the monotonic-paced elapsed beat in RT (used for scheduling relative to "now").

TempoClock.start_time

@property
def start_time()

Wall-clock origin (Unix seconds) while running in real time, else None. The Server uses it to turn a logical beat into a wall-clock OSC timetag — the wall clock, kept separate from the monotonic pacing source so timetags stay valid Unix time.

TempoClock.pacing_origin

@property
def pacing_origin()

The timebase value (seconds) captured at start. For a sample-clock timebase this is sample_origin / sample_rate, which the Server turns into the absolute sample for /sched.

TempoClock.set_tempo

def set_tempo(tempo: float)

Change tempo, pinning the current instant (no discontinuity).

TempoClock.bar

def bar(quant: float, beats: float | None = None) -> float

The bar index the clock's current beat (or an explicit beats position) falls in on a grid of quant beats per bar (0-based; quant <= 0 -> 0). The read complement of the quant argument play takes — computed in the native core, so a GUI ruler in beats shows the same bar:beat this returns.

TempoClock.beat_in_bar

def beat_in_bar(quant: float, beats: float | None = None) -> float

The beat within its bar for the clock's current beat (or an explicit beats position) on a grid of quant beats per bar (0-based, in [0, quant); quant <= 0 returns the position).

TempoClock.lock_to

def lock_to(server, warmup: bool = True, timeout: float = 2.0)

Lock this clock to a master server's sample clock, so events schedule on the server's own sample axis (drift-free) instead of a wall-clock OSC timetag.

Opt-in: a plain clock paces against wall-clock OSC time, which works standalone, against another program, or across a network. lock_to switches it to the server's sample clock — over UDP it tracks the server's published /clock anchor on its own socket. The switch is graceful: an offline (score) server, or a master that does not answer, leaves the clock on wall-clock time, so a client with no Clausters server keeps working. Returns self.

Blocking — call it before start/run, never from inside a routine (it does /clock round trips). Release it with unlock or close.

TempoClock.unlock

def unlock()

Undo a lock_to: close the tracker and return to wall-clock OSC time. Returns self.

TempoClock.close

def close()

Stop the clock and release a lock_to tracker, if any.

TempoClock.join_transport

def join_transport(server)

Adopt a master server's shared /transport beat grid as this clock's tempo and grid, so a quant-ed routine starts on the same beat as every other client joined to it.

Reads the transport once; if the server has none defined, the clock keeps its own grid (no-op). A sample-locked clock (lock_to) aligns sample-exactly; a plain wall-clock clock aligns to beats through the server's OSC-time anchor (drift-bounded). Returns self.

Blocking — call it before start/run, never from a routine.

TempoClock.leave_transport

def leave_transport()

Stop following a joined transport; quant returns to the clock's own grid. Returns self.

TempoClock.sched

def sched(delay_beats: float, item)

Schedule item to run delay_beats from the current beat.

item is a Routine (or any Stream), or a plain callable for a one-shot. When resumed, a routine is rescheduled by whatever delay it yields; a callable that returns a number is rescheduled by that number, and one returning None runs once. Safe to call from another thread or from inside a running routine.

TempoClock.sched_abs

def sched_abs(beat: float, item)

Schedule item at an absolute beat, rather than relative to the current beat as sched does.

TempoClock.play

def play(routine, quant=None)

Schedule a routine (or callable), snapping its start to a beat grid.

Arguments:

  • routine - a Routine, any Stream, or a one-shot callable.
  • quant - start quantization -- the routine starts on the next beat that is a multiple of quant (e.g. 4 = the next bar in 4/4). None or 0 starts immediately. The grid is the clock's own elapsed beats, or a shared one when the clock has joined a transport (join_transport); for multi-client alignment start the clock before playing the quantized routine.

TempoClock.clear

def clear()

Drop every item currently in the schedule queue.

TempoClock.unsched

def unsched(item)

Remove a specific scheduled item from the queue (by identity), leaving the rest in order. Used to cancel one routine — e.g. a clausters.seq.timeline.Playhead stopping or seeking — without clearing everything else clear would drop.

TempoClock.render

def render(until_beat: float | None = None)

NRT drive: process the queue in beat order without sleeping.

Returns when the queue is empty (or the next event is past until_beat). Whatever the routines emit (through a Server) lands in that Server's interface — here we only advance time and resume them.

TempoClock.start

def start()

Begin the real-time driver on a background thread.

TempoClock.stop

def stop()

Stop the real-time driver and join its background thread; returns self. Schedules built up by run/start end here.

TempoClock.run

def run(seconds: float)

Convenience: run the RT driver for seconds then stop.

clausters.base.timebase

Selectable pacing timebase for the clock.

A TempoClock's logical beat advances only by the routines' yields; the timebase is the monotonic-ish source the clock paces its sleeps against (and, in real time, anchors its OSC timetags to). Two choices:

  • MonotonicTimebase (default) — the OS monotonic clock. Events are sent as NTP-timetagged bundles; simple, drift between the client and server clocks is small but real.
  • SampleClockTimebase — seconds derived from the server's sample counter (sample() / sample_rate). The client paces against the server's own clock, and the Server emits via /sched <absolute_sample> instead of a wall-clock timetag, so there is no inter-clock drift and timing is exact at the sample. sample is any callable returning the current sample count (e.g. Clausters.clock or ShmClient.clock).

A timebase is callable (tb() == tb.now()) so it also works as the plain timebase callable the clock accepts.

SampleClockTimebase

class SampleClockTimebase(Timebase)

Seconds from the server's sample clock: now = sample() / sample_rate.

SampleClockTimebase.sample_at

def sample_at(seconds: float) -> int

The absolute sample for a time in this timebase's seconds (the core's seconds->samples conversion, ties to even).

clausters.base.main

Execution context (port of sc3/base/main.py).

A small singleton holding what routines and clocks need to find each other. The client deliberately avoids global state: the server and clock are passed explicitly, so RT and NRT can coexist in one script. The one piece of ambient context that must exist — "which routine is running right now" — is thread-local, so several TempoClock threads (and a live RT clock next to an offline NRT render) never clobber each other's current thread.

The optional default_clock is convenience-only sugar (None by default, never required). Anything that can be passed explicitly, is.

Main

class Main()

Main.current_tt

@property
def current_tt()

The routine being resumed on this thread (thread-local), set by the clock around each wake, so Server can read the running routine's exact logical beat. None outside a routine.

Main.seed

def seed(value=None)

Seeds the context RNG (None reseeds from entropy). Returns the seed actually used so a session can be reproduced.

Main.rng

@property
def rng()

The context value stream (clausters._native.RngStream, the shared core generator — reproducible across client languages). Created lazily, seeded from entropy unless seed was called.

clausters.seq.event

Events (port of sc3/seq/event.py, adapted to Clausters).

An Event is a dict of parameters with sensible defaults that knows how to play itself against a Server. The default 'note' event creates a synth and schedules its release. Timing is the clock's job: an event emits at the running routine's exact logical beat (via server.send_bundle), and the player advances by the event's delta.

By default a note frees its synth after sustain (/n_free) rather than closing a gate — unless has_gate is set, in which case it sends gate 0 (for defs whose env_gen envelope has a release node and a doneAction that frees the synth once the release finishes).

DEFAULTS

Default parameters merged into every Event. type selects behaviour (note or rest); instrument is the def name; dur is the beats to the next event, scaled by legato/stretch into the sounding time; amp is linear amplitude; add_action/target place the synth in the node tree; has_gate picks release-by-free vs gate 0; and octave/root/scale define the pitch space that degree indexes.

Event

class Event(dict)

A note event: a dict of parameters that knows how to play itself.

Built from DEFAULTS overlaid with whatever you pass, exactly like a dict (Event(freq=440, amp=0.2)), so unknown keys are simply stored. The keys split in two: a fixed reserved set drives timing and structure (dur, legato, stretch, add_action/target, the pitch keys, ...) and is never sent to the synth; every other numeric key is forwarded as a synth control.

The derived quantities compute the values actually used: midinote and freq resolve pitch (an explicit freq wins, else midinote, else degree within octave/root/scale), delta is the beats to the next event and sustain the beats the synth sounds. play renders the event on a destination -- a Server or a MIDI destination.

Event.midinote

def midinote() -> float

The MIDI note number this event sounds (the value freq derives from). An explicit freq (Hz) is inverted via cpsmidi; otherwise it comes from midinote, or degree/octave/root/scale.

Event.freq

def freq() -> float

The frequency in Hz this event sounds: an explicit freq if given, otherwise midinote converted through the native midicps.

Event.delta

def delta() -> float

Beats until the next event: an explicit delta key if given, otherwise dur * stretch. As in SuperCollider, the key overrides the calculation when it is present.

Event.sustain

def sustain() -> float

Beats the synth sounds: an explicit sustain key if given, otherwise dur * legato * stretch. As in SuperCollider, the key overrides the calculation when it is present.

Event.play

def play(destination)

Realize this event on destination (double dispatch): the OSC Server turns it into /s_new + release, a MIDI destination into note on/off — without the clock or routine knowing which. Returns whatever the destination's play_event does (the synth node id for OSC, None for a rest or MIDI).

rest

def rest(dur: float = 1.0) -> Event

A silent Event that sounds nothing but still advances time by dur beats -- a rest in the sequence.

clausters.seq.pattern

Patterns (port of sc3/seq/pattern.py + patterns/).

A Pattern is a reusable, lazy description of a value sequence; iterating it yields the values (a fresh stream each time). Value patterns (Pseq, Pwhite, …) feed Pbind, which combines per-key value patterns into a stream of Event objects. An event pattern is played on a clock with Pattern.play (see EventStreamPlayer).

Patterns are plain Python generators under the hood, so nesting and composition are natural; a sub-pattern used as a value is embedded (iterated) in place.

Pattern

class Pattern()

Pattern.stream

def stream()

A Stream over this pattern.

Pattern.play

def play(clock, server, quant=None)

Play this (event) pattern on clock, sending to server.

Pconst

class Pconst(Pattern)

A constant value, length times (infinite by default).

Pseq

class Pseq(Pattern)

The items in order, repeats times (sub-patterns are embedded).

Pser

class Pser(Pattern)

The items in order, yielding exactly length values (cycling).

Prand

class Prand(Pattern)

Random items, length values, drawn from the random context (the running routine's generator, or the root outside one — see clausters.base.rand): main.seed(n) reproduces the choices along with everything else in the script. There is no per-pattern seed — independent seeds would break whole-script consistency.

Pwhite

class Pwhite(Pattern)

Uniform random numbers in [lo, hi), length values, drawn from the random context (the running routine's generator, or the root outside one — see clausters.base.rand): main.seed(n) reproduces the sequence along with everything else in the script. There is no per-pattern seed — independent seeds would break whole-script consistency.

Pseries

class Pseries(Pattern)

Arithmetic series start, start+step, … (length values).

Pgeom

class Pgeom(Pattern)

Geometric series start, start*grow, … (length values).

Pfunc

class Pfunc(Pattern)

Calls func() for each value (length values).

Pn

class Pn(Pattern)

Repeats pattern n times.

Pbind

class Pbind(Pattern)

Binds keys to value patterns; yields an Event per step, stopping when any key's stream stops. Constant values are held; sub-patterns advance one value per event.

clausters.seq.timeline

Static timelines and a playhead (random-access sequencing).

The counterpart to the generative layer (clausters.base.stream.Routine, clausters.seq.pattern.Pbind). A Routine is a forward-only generator: its musical state lives in the generator's locals, so it cannot be seeked. A Timeline is the opposite — a static, editable list of timed items kept sorted by beat, with random access by time (index_at, range). That is what makes DAW-style transport controls possible: a Playhead scans the timeline forward as the clock advances, and play / stop / locate / loop re-seek the cursor by time at the boundaries.

An item is anything that can render itself on a destination — it has a play(destination) method. clausters.seq.event.Event already is one (it plays a note on a Server or a MidiServer — the same double dispatch the patterns use), so a timeline of Events renders to OSC or MIDI by which destination the playhead holds, exactly like the rest of the client. OscEvent and MidiEvent wrap a raw OSC message or MIDI bytes, so a timeline can also be a plain, editable OSC/MIDI score.

This layer is client-side: each playhead has its own local transport over its own timeline, and several clients phase-align through quant and the shared /transport grid (see the timing docs). A server-broadcast transport (one conductor's play/stop/locate driving every client) can layer on top later without changing this.

OscEvent

class OscEvent()

A raw OSC message (addr, *args) as a timeline item: rendering it sends the message at the playhead's current logical beat through a Server.

MidiEvent

class MidiEvent()

Raw MIDI bytes as a timeline item: rendering it emits the message at the playhead's current logical beat through a MidiServer.

Timeline

class Timeline()

A static, editable sequence of (beat, item) kept sorted by beat, with random access by time.

The structure a Playhead plays and a transport seeks. Items are kept in beat order (a stable insert preserves the order of items added at the same beat, e.g. a note-off before a re-trigger). Edit it freely — add, remove, move, clear — and read ranges of it by time — index_at, range, at.

add returns a handle (an opaque entry) you pass back to remove/move, so edits stay correct as other inserts shift indices.

Arguments:

  • items - optional iterable of (beat, item) to seed the timeline.

Timeline.add

def add(beat, item)

Insert item at beat (kept sorted); returns an entry handle.

Timeline.remove

def remove(entry)

Remove an entry returned by add (by identity).

Timeline.move

def move(entry, new_beat)

Move an entry to new_beat, keeping the timeline sorted.

Timeline.clear

def clear()

Drop every item.

Timeline.quantize

def quantize(grid)

Snap every placement to the nearest multiple of grid (beats): each entry's beat moves to the grid line, durations untouched. The data-side counterpart of the piano-roll's q gesture (which quantizes in the view when the GUI runs standalone). A zero/negative grid is a no-op. Returns the timeline.

Timeline.index_at

def index_at(beat) -> int

The cursor (index) of the first item at or after beat — the seek primitive a playhead uses to start or locate.

Timeline.range

def range(t0, t1) -> list

The (beat, item) pairs in the half-open beat window [t0, t1).

Timeline.at

def at(beat) -> list

The items exactly at beat.

Timeline.duration

def duration() -> float

The beat of the last item (0.0 when empty) — the timeline's length.

Timeline.from_pattern

@classmethod
def from_pattern(cls, pattern, dur=None, tempo: float = 1.0) -> "Timeline"

Bounce an event pattern (a Pbind) into a static timeline by running it offline and recording each event at its logical beat. dur bounds an open-ended pattern (beats); None drains a finite one fully.

Playhead

class Playhead()

A transport over a Timeline: play / stop / locate / loop, and a song position.

The playhead scans the timeline forward as a clock advances, rendering each item on a destination (a Server for OSC, a MidiServer for MIDI — the same seam as the rest of the client). The forward scan is what play runs; the random access lives at the boundaries — play(at=…) and locate(beat) re-seek the cursor by time, which a forward-only routine could never do.

Timing rides the clock's logical time like everything else, so a playhead inherits quant (start on a bar), lock_to (sample-exact) and join_transport (the shared grid) for free.

Arguments:

  • timeline - the Timeline to play.
  • clock - the TempoClock that drives it (start it for live playback; render it for offline).
  • destination - where items go — a Server or a MidiServer.

Playhead.play

def play(at: float = 0.0, quant=None)

Start (or restart) playback from beat at, snapping the start to a quant beat boundary of the clock's grid (a bar). Re-seeks the cursor to at by time, so it works as a locate-and-play.

Playhead.stop

def stop()

Halt the playhead. Items already rendered keep sounding (their releases are scheduled); no further items are played.

Playhead.locate

def locate(beat: float)

Seek the playhead to beat. While playing, restarts the scan from there (random access); while stopped, just sets where the next play begins.

Playhead.loop

def loop(start: float, end: float)

Loop the half-open beat window [start, end): when the scan reaches end it wraps back to start. Set before or during play.

Playhead.unloop

def unloop()

Stop looping; the scan plays through to the end.

Playhead.position

def position() -> float

The current song position, in beats. Interpolated from the clock between items while playing; the start/last-seek beat while stopped.

Playhead.follow_transport

def follow_transport(server, recv=None, quant=None)

Make this playhead obey a server's shared transport: when a conductor calls transport_play / transport_stop / transport_locate on the server, the server broadcasts the new state and this playhead rolls / halts / seeks to match — so several clients run in lockstep on the shared grid.

It registers /notify (so the server's /transport.reply pushes arrive) and an clausters.responders.OscFunc on /transport.reply that drives this playhead, then applies the current state once. Pass a started clausters.base.OscReceiver as recv (it must be subscribable on its own socket); one is created if omitted. quant snaps each rolling start to a beat boundary of the shared grid, so all followers land together. Release with unfollow_transport. Returns self.

Beat-aligned in plain wall-clock mode; sample-exact when the clock is also lock_to the server (see the timing docs).

Playhead.unfollow_transport

def unfollow_transport()

Stop following a server transport (see follow_transport): frees the responder and closes the receiver it created. Returns self.

clausters.defs.signals

Faust Signal API as composable, lowercase callables.

The user-facing way to build a FaustDef. Each function here is a small lowercase callable (a design choice that keeps graph-building fluent in Python) that returns a Signal; composing signals with Python operators or these functions builds the JSON signal tree the server's /d_faust consumes ({"signals": [ <node>, … ]}, one node per output — see the server's faust::signals). The same lowercase pattern will return UGen-graph nodes for SynthDefs later.

A Signal is an AbstractObject, so hslider("freq", …).sin() * 0.2 and sin(x) * 0.2 both compose the graph. Plain numbers are constants (Faust int/real); explicit feedback uses recursion/self_ (one sample of delay), and input(n) reads audio input n.

Reserved controls in and out (set with /s_new … "in" b "out" b) choose the input/output buses; they are added by the server, not declared here.

Signal

class Signal(AbstractObject)

One node of a Faust signal graph (one output). Wrap a number to make a constant; compose with operators or the module functions.

signal

def signal(x) -> Signal

Coerces a number or Signal into a Signal.

input

def input(index: int = 0) -> Signal

Audio input index (Faust CsigInput).

self_

def self_() -> Signal

The one-sample-delayed output of the enclosing recursion.

recursion

def recursion(body) -> Signal

Single feedback: body is a signal that may reference self_.

rec

def rec(fn) -> Signal

Pythonic feedback: fn(s) builds the body from its own delayed output s (sugar over recursion/self_).

delay

def delay(x, n) -> Signal

x delayed by n samples (Faust CsigDelay).

fconst

def fconst(ctype, name, file="") -> Signal

A foreign constant: a scalar the server resolves once, at def-compile time, from its runtime (Faust CsigFConst). ctype is "int" or "real", name the runtime symbol, file the include that declares it. The building block of sr -- prefer that helper for sample rate.

fvar

def fvar(ctype, name, file="") -> Signal

A foreign variable: like fconst but re-read each block (Faust CsigFVar).

sr

def sr() -> Signal

The engine's sample rate as a Signal, read from the server at def-compile time -- the port of Faust's ma.SR.

Use this instead of baking a Python SR constant: a def built with sr is correct at whatever rate the server (or NRT renderer) actually runs, e.g. when normalizing a frequency (freq / sr()) or cooking filter coefficients. It reproduces ma.SR exactly, including the stdlib's [1, 192000] clamp around the raw fSamplingFreq constant.

abs

noqa: A001 — Signal API name, by design

min

noqa: A001

max

noqa: A001

pow

noqa: A001

TAU

2*PI; Faust has no ma.TAU, this is just the literal

clausters.defs.boxes

Faust Box API as composable, lowercase callables.

The box counterpart of clausters.defs.signals, and a complete def-building API in its own right: each function returns a Box and composing boxes builds the JSON box tree the server's /d_faust consumes (see the server's faust::boxes for the schema). Boxes are Faust's point-free algebra — seq/par/split/merge/rec compose whole processors by their input/output arities. Where signals describes one output at a time referentially (input(n)), boxes describe multi-channel blocks that plug into each other — the natural shape for routing, chains, and anything conceived as units with inputs and outputs.

On top of the algebra, faust compiles any Faust expression into a Box that composes like a primitive. That addition puts the whole Faust library ecosystem (os.osc, fi.lowpass, re., pm., ...) inside the same algebra without transcribing anything: library functions become boxes among boxes.

Choosing a form: a fixed processing chain written top to bottom often reads best as plain Faust (FaustDef.from_source); graphs assembled one output at a time from arithmetic and feedback suit clausters.defs.signals. Regular banks ("N copies with index-dependent parameters") are best written in Faust itself — par(i, N, ...), widget labels with %i, ba.take — and parametrized from Python by splicing N and lists through faust's eval arguments. Boxes shine when the graph is conceived as composed processors, when its structure is decided by Python data, and whenever library DSP has to mix with Python-built pieces.

Two stages of application, kept separate on purpose:

  • faust("fi.lowpass", 3) — arguments to faust are evaluation-stage: spliced into the Faust source text (fi.lowpass(3)), where structural parameters like a filter order must live.
  • fi_lp(cutoff, wire()) — arguments to a Box call are composition-stage: boxes wired as the box's signal inputs, sugar for seq(par(cutoff, wire()), fi_lp).

The wire rule (the big difference from signals): each wire is a distinct input. There is no referential input(n) here — two wires in two positions are two input channels. Reusing the same wire() (or cut()) object in more than one position is almost always a mistake, and FaustDef.from_box rejects it; route explicitly with split, or write that stretch inside a faust fragment (_ <: ...). Every other box value can be reused freely: a repeated subexpression is computed once (the server shares identical subtrees).

Reserved controls in and out (set with /s_new ... "in" b "out" b) choose the input/output buses; they are added by the server, not declared here.

Box

class Box(AbstractObject)

One node of a Faust box expression. Wrap a number to make a constant; compose with operators, the module functions, or by calling the box.

num_inputs/num_outputs are the box's signal arity as computed on the client from the composition rules; None when unknown (a faust fragment without ins=/outs=). The server does not read them — a real mismatch is reported by Faust itself when the def compiles.

Box.__call__

def __call__(*args) -> "Box"

Applies boxes to this box's inputs: f(a, b) is seq(par(a, b), f) (with one argument, seq(a, f)) — Faust's partial-application style written as a call. The arguments must cover all the box's inputs; use wire for the ones left open.

Box.__getitem__

def __getitem__(index) -> "Box"

Selects one output channel: st[0] is seq(st, par(wire, cut, ...)). Needs a known num_outputs (pass outs= to faust for fragments). The selected fragment is shared, not recomputed, when several channels of the same box value are used.

Box.outs

def outs() -> tuple

All output channels as a tuple: l, r = st.outs().

box

def box(x) -> Box

Coerces a number or Box into a Box (numbers are constants: Python int -> Faust int, float -> real).

wire

def wire() -> Box

The identity box _: one open signal input. Every call is a new, distinct input — reusing one wire object in two positions is an error (see the module docs for the rule and the escapes).

cut

def cut() -> Box

The ! box: swallows one signal. Like wire, each call is a new, distinct position.

seq

def seq(*items) -> Box

Sequential composition a : b : ... (needs at least 2).

par

def par(*items) -> Box

Parallel composition a , b , ... (needs at least 2).

split

def split(*items) -> Box

Split composition a <: b (needs at least 2).

merge

def merge(*items) -> Box

Merge composition a :> b — excess outputs are summed (needs at least 2).

rec

def rec(a, b) -> Box

Recursive composition a ~ b: b feeds a's first inputs back from a's first outputs, with one implicit sample of delay. Point-free — for the rec(lambda s: ...) style, build the loop in a faust fragment or with clausters.defs.signals instead.

faust

def faust(src: str,
          *eval_args,
          defs: str = "",
          ins: int = None,
          outs: int = None) -> Box

A Faust expression compiled into a box — the door to the Faust libraries (stdfaust.lib is imported for you). The resulting box is indistinguishable from a primitive: compose it, call it, do arithmetic on it.

eval_args are evaluation-stage arguments, spliced into the source text as Faust application — faust("fi.lowpass", 3) compiles fi.lowpass(3). That is where structural parameters (a filter order, a table size, a list of coefficients) must go; they cannot travel as signals. Formatting: int/float as literals, a list/tuple as a Faust list (a, b, c), a string verbatim (for expressions or library functions passed as arguments). Signal inputs are then wired by calling the box:

lp = faust("fi.lowpass", 3)      # fi.lowpass(3): inputs (fc, x)
y = lp(cutoff, wire())

defs prepends auxiliary Faust definitions (helper functions, pattern matching) to the generated program. ins/outs declare the fragment's signal arity — only the Faust compiler knows it, so pass outs= when you need channel selection (st[0] / .outs()); a wrong declaration is caught by Faust when the def compiles.

Each distinct generated source is compiled (and cached) separately on the server; reusing one fragment value many times compiles and computes it once.

delay

def delay(x, n) -> Box

x delayed by n samples (Faust @).

delay1

def delay1(x) -> Box

One sample of delay (Faust '), sugar for delay(x, 1).

fconst

def fconst(ctype, name, file="") -> Box

A foreign constant: a scalar the server resolves once, at def-compile time, from its runtime. ctype is "int" or "real". The building block of sr -- prefer that helper for sample rate.

fvar

def fvar(ctype, name, file="") -> Box

A foreign variable: like fconst but re-read each block.

sr

def sr() -> Box

The engine's sample rate as a Box, read from the server at def-compile time -- the port of Faust's ma.SR, with the stdlib's [1, 192000] clamp. Use this instead of baking a Python constant so the def is correct at whatever rate the engine or NRT renderer runs.

abs

noqa: A001 — box schema name, by design

round

noqa: A001

min

noqa: A001

max

noqa: A001

pow

noqa: A001

TAU

2*PI

waveform

def waveform(values) -> Box

A fixed table; outputs the (size, content) pair, ready to stand in for rdtable/rwtable's leading (size, init) boxes.

rdtable

def rdtable(*args) -> Box

rdtable(size, init, ridx) — or rdtable(wf, ridx) with a waveform standing in for (size, init).

rwtable

def rwtable(*args) -> Box

rwtable(size, init, widx, wsig, ridx) — or the 4-argument form with a waveform up front.

check_wires

def check_wires(node)

Rejects a tree where the same wire/cut object appears in more than one position. Each wire is a distinct input in the box algebra; reusing one object almost always means the graph silently reads more bus channels than intended. Duplicating any other box value is fine (shared subtrees are computed once).

clausters.defs.ugens

UGen graph as composable, lowercase callables (port of the UGen side of sc3/synth, adapted to Clausters' server format).

The UGen-graph counterpart of clausters.defs.signals: each function here is a small lowercase callable that returns a Ugen node (one output); composing nodes with Python operators or these functions builds the graph a SynthDef serializes into the JSON SynthDefSpec the server's /d_recv consumes ({"controls": […], "ugens": […]} — see the server's synthdef module).

Instance-based, no global build context. Unlike sclang — where SynthDef build relies on a thread-global "current graph" that every UGen.new mutates (UGen.buildSynthDef) — the graph here is the tree of composed objects: a Ugen's inputs hold its operands directly, and the SynthDef walks that tree to emit the spec. Nothing is global, so several defs can be built concurrently.

The server's UGen set is deliberately focused: oscillators/sources (sin_osc, impulse, white_noise, rand), bus and buffer I/O (in_/in_ctl, out/replace_out, play_buf/buf_rd), feedback (local_in/local_out), the env_gen envelope, the lag/ var_lag smoothers, the demand pair (dseq/demand), and the fused mul_add/sum3/sum4. Maths works: + - * / map to the Add/Sub/Mul/Div kinds and every other operator or method (%, min/max, comparisons, .sin(), .midicps(), .distort() …) composes a generic BinaryOpUGen/UnaryOpUGen carrying the operator name — the same op the value side computes, so the two agree bit-for-bit. Reach for a Faust def (clausters.defs.signals) only for genuinely custom per-sample DSP (recursion, tables, sample-accurate feedback).

Each UGen output carries a rate (ir/kr/ar/dr); it defaults per kind and can be set with Ugen.at_rate. Controls carry a type and an optional lag — see control/Control.

Envelopes are the Env breakpoint builder plus the env_gen callable, which serialize to the EnvGen UGen's flat input list.

Reserved controls in and out (the input/output buses, set with /s_new … "in" b "out" b) are added by the server, not declared here.

Ugen

class Ugen(_Node)

One UGen node (one output). kind is a server UGen name; inputs is a list of operands, each a Ugen, a Control, or a plain number (a constant). Build them with the lowercase callables below rather than directly.

rate is the optional output calculation rate ("ir"/"kr"/ "ar"/"dr"); None lets the server pick the kind's default (ar for signal UGens). Set it fluently with at_rate. op is the operator name carried by the generic BinaryOpUGen/UnaryOpUGen (S3), e.g. "mul" / "midicps"; None for every other kind. label is the string tag the side-effect UGens carry — send_reply's command name and poll's label; None for every other kind. static is a dict of any other non-signal fields (the spectral UGens' fft_size/hop/ wintype); it merges verbatim into the serialized UGen spec.

Ugen.at_rate

def at_rate(rate: str) -> "Ugen"

Set this UGen's output rate ("ir"/"kr"/"ar"/"dr") and return it, e.g. sin_osc(5.0).at_rate("kr") for a control-rate LFO.

Control

class Control(_Node)

A named control (a /s_new//n_set parameter) with a default and an optional type and lag (S2), mirroring the server's control types:

  • rate="tr" — a trigger: a /n_set holds for one block, then the server resets it to 0 (drives an env_gen gate, a sample-and-hold).
  • rate="ir" — a scalar: read once at init and frozen; a later /n_set is ignored. As ir it may feed an ir input (rand, buffer-info UGens).
  • lag (seconds) — smooth a kr control's changes with an implicit one-pole (a lag/var_lag UGen the server inserts); lag_down gives a separate downward time.

Used as a UGen input it serializes to a {"control": index} reference; the SynthDef gathers the controls a graph references, in first-seen order.

control

def control(name, default=0.0, rate=None, lag=None, lag_down=None) -> Control

A named control (/s_new//n_set parameter). rate is its type ("tr" trigger, "ir" scalar, or the default kr); lag (with an optional lag_down) smooths a kr control. See Control.

sin_osc

def sin_osc(freq=440.0) -> Ugen

Sine by f64 phase accumulation, starting at phase 0.

impulse

def impulse(freq=1.0) -> Ugen

A single-sample 1.0 every freq Hz, 0.0 between (freq 0 = one impulse then silence). The first sample is always an impulse.

white_noise

def white_noise() -> Ugen

Uniform white noise in ±1.

in_

def in_(bus=0.0) -> Ugen

Reads an audio bus (sampled per block).

in_ctl

def in_ctl(bus=0.0) -> Ugen

Reads a control-bus value, constant over the block.

out_ctl

def out_ctl(bus, signal) -> Ugen

Writes signal's latest per-block value to a control bus — the write side of in_ctl, so a node reading that bus (via /n_map or in_ctl) tracks it. Passes signal through as its output.

out

def out(bus, signal) -> Ugen

Sums signal into the audio bus (output happens only here).

replace_out

def replace_out(bus, signal) -> Ugen

Overwrites the audio bus with signal instead of summing.

send_trig

def send_trig(trig, id=0, value=0.0) -> Ugen

On each trigger of trig, sends /tr nodeID id value to /notify clients. Output is silence; pass it as a SynthDef root.

send_reply

def send_reply(trig, *values, cmd="/reply", reply_id=-1) -> Ugen

On each trigger of trig, sends the OSC message cmd nodeID reply_id value… to /notify clients (cmd defaults to /reply). values is the arbitrary-arity payload. Output is silence; pass it as a SynthDef root.

poll

def poll(trig, signal, label="poll", trig_id=-1) -> Ugen

On each trigger of trig, posts label: value (the signal value) to the server console and, when trig_id >= 0, also sends /tr nodeID trig_id value. signal passes through the output, so poll can sit mid-chain.

fft

def fft(source, active=1.0, *, fft_size=1024, hop=0.5, wintype=0) -> Ugen

Opens a spectral chain: windows source (an audio signal) and transforms it to a spectral frame once per hop. active > 0 runs the transform, <= 0 holds. fft_size is the window size (a power of two: 256/512/1024/2048/4096), hop the fraction of the window between frames, wintype the window (a clausters._native.Window: 0 Hann, 1 sine, …). These size the transform, so they are static fields given only here — the server propagates them to the rest of the chain. The window is also settable live with Server.u_cmd. Feed the result to a pv_* filter or ifft.

ifft

def ifft(chain) -> Ugen

Closes a spectral chain: inverse-transforms each fresh frame and overlap-adds it back to audio (window-normalized, so a bare fft->ifft reconstructs at unity gain, delayed by one window). chain is the output of an fft or a pv_* filter.

pv_mag_above

def pv_mag_above(chain, threshold) -> Ugen

Passes only the bins whose magnitude is above threshold, zeroing the rest. chain comes from fft or another pv_*.

pv_mag_below

def pv_mag_below(chain, threshold) -> Ugen

Passes only the bins whose magnitude is below threshold.

pv_brick_wall

def pv_brick_wall(chain, wipe) -> Ugen

Brick-wall band limit: wipe > 0 zeroes the top fraction of bins (a low pass), wipe < 0 the bottom (a high pass), 0 passes everything (wipe in -1..1).

play_buf

def play_buf(bufnum, chan=0.0, rate=1.0, loop=0.0) -> Ugen

Mono buffer player with linear interpolation; rate is frames per output sample (1.0 = server rate).

buf_rd

def buf_rd(bufnum, chan, phase, loop=0.0) -> Ugen

Reads a buffer at a phase signal in frames (linear interpolation).

local_in

def local_in(channel=0.0) -> Ugen

Reads synth-private feedback channel channel (a constant); pairs with local_out for one-block feedback. LocalIn must precede its LocalOut — the SynthDef's topological order does that as long as the output graph reaches the local_in before the local_out.

local_out

def local_out(channel, signal) -> Ugen

Writes signal into synth-private feedback channel channel (a constant); also passes signal through as its output (so it can be a SynthDef output to keep the write in the graph).

mul_add

def mul_add(a, b, c) -> Ugen

a*b + c in one UGen (the multiply-accumulate the server fuses). The plain expression a * b + c builds the same value with two op UGens; this is the fused equivalent.

sum3

def sum3(a, b, c) -> Ugen

a + b + c in one UGen.

sum4

def sum4(a, b, c, d) -> Ugen

a + b + c + d in one UGen.

lag

def lag(signal, time=0.1) -> Ugen

One-pole smoother: signal lagged over time seconds (symmetric); time 0 passes through. The same UGen the server inserts for a lagged control -- use it directly to smooth any signal.

var_lag

def var_lag(signal, up=0.1, down=0.1) -> Ugen

One-pole smoother with separate rise (up) and fall (down) times.

sample_rate

def sample_rate() -> Ugen

The engine sample rate in Hz, computed once at init (ir).

buf_frames

def buf_frames(bufnum) -> Ugen

The number of frames in a buffer, block-constant (kr).

rand

def rand(lo=0.0, hi=1.0) -> Ugen

One uniform random value in [lo, hi), drawn once at synth init and held for the node's life (ir); lo/hi must be constants or ir.

dseq

def dseq(values, repeats=0.0) -> Ugen

A demand-rate sequence source: yields values in order, repeats times (0 loops forever), then signals end-of-stream. Only valid as a demand source.

demand

def demand(trig, reset, source) -> Ugen

Demand driver: pulls the next value from a demand source (a dseq) on each rising edge of trig and holds it between triggers; a rising reset restarts the source.

DoneAction

class DoneAction()

The action env_gen takes when its envelope finishes — scsynth's full done-action set (0-15). Pass one as done_action. The relative actions (3-13, 15) act on the synth's neighbours in its group; a paused node is resumed with Server.run (/n_run).

NONE

Do nothing; the envelope just holds its final level.

PAUSE_SELF

Pause the synth (stops processing; it stays in the tree). Resume with Server.run.

FREE_SELF

Free the synth — the usual choice for a one-shot or a released note.

FREE_SELF_AND_PREV

Free the synth and the preceding node.

FREE_SELF_AND_NEXT

Free the synth and the following node.

FREE_SELF_AND_FREE_ALL_IN_PREV

Free the synth; if the preceding node is a group, free all its children.

FREE_SELF_AND_FREE_ALL_IN_NEXT

Free the synth; if the following node is a group, free all its children.

FREE_SELF_TO_HEAD

Free the synth and every preceding node in its group.

FREE_SELF_TO_TAIL

Free the synth and every following node in its group.

FREE_SELF_PAUSE_PREV

Free the synth and pause the preceding node.

FREE_SELF_PAUSE_NEXT

Free the synth and pause the following node.

FREE_SELF_AND_DEEP_FREE_PREV

Free the synth; if the preceding node is a group, deep-free it.

FREE_SELF_AND_DEEP_FREE_NEXT

Free the synth; if the following node is a group, deep-free it.

FREE_ALL_IN_GROUP

Free the synth and every other node in its group.

FREE_GROUP

Free the synth's whole enclosing group.

FREE_SELF_RESUME_NEXT

Free the synth and resume (unpause) the following node.

Env

class Env()

A breakpoint envelope: levels (one more than times), the segment times in seconds, and a curve per segment (a shape name, a numeric curvature, or a list of either, one per segment).

release_node is the index into levels where the envelope sustains while the gate is held (None = no sustain, plays straight through). Feed it to env_gen. Modelled on SuperCollider's Env; the shapes match the server's EnvGen.

Env.perc

@classmethod
def perc(cls, attack=0.01, release=1.0, level=1.0, curve=-4.0)

A fixed-duration percussive hit: 0 -> level -> 0. No sustain, so a rising gate triggers the whole thing.

Env.adsr

@classmethod
def adsr(cls,
         attack=0.01,
         decay=0.3,
         sustain=0.5,
         release=1.0,
         peak=1.0,
         curve=-4.0)

The classic attack/decay/sustain/release. Sustains at peak * sustain (the release node) until the gate falls.

Env.asr

@classmethod
def asr(cls, attack=0.01, sustain=1.0, release=1.0, curve=-4.0)

Attack to sustain, hold there until release, then fall to 0.

Env.step

@classmethod
def step(cls, levels, times, release_node=None, loop_node=None)

A step sequence: each value held for its durationlevels and times have the same length, unlike the raw constructor (Env.step([0, 1], [0.5, 0.5]) holds 0 for 0.5, then 1 for 0.5).

This is the conceptual interface of a value-with-duration sequence; like SuperCollider's Env.step, it is realized over the raw initial-level + (target, duration) form by prepending the first level with the "step" shape (which jumps to each segment's target at its start).

Env.to_inputs

def to_inputs()

The envelope as the flat number list env_gen appends after its fixed inputs: initLevel, numSegments, releaseNode, loopNode then target, duration, shape, curve per segment.

env_gen

def env_gen(env: Env,
            gate=1.0,
            level_scale=1.0,
            level_bias=0.0,
            time_scale=1.0,
            done_action=DoneAction.NONE) -> Ugen

Plays an Env. A rising gate (re)triggers from the start; while the gate is held the envelope sustains at the env's release node; when the gate falls it plays the release segments. level_scale/level_bias affine the output, time_scale stretches every segment. done_action is taken when the envelope finishes (see DoneAction).

env_to_points

def env_to_points(env, *, time_at: float = 0.0) -> list

An Env (levels / segment times / curves) as the flat bpf breakpoint list [t, v, shape, curve, ...], with absolute times starting at time_at. The last point carries a linear placeholder (no segment leaves it). Feed the result to the bpf widget or to a live points set.

points_to_env

def points_to_env(points, *, time_at: float = 0.0, **env_kwargs)

A bpf breakpoint list — the flat t v shape curve ... quads a "points" event carries — as an Env: absolute times become segment durations and each segment keeps its shape (the numeric curvature for the custom shape, the shape name otherwise).

A first breakpoint later than time_at (default 0.0) is a drawn initial delay, realized as a leading hold segment (the first level held for that duration) so what was drawn and what plays stay identical. Extra keywords (release_node, loop_node) pass through to Env.

clausters.defs.faustdef

FaustDef: a named Faust definition ready for /d_faust.

Wraps a graph built with clausters.defs.signals (the signal tree form), one built with clausters.defs.boxes (the box tree form — a Box, or a raw dict for machine-generated trees), or a Faust source string: the three payloads the server's /d_faust accepts, on equal footing (it sniffs which by the first byte; see the server's faust module). They are three ways of writing Faust, not a main road and two detours — pick the one that says what you mean. Sending and instantiating is the Server's job; this only builds the payload and exposes the declared control names (UI labels), plus the reserved in/out bus controls the server adds.

FaustDef

class FaustDef()

FaustDef.from_signals

@classmethod
def from_signals(cls, name: str, *outputs) -> "FaustDef"

One output per argument (Signal or number).

FaustDef.from_box

@classmethod
def from_box(cls, name: str, box) -> "FaustDef"

From a clausters.defs.boxes.Box (or a raw box-tree dict, kept for machine-generated graphs). A Box is checked for the one silent mistake the box algebra allows: reusing the same wire()/cut() object in two positions (each wire is a distinct input).

FaustDef.dump_def

def dump_def() -> str

The def serialized to text -- the /d_faust <name> <payload> wire payload: a JSON signal/box tree, or the Faust source string verbatim. Useful to inspect the built graph before sending it.

FaustDef.control_names

def control_names() -> list[str]

The control names this def declares (UI labels), in tree order. The reserved in/out bus controls (added by the server) are not included; see reserved.

reserved

bus-selecting controls every Faust synth also accepts.

clausters.defs.synthdef

SynthDef: a named UGen graph ready for /d_recv (port of the SynthDef side of sc3/synth, adapted to Clausters' JSON SynthDefSpec).

The UGen-graph counterpart of FaustDef: it wraps one or more output Ugen nodes (built with the lowercase callables in clausters.defs.ugens), walks the graph and serializes the {"name", "controls", "ugens"} JSON the server compiles.

from clausters.defs import SynthDef, control, sin_osc, out

freq = control("freq", 440.0)
amp = control("amp", 0.2)
sig = sin_osc(freq) * amp
sdef = SynthDef("beep", out(0.0, sig), out(1.0, sig))   # stereo
server.add_synthdef(sdef)                                # /d_recv

Instance-based build (no globals). The walk is a plain post-order traversal of the output nodes: a UGen is emitted only after its inputs, so the ugens list is topologically ordered (every {"ugen": w} reference points at an earlier node, as the server requires) and shared sub-graphs are emitted once (dedup by object identity). Controls are gathered in first-seen order; reusing the same name with a different default is an error. No thread-global build context is touched, so defs build concurrently.

SynthDef

class SynthDef()

A named UGen graph. Pass the graph's root UGens — normally the outputs (out(...) / replace_out(...), and any local_out(...) to keep feedback writes in the graph), but a root can equally be a side-effect UGen with no audio output (send_trig(...) / send_reply(...) / poll(...)): a def may consist only of those and no out at all. Every root must be a UGen; a def needs at least one (the server rejects an empty graph). A def with no output UGen is simply silent on the server.

SynthDef.spec

def spec() -> dict

The SynthDefSpec dict the server's /d_recv compiles.

SynthDef.dump_def

def dump_def() -> str

The def serialized to text -- the /d_recv wire payload, the JSON SynthDefSpec (see spec). Useful to inspect the built graph before sending it.

SynthDef.control_names

def control_names() -> list[str]

The control names this def declares, in spec order (parallels FaustDef.control_names).

clausters.defs.graphdef

GraphDef: a named node-graph "program" ready for /d_graph.

Where SynthDef and FaustDef each describe a single synthesis node, a GraphDef describes a whole configuration of member nodes wired by buses — an effect chain, a mixer, a layered instrument — that the server stores and instantiates as one unit. It exposes a named parameter surface: ports that map to inner member controls, so the running instance is driven through the port names, never the private member node ids (the same encapsulation a composite SynthDef would give).

It is a thin JSON builder, like the other two def kinds: it composes a spec and hands it to server.add_graphdef (/d_graph). The server resolves the member def names (SynthDef or FaustDef, identically), allocates the instance's private buses and wires them.

from clausters.defs import GraphDef

g = GraphDef("chain")
mix = g.bus("mix")                          # a private internal audio bus
src = g.add("gsrc", out=mix, level=1.0)     # a member; `out` control -> the bus
g.add("gsink", {"in": mix, "out": "OUT"})   # `in` reads `mix`, `out` -> hardware
g.port("gain", src["level"], default=0.5)   # surface port -> the source's level
server.add_graphdef(g)                       # /d_graph (blocks on /done in RT)

inst = server.graph("chain", {"gain": 0.8})  # /graph_new
server.set(inst, {"gain": 0.3})              # resolves against the surface
server.free(inst)                            # frees the group + its private buses

The reserved control name "OUT" wires a member's output to hardware bus 0; any other string value of a member control is the name of an internal bus.

GraphBusRef

class GraphBusRef()

A reference to an internal GraphDef bus, returned by GraphDef.bus. Used as a member control value (it serializes to the bus name).

_Target

class _Target()

One inner target of a surface port: a member's control with optional linear scaling (mul·v + add).

_Target.scaled

def scaled(mul: float = 1.0, add: float = 0.0) -> "_Target"

A copy of this target with linear scaling applied to incoming values, e.g. filt["cutoff"].scaled(7800, 200) maps a 0..1 port to 200..8000 Hz.

MemberRef

class MemberRef()

A handle to a member added with GraphDef.add. Index a control name (member["cutoff"] or member.cutoff) to get a surface _Target.

GraphDef

class GraphDef()

A named node graph. Build it with bus, add and port, then send it with server.add_graphdef.

GraphDef.bus

def bus(name: str, *, rate: str = "audio", channels: int = 1) -> GraphBusRef

Declares a private internal bus (rate "audio" or "control"). Each instance allocates its own, so two instances never collide.

GraphDef.add

def add(defname: str,
        controls: dict | None = None,
        *,
        maps: dict | None = None,
        voice: bool = False,
        **control_kw) -> MemberRef

Adds a member: an instance of the SynthDef/FaustDef defname. Control values may be numbers, a GraphBusRef (to wire the control to an internal bus), or "OUT" (hardware bus 0). maps binds controls to internal control buses via /n_map. Pass controls as a dict (needed for reserved names like in) and/or as keywords. voice=True marks a per-voice member: instantiated once per Server.graph_voice (or MIDI note) instead of at instantiation — the per-note part of a polyphonic instrument.

GraphDef.port

def port(name: str, *targets: _Target, default: float | None = None)

Defines a surface port mapping name to one or more member controls (each a _Target, optionally .scaled(...)). default is applied at instantiation unless overridden.

GraphDef.spec

def spec() -> dict

The GraphDefSpec dict the server's /d_graph validates.

GraphDef.dump_def

def dump_def() -> str

The def serialized to text -- the /d_graph wire payload, the JSON GraphDefSpec (see spec). Useful to inspect the composition before sending it.

clausters.defs.server

Server facade: the running Clausters server, its resources and comms.

This is the server-application side of the client: it owns the communication interface (RT over UDP by default; an OscNrtInterface for offline; shared-memory/embed would be further interfaces), the client-side resource allocators (node/bus/buffer), builds the OSC and handles the async replies (/done / /fail) and notifications.

Timing is not here and not in the clock-as-sender: the clock (clausters.base.clock.TempoClock) only schedules and tells time; the Server emits, reading the logical time from the clock of the routine in flight. A routine sequences events by calling send_bundle; swapping the Server's interface retargets every routine from live RT to an NRT score without touching clock or routine.

ServerOptions

@dataclass
class ServerOptions()

Client-owned server configuration, the way SuperCollider's ServerOptions works: it both sizes the client's bus allocators and emits the CLI flags to launch a matching server (args), so the two agree by construction. Verify a running server with Server.query_info.

outputs

Hardware output channels; None follows the device default (no flag).

inputs

Hardware input channels; 0 opens no input device.

max_nodes

Node slab capacity, root included.

max_buffers

Buffer pool size.

max_graph_children

Per-group child capacity.

max_ugen_inputs

Accepted inputs per UGen (clamped to 32 by the server).

taps

Audio-tap rings for oscilloscopes (0 disables the tap region).

tap_frames

Per-tap ring capacity in samples (rounded up to a power of two).

ServerOptions.args

def args() -> list[str]

The clausters CLI flags that launch a server matching these options (pass to subprocess after the binary path). outputs is emitted only when set (otherwise the server follows the device); the pre-allocated pool sizes are always emitted so the launched server and this object agree by construction.

ServerInfo

@dataclass
class ServerInfo()

The static configuration a running server reports over /server_info (read-only; the result of Server.query_info).

The first six fields are the stable original set; the rest are the boot-time capacities the server appends. channels is the hardware output channel count, input_channels the live input count (0 when the server was launched without --inputs). Against a pre-S7 server that reports only six fields, the appended ones fall back to the compiled defaults.

taps

Audio-tap region shape; 0/0 when the server has no segment (or predates taps).

max_frame

The stream-transport frame ceiling in bytes (--max-frame): the largest OSC frame a TCP/WebSocket client may send or receive, what bulk requests (Server.get_samples chunks) are sized from. Falls back to the UDP datagram cap against a server too old to report it.

Server

class Server()

Server.boot

@classmethod
def boot(cls,
         options: "ServerOptions | None" = None,
         *,
         shm="auto",
         transport: "str | None" = None,
         verbose: int = 0,
         data_dir=None,
         server_args=(),
         latency: "float | None" = None,
         ready_timeout: float = 10.0) -> "Server"

Start a separate clausters server process and return a Server connected to and owning it.

The launcher's ergonomic non-Session entry point: it spawns the standalone server (choosing a shared-memory segment), waits until it answers, and hands back a Server whose close also stops the process (and interpreter exit stops it too). Pair it with clausters.gui.GuiHost.boot for the GUI, or use clausters.Session.live for the bundled, clock-included path.

Arguments:

  • options - a ServerOptions sizing the launched server and this handle's allocators alike; None uses the server's defaults.
  • shm - the shared-memory segment — "auto" picks one, a path forces it, None launches without one. Remembered for a GUI to map.
  • transport - the carrier this handle talks over — "tcp" (default), "udp" or "ws" (a --ws server). The boot-or-attach probe itself always rides UDP.
  • verbose - server log verbosity (1/2/3 -> -v/-vv/ -vvv; negative -> -q).
  • data_dir - the server's --data-dir; None uses its default.
  • server_args - extra server CLI tokens (e.g. ["--tcp"]).
  • latency - seconds added to RT timetags (see the constructor).
  • ready_timeout - seconds to wait for the server to answer.

Returns:

A booted Server; server.shm is the segment path (or None).

Server.shm

@property
def shm() -> "str | None"

The shared-memory segment path of the server this handle boot-ed, or None (attached, or booted without a segment). A GUI maps this.

Server.send_msg

def send_msg(addr, *args)

Send one message immediately.

Server.send_bundle

def send_bundle(*messages, delay_beats: float = 0.0, clock=None)

Emit a timetagged bundle of (addr, *args) messages at the running routine's exact logical beat (+ optional lookahead). Call it from a routine playing on a clock (found via main.current_tt) or pass clock=. The timetag comes from the yield-accumulated beat, not from wall-clock now, so inter-event timing is exact; the interface decides the wire time (wall clock for RT, seconds-from-start for NRT).

Server.play_event

def play_event(event)

Realize a note Event as OSC: /s_new at the routine's logical beat, then /n_free (or gate 0) after the sustain. The OSC side of the double dispatch — a MIDI destination renders the same event as note on/off. Returns the synth node id (or None for a rest).

Server.request

def request(addr, *args, timeout: float = 5.0, expect=None)

Sends a message and returns the first matching reply (addr, args) (RT only; the interface must reply). expect filters reply addresses.

Server.query_info

def query_info(timeout: float = 5.0) -> ServerInfo

Asks the running server for its static configuration (RT only): bus counts, output/input channels, block size, sample rate and the boot-time pool sizes. Use it to size or check allocators against a server you did not launch; compare the result with options. The appended capacity fields degrade to the defaults against a server too old to report them.

Server.query_tree

def query_tree(group=ROOT_NODE_ID,
               *,
               controls: bool = True,
               timeout: float = 5.0) -> dict

The node tree from group down (scsynth /g_queryTree), as a nested dict: a group is {"id", "children": [...]}, a synth is {"id", "def", "controls": {name: value}} (controls only when controls=True). This is the structured way to read the tree — never scrape the server's logs.

Server.node_query

def node_query(node, timeout: float = 5.0) -> dict

Per-node detail (/n_query -> /n_info): id, parent, prev/next siblings, is_group; for a group head/tail; for a synth def, controls, maps (/n_map bindings) and the inferred reads/writes bus lists.

Server.dump_graph

def dump_graph(group=ROOT_NODE_ID, timeout: float = 5.0) -> str

The inferred bus graph of group as a human-readable string (/g_dumpGraph): what each child reads/writes and the current order. A debugging aid; for machine use prefer query_tree.

Server.add_faustdef

def add_faustdef(fdef: FaustDef,
                 *,
                 wait: bool = True,
                 timeout: float = 10.0) -> str

Sends a FaustDef via /d_faust.

/d_faust JIT-compiles asynchronously on the server's network thread (answered later by /done//fail). In RT, wait=True (the default) blocks until that reply -- raising CommandError on /fail or ReplyTimeout if it never lands; wait=False returns immediately (fire-and-forget), so use sync as a barrier before relying on the def (e.g. yield it from a routine, never block in one). In NRT it always scores /d_faust at time 0 -- the renderer compiles it before time advances -- so wait does not apply.

Server.add_synthdef

def add_synthdef(sdef, *, wait: bool = True, timeout: float = 10.0) -> str

Sends a UGen SynthDef via /d_recv. Like add_faustdef: wait=True (default) blocks in RT until /done//fail; wait=False is fire-and-forget (pair with sync). In NRT it scores /d_recv at time 0 so the renderer compiles it before time advances.

Server.add_graphdef

def add_graphdef(gdef, *, wait: bool = True, timeout: float = 10.0) -> str

Sends a GraphDef via /d_graph. Like add_synthdef/add_faustdef: wait=True (default) blocks in RT until /done//fail; wait=False is fire-and-forget (pair with sync). In NRT it scores /d_graph at time 0. Loading a GraphDef is cheap on the server (no JIT — it only validates and references the member defs), but it is still asynchronous, so the same barrier discipline applies.

Server.graph

def graph(defname,
          ports=None,
          *,
          target=ROOT_NODE_ID,
          action=AddAction.TAIL) -> Group

Instantiates a GraphDef (/graph_new) as a wired group, with ports (a {name: value} dict) overriding the def defaults. The returned Group is the instance: drive it through the surface with set (/n_set resolves names against the surface, not the private members) and tear it down with free (which also reclaims its private buses).

Server.graph_voice

def graph_voice(instance, ports=None) -> Group

Spawns a per-voice sub-graph (/graph_voice) inside a running GraphDef instance (a Group from graph), wired to its shared private buses. ports overrides the voice-port defaults. The returned group is the voice: drive it through its surface with set and free it with free.

Server.u_cmd

def u_cmd(node, ugen_index: int, name: str, *args)

Sends a typed command to one UGen instance inside a synth (/u_cmd nodeID ugenIndex name args…). The server hashes name to a stable selector and routes the numeric args to that UGen on the audio thread. The FFT chain uses it to swap a window live, e.g. server.u_cmd(synth, fft_index, "window", 4) for a Blackman window (a clausters._native.Window value); an unrecognized name is a no-op on the server.

Server.run

def run(node, flag: bool = True)

Pauses (flag=False) or resumes (flag=True) a node — a synth or a whole group — with /n_run. A paused node stays in the tree and keeps its state but is skipped (silent); this is what resumes a synth parked by DoneAction.PAUSE_SELF.

Server.pause

def pause(node)

Pauses a node (/n_run … 0). See run.

Server.resume

def resume(node)

Resumes a paused node (/n_run … 1). See run.

Server.stream_buses

def stream_buses(period_ms: int, *buses, timeout: float = 5.0)

Subscribes this client to a periodic /c_set snapshot of the given control buses (/c_stream): the server sends one snapshot immediately and then one every period_ms (floor 10 ms, at most 128 buses) with no further requests -- the network counterpart of reading the shared-memory segment, e.g. for meters over WebSocket. One subscription per client, replaced on each call; period_ms <= 0 (or no buses) cancels it. Receive the snapshots with an OscFunc on /c_set. Blocks on the /done ack.

Server.tap

def tap(tap: int, bus)

Routes audio bus into the server's audio-tap ring tap (/tap): from the next block on, the engine appends that bus's samples to the ring, where a GUI oscilloscope reads them out of shared memory with zero messages (or this client streams them with stream_taps). bus = -1 stops the tap. No ack, like /n_map (failures reply /fail); sequence with sync when needed. The server must have a tap region (--taps > 0, the default) -- check query_info.

Server.stream_taps

def stream_taps(period_ms: int, frames: int, *taps, timeout: float = 5.0)

Subscribes this client to a periodic /tap_data snapshot of the given audio taps (/tap_stream): every period_ms (floor 10 ms) the server sends, per tap, the newest frames samples of its ring as /tap_data tap endPosition blob -- the tap index, the tap's stream position (total samples written) at the window's end, and the window as raw little-endian float32. The network counterpart of reading the tap rings out of shared memory, e.g. for a browser oscilloscope or headless capture. frames is clamped to 8192 and to half the ring; at most 8 taps per subscription; one subscription per client, replaced on each call; period_ms <= 0 (or no taps) cancels. Receive the snapshots with an OscFunc on /tap_data. Blocks on the /done ack.

Server.alloc_buffer

def alloc_buffer(frames: int,
                 channels: int = 1,
                 *,
                 wait: bool = True,
                 timeout: float = 5.0) -> Buffer

Allocates a zeroed buffer. In NRT it scores /b_alloc at time 0 (so the renderer installs it before time advances); in RT wait=True (default) blocks on /done, wait=False is fire-and-forget.

Server.gen_buffer

def gen_buffer(buf, cmd: str, *args, wait: bool = True, timeout: float = 5.0)

Fills a buffer through /b_gen (the wavetable/generator commands: "env", "sine1"/"sine2"/"sine3", "cheby", "copy"). Like alloc_buffer: NRT scores at time 0; RT wait=True blocks on /done, wait=False is fire-and-forget.

Server.read_buffer

def read_buffer(path,
                *,
                file_start: int = 0,
                num_frames: int = 0,
                wait: bool = True,
                timeout: float = 5.0) -> Buffer

Allocate a buffer and read a sound file into it (/b_allocRead): the shape and sample rate come from the file (num_frames 0 = the whole file, from file_start). Decoding is by content (WAV, FLAC, OGG, MP3, …). In NRT it scores at time 0; in RT wait=True blocks on /done. The returned Buffer's frames/channels are unknown client-side until query_buffer.

Server.read_into

def read_into(buf,
              path,
              *,
              file_start: int = 0,
              num_frames: int = -1,
              buf_start: int = 0,
              wait: bool = True,
              timeout: float = 5.0)

Read a sound file into an existing buffer (/b_read), keeping its shape. NRT scores at time 0; RT wait=True blocks on /done.

Server.write_buffer

def write_buffer(buf,
                 path,
                 *,
                 sample_format: str = "int16",
                 num_frames: int = -1,
                 buf_start: int = 0,
                 wait: bool = True,
                 timeout: float = 5.0)

Write a buffer to a WAV file (/b_write); sample_format is "int16", "int24" or "float". NRT scores at time 0; RT wait=True blocks on /done.

Server.zero_buffer

def zero_buffer(buf, *, wait: bool = True, timeout: float = 5.0)

Zero a buffer (/b_zero). NRT scores at time 0; RT wait=True blocks on /done.

Server.query_buffer

def query_buffer(buf, timeout: float = 5.0) -> Buffer

Ask the running server for a buffer's shape (/b_query/b_info bufnum frames channels sampleRate) and fill it into the Buffer handle. RT only (it needs a reply).

Server.get_samples

def get_samples(buf,
                start: int = 0,
                count: int = -1,
                *,
                chunk: "int | None" = None,
                timeout: float = 5.0)

Fetch interleaved samples from a buffer (/b_getn/b_setn), in chunks, as a stdlib array('f'). count -1 = to the end (the shape is queried first). RT only (it needs replies); for display the GUI host fetches buffers itself.

chunk (samples per round-trip) defaults to the transport's bound: over a stream transport (TCP/WebSocket) it is sized from the frame ceiling the server advertises in /server_info — megabytes per reply — while over UDP each reply must fit a datagram, so it stays at 1024. Pass an explicit chunk to override either.

Server.render

def render(sample_rate: float = 48_000.0, channels: int = 2)

Renders the accumulated score (the interface must be an OscNrtInterface). Schedule a closing bundle (e.g. /n_free 0) so the render has a defined duration.

Server.sync

def sync(timeout: float = 5.0) -> int

The async barrier (scsynth /sync): sends /sync id and blocks until the server answers /synced id, which it does only once every async command sent earlier -- Faust/SynthDef compiles, buffer jobs -- has completed. Use it after a wait=False add_faustdef / add_synthdef / buffer alloc. RT only (in NRT the renderer already serializes async work at time 0). Returns the id used.

Blocking — never call from a routine. This (and any wait=True) blocks the calling thread on a reply: fine on your own thread, but it would freeze the clock thread if called from inside a routine generator (see Routine). It also polls the socket synchronously; a non-blocking, notification-driven barrier you can yield from a routine is future work (OSCFunc).

Server.sample_clock

def sample_clock(window: int = 64, timeout: float = 2.0)

A UdpSampleClock tracking this server's sample clock over UDP. Pass its .timebase() to a TempoClock to anchor timing to the server and schedule by /sched.

Server.transport

def transport(timeout: float = 5.0)

The server's shared transport grid (/transport) as (origin_sample, tempo), or None if none is set. The grid lets several clients phase-align on the master clock; join it from a clock with clausters.base.clock.TempoClock.join_transport. RT only.

Server.set_transport

def set_transport(origin_sample: int, tempo: float, timeout: float = 5.0)

Define the server's shared transport grid (/transport): beat 0 at origin_sample on the sample clock, advancing at tempo beats per second. One client (the conductor) sets it; the others join_transport. Last writer wins. Defining the grid resets the rolling state to stopped at position 0.

Server.transport_state

def transport_state(timeout: float = 5.0)

The full shared transport state as a dict {origin_sample, tempo, playing, position}, or None if no grid is defined. playing is whether the transport is rolling and position the song-position beat (where play starts, or where a stopped transport sits). A clausters.seq.timeline.Playhead follows this with follow_transport. RT only.

Server.transport_play

def transport_play(position: "float | None" = None, timeout: float = 5.0)

Start the shared transport rolling (/transport_play). With position playback starts from that song-position beat; without it, from where it last stopped or located. The server broadcasts the change to every /notify client, so all playheads following the transport roll together. Needs a grid defined (set_transport).

Server.transport_stop

def transport_stop(timeout: float = 5.0)

Stop the shared transport (/transport_stop); every following playhead halts. Broadcast to /notify clients.

Server.transport_locate

def transport_locate(position: float, timeout: float = 5.0)

Set the shared transport's song position (/transport_locate) — where play starts, or where it seeks to while playing. Every following playhead locates to it. Broadcast to /notify clients.

Server.close

def close()

Close the communication interface and, if this handle boot-ed a server process, stop it too.

clausters.defs.node

Nodes (synths and groups) and client-side id allocation.

The server's node tree (node): the root group is id 0; clients allocate positive ids. Add actions match the server: head/tail of a group, before/after a node, or replace. Synth and Group are flat handles holding an id; the Server does the OSC.

NodeIdAllocator

class NodeIdAllocator()

Hands out node ids from start (scsynth clients use 1000+).

clausters.defs.bus

Audio and control buses, with client-side allocation.

Mirrors the server's bus model (dsp): audio buses (0..channels are the hardware outputs) and single-float control buses. Like scsynth, the client owns allocation; the server just indexes. A Bus is a flat (index, channels, rate) — only flat data ever leaves for the wire.

The allocators carry no default size of their own: how many buses exist is a property of the server, not the bus module. The Server sizes them from its ServerOptions (which also emits the matching --audio-buses/--control-buses launch flags), and the live counts can be read back with query_info.

AudioBusAllocator

class AudioBusAllocator(_Allocator)

Allocates audio buses above the hardware outputs (reserved). size is the server's audio-bus count (from ServerOptions/query_info).

ControlBusAllocator

class ControlBusAllocator(_Allocator)

size is the server's control-bus count (from ServerOptions/query_info).

clausters.defs.clocksync

Track the server's sample clock over UDP.

To use a SampleClockTimebase over UDP — where the client can't read the sample counter directly (as shm/embed can) — it queries the server's /clock and models

sample(t_local) = a + b * t_local

from (local monotonic time, counter) anchor pairs, a least-squares line over a sliding window (JACK-DLL / Ableton-Link in spirit; same model as the server's examples/sample_clock.py). The TempoClock then paces against this and the Server schedules every event by absolute sample with /sched — drift-free.

Query latency does not accumulate: an anchor is paired with the midpoint of its round trip, whose half-width is a bounded uncertainty that only shifts the whole grid by a constant. Relative timing stays sample-exact by construction.

Why this is drift-free, and what can fail. Each event's target is an absolute sample recomputed from the routine's absolute logical beat — round((origin + beat / tempo) * sample_rate) — not stepped from the previous event, so error never accumulates: at 48 kHz the target stays integer-exact within float64 for hours. The fitted line above only sizes the lead (how far ahead the /sched is queued), never the event time, which the server's own counter resolves exactly. So the only failure mode is binary, not cumulative: a /sched that arrives after its target sample (lead < worst-case client + network + model jitter) lands late; one that arrives in time lands exact. A long run at perfect spacing just means the lead was never violated.

Surviving suspend. The server's counter counts samples actually emitted, so it freezes when the audio device suspends (system sleep, or the sink going idle) and resumes in place — it is not a wall clock. A /sched keyed to an absolute sample simply waits in the server's queue and fires when the counter reaches it, so the audio grid stays sample-exact across the gap (consecutive events keep their exact spacing; the freeze just drops out of the timeline). The tracker rides this automatically: while the counter is stalled its anchors fit a flat slope, so the predictor stops running ahead and the lead/backlog stays bounded; on resume the slope recovers. Only the wall-clock phase shifts, by the suspend duration — relative sample spacing is preserved.

SampleClockModel

class SampleClockModel()

sample(t) = a + b·t, least-squares over a sliding anchor window.

The fit itself lives in the native core (clausters._native.ClockSyncModel over clausters_core::clocksync), so every client predicts the same sample from the same anchors; this class only adds the local-time convenience (now reads the monotonic clock).

SampleClockModel.a

@property
def a() -> float

Fitted intercept (samples at local time 0).

SampleClockModel.b

@property
def b() -> float

Fitted slope (samples per local second).

SampleClockModel.now

def now() -> int

Predicted current value of the server's sample counter.

SampleClockModel.local_time_of

def local_time_of(sample: int) -> float

Inverse: the monotonic time the counter reaches sample.

UdpSampleClock

class UdpSampleClock()

Tracks a server's sample clock over UDP and yields a timebase.

Uses its own socket (so /clock round trips never contend with the Server's command socket). Build one with server.sample_clock().

UdpSampleClock.anchor

def anchor() -> float

One /clock round trip; returns the anchor's uncertainty (s).

UdpSampleClock.warmup

def warmup(n: int = 5, gap: float = 0.05) -> float

A few anchors to seed the model; returns the worst uncertainty.

UdpSampleClock.track

def track(interval: float = 0.5)

Re-anchor in the background forever (keeps the slope fresh).

UdpSampleClock.timebase

def timebase() -> SampleClockTimebase

A SampleClockTimebase reading this tracker's model.

clausters.form.element

The arrangement — elements and their temporal character.

The client-side layer under a multitrack editor of recursive granularity: it places elements in time, groups them recursively and renders them. An Element is an arbitrarily delimited entity that produces a unit of meaning and can be decomposed or combined — generated (the rendered thing, editable and random-access) or a generator (the algorithm that renders it, forward-only), with the change of state between them. It is a thin adornment over the objects the client already has (clausters.seq.Event, clausters.seq.Timeline, a Buffer, a Pattern, a def): it carries the temporal metadata (onset, duration, and the derived temporal character) and belongs to a Group, while it delegates playing to the wrapped item's play(destination) — the double-dispatch seam every leaf item in the client already shares. The arrangement does not reimplement or subclass those objects.

The five primitives map one-to-one onto what the client already has:

  • Eventevent/clip: parameters grouped into one action (internally simultaneous), with its own onset/duration. Wraps clausters.seq.Event.
  • SequenceList: strict order with no concrete time, only sequence. Wraps a Python list or a Pattern.
  • BufferBuffer: a list at constant time (audio or control samples). Wraps clausters.defs.Buffer.
  • TrackSet: mixed placement of elements, a DAW track. Wraps clausters.seq.Timeline.
  • GeneratorFunction: a generator element — server DSP (a def) or a sequence generator (Pbind/Routine).

Grouping and rendering live in clausters.form.group and clausters.form.render. This module is pure and transport-agnostic (factorable into clausters-core in a future port).

SEGMENT

The temporal character of an element, derived from which of onset and duration are present. segment has both; punctual has an onset but no duration; relative has a duration but no onset; abstract has neither (a pure context/container that only a parent gives concrete time).

temporal_character

def temporal_character(onset, duration) -> str

The temporal character for a given onset/duration pair (the pure rule behind Element.temporal_character).

Element

class Element()

Base of the arrangement: temporal metadata over a wrapped item.

An element carries an optional onset and duration (in beats, relative to its context) and wraps an underlying client object it delegates to. The concrete onset of an element typically comes from its placement inside a clausters.form.group.Group, not from the element itself, so a standalone leaf commonly has a duration but no onset (a relative character).

Arguments:

  • wraps - the underlying object playing delegates to (or None for a pure container like a Group).
  • onset - start in beats relative to the context, or None.
  • duration - length in beats, or None.

Element.temporal_character

@property
def temporal_character() -> str

This element's character (SEGMENT/PUNCTUAL/RELATIVE/ABSTRACT), derived from the presence of onset and duration.

Element.play

def play(destination)

Delegate playing to the wrapped item's play(destination) — the double-dispatch seam shared by clausters.seq.Event, clausters.seq.timeline.OscEvent/MidiEvent and clausters.seq.Automation.

Container and pattern-backed elements (Group, Track, a Sequence wrapping a Pattern) are not directly playable this way — they are rendered by render(). Delegating here requires the wrapped object to follow the play(destination) protocol.

Element.to_timeline

def to_timeline(base: float = 0.0)

Flatten this element to a flat clausters.seq.Timeline in absolute beats (accumulating nested placement offsets). See clausters.form.render.

Element.render

def render(destination,
           clock=None,
           *,
           at: float = 0.0,
           quant=None,
           ports=None)

Render this element onto destination — the change of state to sound. A concrete element flattens and plays through a clausters.seq.Playhead over clock (returns the playhead); a logical Group sends and instances a GraphDef on the server (returns the instance). See clausters.form.render.render.

Event

class Event(Element)

event/clip: parameters grouped into one action, internally simultaneous.

Wraps a clausters.seq.Event (or a plain dict of parameters). Its duration defaults to the event's dur when not given explicitly; its onset usually comes from its placement in a Group.

Sequence

class Sequence(Element)

List: strict order with no concrete time — only sequence.

Wraps a Python list or a clausters.seq.pattern.Pattern. The items can be numbers, events, notes or whole elements; the structure fixes only their successive order. Rendering bounces a pattern-backed sequence; a list is interpreted by its content.

Buffer

class Buffer(Element)

Buffer: a list at constant time — audio or control samples.

Wraps a clausters.defs.Buffer. An automation sampled at a constant interval is a control buffer (the List/Buffer duality of the arrangement).

A buffer is data, so rendering it as an audio clip needs an instrument: the def that plays it, named by instrument (a synth whose buf control takes the buffer number, as a sampler's does). Rendering then emits one event playing that def — to_event. Without an instrument the element is still perfectly good structure (and the editor draws its take), it simply has no sound of its own.

Arguments:

  • buffer - the clausters.defs.Buffer on the server.
  • instrument - the def that plays it (its buf control gets the buffer number), or None for a buffer that is data only.
  • controls - extra event parameters passed to that def (amp, rate…).
  • onset - start in beats relative to the context, or None.
  • duration - length in beats — how long the clip sounds. Give it for a take placed in time (an event's default length is used otherwise).

Buffer.to_event

def to_event()

The event that plays this buffer: the instrument def with the buffer number in its buf control, sounding for the element's duration.

legato is 1 so the take sounds its whole length (the note default of 0.8 would cut it short — a sampled take is not a note with a gap).

Track

class Track(Element)

Set: mixed placement of elements — a DAW track.

Wraps a clausters.seq.Timeline (free placement of items by beat). A fresh empty Timeline is created when none is given.

Generator

class Generator(Element)

Function: a generator element.

Wraps either server DSP (a SynthDef/FaustDef/GraphDef, or a def name) or a sequence generator (a Pbind/Routine). Its change of state — evaluating the generator into a generated element — happens at rendering: a contained event pattern is bounced to a timeline; a def member of a logical Group becomes a wired GraphDef member.

Arguments:

  • generator - the wrapped def (name or object) or sequence generator.
  • controls - control values for a logical-graph member — numbers, an internal-bus name (a str matching a Group bus), or "OUT" (hardware). Used by Group.to_graphdef.
  • maps - control-bus bindings for a logical-graph member (/n_map),
  • ```{control` - bus_name}``.

Generator.def_name

@property
def def_name() -> str

The member def name — the wrapped string itself, or the def object's name.

clausters.form.group

The arrangement — grouping, and the derived temporal relation.

A Group is the one genuinely new structure of the arrangement: the recursive placement of elements with an offset, and the temporal relation derived from how the members sit in time. Everything else (the five primitives) already exists and is merely adorned by clausters.form.element.Element.

Two kinds of grouping:

  • concrete — the members relate in time (a section holding clips, a melody holding note-events), with no processing relation.
  • logical — the members relate by processing or generation logic (a bus-wired signal chain on the server, or a generative dependency on the client).

Rendering lives in clausters.form.render; this module is pure structure plus the temporal-relation derivation (a pure function over the members' placements).

CONCRETE

The kind of a Group.

SUCCESSIVE

The temporal relation between a group's members, derived from their placements successive — duration-only, tiling contiguously; simultaneous — all starting and ending together (a container that can be reinterpreted, enabling recursion); mixed — any other combination.

_Member

class _Member()

One placed member of a Group. A stable object so it can be removed or moved by identity after other edits shift things.

offset is the member's start in beats relative to the group's context; dur is an explicit placement length that overrides the element's own duration when set.

_Member.length

@property
def length()

The effective length of this member: the placement dur if given, else the element's own duration (may be None).

Group

class Group(Element)

A composite element: a set of placed members with a grouping kind.

Members are placed by an offset (beats relative to the group's context) and an optional placement dur. Edit freely — add, remove, move; a handle returned by add stays valid across other edits (like clausters.seq.Timeline).

A LOGICAL group additionally names the composition and may declare internal buses; to_graphdef translates it into a clausters.defs.GraphDef (the bus-wired configuration the server already expresses).

Arguments:

  • children - optional iterable seeding the group. Each item is a (offset, element) pair, a (offset, dur, element) triple, or a bare Element (placed at offset 0).
  • kind - CONCRETE (default) or LOGICAL.
  • name - the composition's name — the GraphDef name for a logical group.
  • buses - internal buses for a logical group — each a name (audio, 1 channel) or a (name, rate) / (name, rate, channels) tuple.
  • onset - the group's own onset in its parent context, or None.
  • duration - the group's own duration, or None.

Group.add

def add(element, offset=0.0, dur=None)

Place element at offset (beats), optionally overriding its length with dur. Returns a member handle for remove/move.

Group.remove

def remove(member)

Remove a member returned by add (by identity).

Group.move

def move(member, offset, dur=None)

Reposition member to offset (and optionally set dur).

Group.clear

def clear()

Drop every member.

Group.members

@property
def members() -> list

The members as (offset, dur, element) triples, insertion order.

Group.handles

@property
def handles() -> list

The member handles (the objects add returns), insertion order — the stable identities remove and move take. Reading a placement is members; holding on to one across edits (as an editor keying its widgets by member does) needs these.

Group.temporal_relation

def temporal_relation()

Derive this group's temporal relation (SUCCESSIVE/SIMULTANEOUS/ MIXED) from its members' placements, or None when empty.

  • SIMULTANEOUS: every member starts and ends together (a single member trivially qualifies).
  • SUCCESSIVE: members tile contiguously in time — sorted by start, each member begins exactly where the previous ends (requires known lengths).
  • MIXED: anything else.

Group.to_graphdef

def to_graphdef(name=None)

Translate this logical group into a clausters.defs.GraphDef — the 1:1 mapping of the arrangement's logical grouping (nodes wired by sender/ receiver buses) onto the configuration the server already expresses.

Each member must be a clausters.form.element.Generator (its def_name is the member def; its controls — numbers, an internal bus name, or "OUT" — and maps wire it). The group's buses become the private internal buses. Placement offsets are ignored (a logical group is a signal graph, not a timeline). Returns the GraphDef; sending and instancing it is clausters.form.render.

clausters.form.render

Rendering — the change of state from the arrangement to sound.

A concrete Group is rendered by flattening it: a tree-walk that accumulates the nested placement offsets into absolute beats, producing a flat clausters.seq.Timeline of items that each know how to play(destination). That timeline is then played by a clausters.seq.Playhead — RT (timetagged bundles) or NRT (a score for Session.render) purely by which destination and clock it holds, sample-identical, with no scheduling path of its own. This mirrors Timeline.from_pattern: the arrangement reuses the sequencing layer rather than duplicating it.

Scope of this phase (the concrete path):

  • Group{concrete} — flattened recursively; each member's offset (and any nested group's) accumulates into the child's absolute beat.
  • Track — its Timeline's items are shifted by the placement beat.
  • Event — placed as a single item at its beat.
  • Sequence/Generator wrapping an event pattern (a Pbind) — bounced in the same pass (its change of state); a Sequence of elements is laid out successively by their durations.
  • An abstract element (no onset/duration, no content) contributes context, not an event.

A Buffer is data: it sounds through the instrument that plays it (a def whose buf control takes the buffer number), so a Buffer with an instrument emits one event playing it — the audio clip — and one without contributes structure only. A Group{logical} takes the other path entirely (it becomes a GraphDef); instancing a bare def still needs an instrument of its own and raises a clear NotImplementedError here.

flatten

def flatten(element, base: float = 0.0) -> list

Flatten element into (absolute_beat, item) pairs, sorted by beat, accumulating nested placement offsets onto base. The items are playable (they follow the play(destination) protocol).

to_timeline

def to_timeline(element, base: float = 0.0)

Flatten element into a flat clausters.seq.Timeline in absolute beats — the structure a Playhead plays and a transport seeks.

render

def render(element,
           destination,
           clock=None,
           *,
           at: float = 0.0,
           quant=None,
           ports=None)

Render element onto destination.

A concrete element (a Group, Track, Event, …) is flattened to a timeline and played through a Playhead over clock — RT (start/run the clock) or NRT (clock.render() then destination.render(), or Session.render), sample-identical; returns the Playhead.

A logical Group is translated to a clausters.defs.GraphDef, sent (/d_graph) and instanced (/graph_new, with ports overriding the surface defaults) on the Server destination; returns the instance group. The seam is the destination, not the element.

render_logical

def render_logical(group, server, *, ports=None)

Send a logical group's GraphDef (Group.to_graphdef) and instance it on server. Returns the instance group (server.graph's handle).

clausters.gui.guidef

Building GuiDefs the way defs are built.

A GuiDef is the GUI analogue of a SynthDef/GraphDef: a tree of {id, type, ...props, children} nodes serialized to JSON and carried inside one OSC argument. These helpers compose that tree as plain dicts — they are host-agnostic, just like building a SynthDef is server-agnostic; only clausters.gui.host.GuiHost knows how to send one. The root node carries no id (it comes from the /gui_def <id> argument); every child carries its own client-allocated integer id.

The int/float distinction is the user's to make and is preserved end to end: write 480 for an integer property and 480.0 for a float — json.dumps keeps them apart in the JSON text and the host's serde parse keeps them apart on the wire (ids stay integers, control values stay floats).

node

def node(type: str, *, id: int | None = None, children=None, **props) -> dict

A generic widget node {id?, type, ...props, children?}.

The building block every other helper wraps. Pass id for any non-root widget, children as an iterable of nodes for a container, and any other keyword as a property (kept verbatim, so its int/float type is preserved).

window

def window(*children,
           title: str | None = None,
           w: int | None = None,
           h: int | None = None,
           layout: str | None = None,
           **props) -> dict

A top-level window container (a GuiDef root). It takes no id.

panel

def panel(id: int, *children, layout: str | None = None, **props) -> dict

A nestable panel container; layout is row/col/grid/free.

label

def label(id: int, text: str, **props) -> dict

Static label text.

knob

def knob(id: int,
         *,
         label: str | None = None,
         min: float | None = None,
         max: float | None = None,
         value: float | None = None,
         **props) -> dict

A rotary knob over a continuous range.

slider

def slider(id: int,
           *,
           label: str | None = None,
           min: float | None = None,
           max: float | None = None,
           value: float | None = None,
           vertical: bool = False,
           **props) -> dict

A continuous slider over a range. vertical=True lays it out along the y axis (min at the bottom, max at the top) instead of horizontally.

number

def number(id: int,
           *,
           label: str | None = None,
           min: float | None = None,
           max: float | None = None,
           value: float | None = None,
           **props) -> dict

A draggable numeric read-out over a range.

button

def button(id: int, *, label: str | None = None, **props) -> dict

A momentary push button (emits 1 on press, 0 on release).

toggle

def toggle(id: int,
           *,
           label: str | None = None,
           value: bool | None = None,
           **props) -> dict

A boolean toggle. value is sent as 1/0 (OSC has no bool).

text

def text(id: int,
         *,
         value: str | None = None,
         label: str | None = None,
         **props) -> dict

A text field showing value (script-driven via /gui_set).

def menu(id: int,
         options,
         *,
         index: int | None = None,
         label: str | None = None,
         **props) -> dict

A menu selector over options (a list of strings); a click cycles to the next and emits the chosen index.

waveform

def waveform(id: int,
             *,
             data=None,
             blob: int | None = None,
             buffer: int | None = None,
             path: str | None = None,
             cache: str | None = None,
             channels: int | None = None,
             base_bucket: int | None = None,
             overlay: bool | None = None,
             ruler: str | None = None,
             ruler_y: str | None = None,
             bit_depth: int | None = None,
             sample_rate: float | None = None,
             tempo: float | None = None,
             beat_at: float | None = None,
             quant: float | None = None,
             sel_start: float | None = None,
             sel_len: float | None = None,
             playhead_at: float | None = None,
             y_start: float | None = None,
             y_len: float | None = None,
             link: int | None = None,
             **props) -> dict

The heavy waveform view, fed its samples one of several ways (in the host's precedence order):

  • cache — a path to a prebuilt peak-pyramid file (see peaks_cache_file) the host memory-maps and renders directly; the raw samples are never loaded. The most compact bulk path: nothing rides OSC. A cache built with channels > 1 holds every channel in the one file.
  • path — a path to a file of raw little-endian f32 samples (see samples_to_file, or the server's /b_export) the host memory-maps; a multi-megabyte buffer renders with no OSC and no re-send.
  • buffer — a server buffer number; the host fetches its samples from the audio server over OSC (it must be started with --server). The async fallback when a shared file is not available.
  • data — a small list of floats embedded inline in the JSON;
  • blob — the index of a binary blob carried beside the JSON in the same /gui_def message (see samples_to_blob and GuiHost.define).

channels is the interleaved channel count of path/data/blob (default 1): every channel is kept and drawn — stacked lanes sharing the time axis by default, or per-color overlaid traces with overlay=True. base_bucket sets the peak-pyramid bucket size (default 256); for path it also keys the sibling cache the host writes beside the file.

The rulers (each in its own strip beside the view, each independently switchable off, all live via GuiHost.set — so a menu or button in the same GUI can retune them): ruler labels the time axis — "time" (the default; clock time, using sample_rate or the rate the source brings), "samples", "beats" (musical time: tempo in beats per second — pass clock.tempobeat_at the beat position of sample 0, quant the beats per bar, labels bar:beat), or "off". ruler_y labels the amplitude axis — "norm" (the default; normalized [-1, 1]), "db" (dBFS), "bits" (integer sample values at the bit_depth resolution, default 16), "percent" (0-100% of full scale), or "off".

The rest of the editor chrome: sel_start/sel_len set the selection in samples (dragging on the view updates it and emits /gui_event id "selection" start len; Shift+drag pans, the wheel zooms). playhead_at draws a playhead tracking the engine sample clock: pass the /clock sample value that corresponds to buffer position 0 (negative or omitted = no playhead). y_start/y_len set the vertical view window — the visible slice of the amplitude axis, in normalized display units where 0, 1 (the default) is the full axis: the wheel over the y-ruler strip zooms it, dragging the strip pans it, and every change is reported as /gui_event id "view_y" y_start y_len (a non-positive y_len resets to the full axis).

link puts the view in a shared navigation group: every timeline view (waveform or spectrogram, in any window) declaring the same link id shares one horizontal view, selection and playhead — a zoom, pan or drag-selection on any member moves all of them, and setting view_start/view_len (samples; a non-positive view_len resets to the whole timeline), sel_start/sel_len or playhead_at via GuiHost.set on any member applies group-wide. Events still emit once, with the interacted member's id. Membership is live: set link to another group id to move the view, or to a negative value to unlink it (it keeps the view it had). Only the vertical window y_start/y_len stays per-view.

spectrogram

def spectrogram(id: int,
                *,
                data=None,
                blob: int | None = None,
                buffer: int | None = None,
                path: str | None = None,
                cache: str | None = None,
                channels: int | None = None,
                window_size: int | None = None,
                hop: int | None = None,
                sample_rate: float | None = None,
                db_floor: float | None = None,
                db_ceil: float | None = None,
                freq_scale: str | None = None,
                log_freq: bool | None = None,
                colormap: int | None = None,
                ruler: str | None = None,
                ruler_y: str | None = None,
                tempo: float | None = None,
                beat_at: float | None = None,
                quant: float | None = None,
                sel_start: float | None = None,
                sel_len: float | None = None,
                playhead_at: float | None = None,
                y_start: float | None = None,
                y_len: float | None = None,
                link: int | None = None,
                **props) -> dict

The heavy spectrogram (STFT time-frequency) view, fed like the waveform: a mapped path of raw little-endian f32, a server buffer, inline data/blob, or a prebuilt single-channel STFT cache file. channels de-interleaves the source (default 1); each channel gets its own analysis, drawn as stacked lanes sharing the time axis.

The analysis: window_size is the FFT size (a power of two, default 1024) and hop the frame advance (default window_size // 2; the host raises it as needed so a long file fits the GPU texture). sample_rate places the frequency axis for path/inline sources (a fetched buffer brings its own rate). The display is live (GuiHost.set): the dB window [db_floor, db_ceil] (default -90/0) controls contrast, freq_scale picks the frequency axis — "log" (the default), "linear", "mel" or "bark" (log_freq is the legacy boolean alias for the first two) — and colormap picks 0 viridis / 1 magma / 2 grayscale.

The rulers ride their own strips beside the view: ruler_y ("hz", the default, or "off") draws the frequency ruler, its tick positions following freq_scale; ruler labels the time axis exactly as on the waveform ("time"/"samples"/"beats" with tempo/beat_at/quant, or "off"). The rest of the editor chrome (sel_start/sel_len, playhead_at, drag-to-select / Shift+drag pan / wheel zoom) also works exactly as on the waveform — including the vertical view window y_start/y_len, which here slices the frequency display axis (normalized, 0, 1 = the full axis, whatever the freq_scale): wheel over the Hz-ruler strip zooms, dragging it pans, changes emit /gui_event id "view_y" y_start y_len.

link joins a shared navigation group exactly as on the waveform — the classic composition is a waveform lane and a spectrogram lane of the same render under one link, scrolling and selecting in lockstep.

meter

def meter(id: int,
          bus: int,
          *,
          min: float | None = None,
          max: float | None = None,
          label: str | None = None,
          **props) -> dict

A level meter reading control bus straight from the audio server's shared-memory segment each frame (zero OSC messages). The host must be started with --shm pointing at the server's segment. min/max scale the bar (default 0/1).

scope

def scope(id: int,
          bus: int = 0,
          *,
          tap: int | None = None,
          window_ms: float | None = None,
          trigger: float | None = None,
          hold: bool | None = None,
          min: float | None = None,
          max: float | None = None,
          label: str | None = None,
          **props) -> dict

A time-domain scope, in one of two rates. By default (control rate) it plots the recent history of control bus, read from shared memory each frame (needs --shm like meter). Passing tap makes it an audio-rate oscilloscope over that audio-tap ring of the server (route a bus into it first with Server.tap): a window_ms display window (default 20 ms), re-read every frame and aligned on a rising crossing of trigger (default 0.0, with hysteresis; free-running when the signal never crosses), so a periodic signal draws a stable trace. hold freezes the trace. Natively the host reads the tap out of the --shm segment with zero messages; in the browser it subscribes /tap_stream over the server leg. min/max set the vertical range (default the bipolar -1/1).

phasescope

def phasescope(id: int,
               tap: int,
               tap2: int | None = None,
               *,
               window_ms: float | None = None,
               hold: bool | None = None,
               label: str | None = None,
               **props) -> dict

A phasescope (goniometer): the two audio taps tap (left) and tap2 (right, default tap + 1) drawn as the 45°-rotated Lissajous figure — vertical is the mid (L + R)/√2, horizontal the side (L - R)/√2, the audio-engineering convention where mono reads as a vertical line, anti-phase as horizontal and a wide field fills the lozenge. An age-faded persistence trail spans the last window_ms of pairs (default 30 ms) and a correlation read-out (Pearson's r over the window) sits under the field. Route each channel's bus into its tap first with Server.tap; hold freezes the trace. Reads the segment natively (zero messages) and /tap_stream in the browser, like the oscilloscope.

spectrum

def spectrum(id: int,
             tap: int,
             *,
             fft_size: int | None = None,
             db_floor: float | None = None,
             db_ceil: float | None = None,
             log_freq: bool | None = None,
             averaging: float | None = None,
             peak_hold: bool | None = None,
             label: str | None = None,
             **props) -> dict

A live spectrum (spectroscope): one forward FFT per frame over the newest window of audio tap tap, drawn as a magnitude curve. fft_size is a power of two (256..4096, default 2048); the vertical axis is dB over [db_floor, db_ceil] (default -100/0); log_freq (default true) selects a log frequency axis. Raw per-frame FFTs flicker, so averaging (0..1, default 0.5) exponentially smooths each bin and peak_hold (default false) overlays a slowly decaying peak trace. Route a bus into the tap first with Server.tap; the analysis uses the shared-core FFT and Hann window, so it agrees with the spectrogram. Native reads the segment; the browser subscribes /tap_stream.

nodetree

def nodetree(id: int,
             *,
             group: int = 0,
             controls: bool | None = None,
             label: str | None = None,
             **props) -> dict

A live nodetree view of the audio server's node tree rooted at group (default the root group 0). The host mirrors the server's tree over its client leg (it must be started with --server), refreshing on node creation/removal and a low-rate poll, so group/synth changes and /n_set edits show live. controls (default true) shows each synth's control name/value pairs. A read-only view.

bpf

def bpf(id: int,
        *,
        points=None,
        min: float | None = None,
        max: float | None = None,
        duration: float | None = None,
        exp: bool | None = None,
        label: str | None = None,
        **props) -> dict

A drawable bpf break-point function — the envelope editor.

Breakpoints (time, value) plus a per-segment shape using the server's own envelope shape numbers, evaluated host-side through the same shared math the server's EnvGen plays — what you draw is what you hear. points accepts either the flat quad list [t, v, shape, curve, ...] (the wire form: shapes int, everything else float) or a list of tuples (time, value) / (time, value, shape) where shape is an Env-style curve spec — a name ("lin", "exp", "sin", "step", "hold", ...) or a numeric curvature. Omitting points draws a flat, immediately editable line. See env_to_points / points_to_env for the round trip with clausters.defs.Env.

The widget is general on purpose (the automation-lane shape): values live in [min, max] — unipolar (the 0/1 default), bipolar, or any parameter span; an on/off lane is the "hold" shape over 0/1 (each point's value held until the next point — "step", per the SuperCollider semantics, instead jumps to the target level at segment start, so a step segment shows the next point's value); exp=True gives frequency-like ranges a geometric display scale (requires 0 < min < max). Times span [0, duration] (omitting duration fits the last point).

Editing (drag a point — times stay monotonic; drag a segment vertically to bend its curvature; Ctrl+click adds a point, Ctrl+click on one removes it) flows back per the edit-back pattern: /gui_event <id> "points" <t v shape curve ...> to the script — or, when the widget is bound (GuiHost.bind or an inline bind), the flat list is forwarded straight to the audio server after the binding's prefix. Setting is live too: GuiHost.set(id, points=json.dumps(flat)) replaces the whole list (a /gui_set value is a scalar, so the array rides as its JSON string).

plot

def plot(id: int,
         *,
         data=None,
         blob: int | None = None,
         path: str | None = None,
         channels: int | None = None,
         min: float | None = None,
         max: float | None = None,
         label: str | None = None,
         **props) -> dict

A simple static plot of a signal over [min, max] (default the bipolar -1/1) — a line when the data fits the width, a min/max envelope when it does not. Unlike the heavy waveform, it does not zoom or pan; it is the catalog's "plot of an NRT-generated signal/file". Its samples come from:

  • path — a file of raw little-endian f32 (see samples_to_file, or an NRT render written out) the host memory-maps; the bulk path, no OSC. channels de-interleaves channel 0 (default 1).
  • data — a small list of floats inline in the JSON;
  • blob — the index of a binary blob carried beside the JSON (see samples_to_blob and GuiHost.define).

track

def track(id: int,
          *clips,
          label: str | None = None,
          height: float | None = None,
          snap: float | None = None,
          ruler: str | None = None,
          sample_rate: float | None = None,
          tempo: float | None = None,
          beat_at: float | None = None,
          quant: float | None = None,
          playhead_at: float | None = None,
          **props) -> dict

A multitrack track lane holding clip children placed on a shared time axis — the DAW-style track editor's lane. label names it in a left header; height is its lane weight when several tracks stack under one window (a col layout). The window's tracks share one time axis, so a clip at a given offset lines up across lanes. snap is the drag grid in timeline samples a clip's move/resize rounds to (omitted / 0 = snap to whole samples).

The lanes of a window navigate as one: they share a time axis you can zoom (wheel) and pan (Shift+drag), spanning the composition (the longest clip end over every lane, so dragging a clip past the end lengthens it). That is the same navigation group the heavy views use, so link joins or splits it — pass a shared id to align lanes across windows, or a distinct one to give a lane an axis of its own. Scripted navigation is GuiHost.set(track_id, view_start=…, view_len=…), and it applies group-wide.

A lane carries the same time chrome as the heavy editor views:

  • ruler — a time ruler under the lane ("time", "samples", "beats", or the default "off": a lane reserves no ruler strip unless asked). sample_rate labels real time, and tempo/beat_at/ quant label beats. One ruler under the bottom lane is the usual layout.
  • playhead_at — the engine sample-clock value at timeline position 0, so the playhead sweeps the clips as the composition plays (the same anchor the waveform uses; read the clock with Server.request("/clock")). Set it live with GuiHost.set(track_id, playhead_at=clock); a negative value (the default) draws no playhead.

Pass the clips positionally::

track(1, clip(10, offset=0, dur=4, data=take_a),
         clip(11, offset=4, dur=2, data=take_b), label="drums")

clip

def clip(id: int,
         *,
         offset: float = 0.0,
         dur: float,
         data=None,
         blob: int | None = None,
         buffer: int | None = None,
         path: str | None = None,
         cache: str | None = None,
         channels: int | None = None,
         base_bucket: int | None = None,
         notes=None,
         points=None,
         exp: bool | None = None,
         min: float | None = None,
         max: float | None = None,
         label: str | None = None,
         **props) -> dict

One clip on a track: a placed rectangle spanning [offset, offset + dur] in timeline sample units (the graphic unit — length = duration). Its body is one of three:

  • a waveform — the take, drawn decimated to the clip's pixel width;
  • a piano-rollnotes, an iterable of (start, dur, pitch) (or (start, dur, pitch, velocity, channel)) events (times relative to the clip, in samples; pitch mapped over [min, max]), drawn as note bars — the events-track view. The dedicated editor-grade pianoroll widget draws the same notes with a keyboard and editing; or
  • an automation curvepoints, break-points over the clip's span (the bpf editor's break-points and shape math, placed on a lane): times relative to the clip in samples, values over [min, max] (exp=True gives a frequency-like range a geometric display scale). It is editable in place — drag a point, Ctrl+click to add one or remove the one under the cursor — and an edit flows back as the same flat "points" event the bpf view sends, so an clausters.seq.Automation consumes it either way.

A real take is minutes long, so it never rides the wire as JSON. The waveform body reaches the clip exactly the ways the heavy waveform view's samples do, in the same precedence order:

  • cache — a prebuilt peak-pyramid file the host maps (see peaks_cache_file); the most compact bulk path, raw samples never loaded.
  • path — a file of raw little-endian f32 the host maps (see samples_to_file); channels de-interleaves it, base_bucket sizes the pyramid built (and cached) on load. No OSC.
  • buffer — a server buffer, fetched over the host's client leg.
  • data/blob — a short body inline (a float list, or the index of a blob carried beside the JSON — see samples_to_blob); it must fit the datagram, so keep it to a sketch.

Whichever the source, the body is summarized to fit the clip rectangle through the take's peak pyramid — the same "never resolve finer than the screen" rule the editor views follow.

Other keywords:

  • offset — the clip's start on the shared timeline (samples; >= 0).
  • dur — its duration (samples); a clip with no duration draws nothing. For an audio take placed 1:1, that is the take's frame count.
  • min/max — the waveform value range, or the low/high pitch of a piano-roll (default the bipolar -1/1).

Dragging a clip (move) or its edge (resize) flows back as a "clip" event carrying the new offset/dur — the edit-back path — so a driver can update the arrangement and re-render.

pianoroll

def pianoroll(id: int,
              *,
              notes=None,
              osc=None,
              min: float | None = None,
              max: float | None = None,
              snap: float | None = None,
              velocity: bool | None = None,
              osc_lane: bool | None = None,
              midi_in: bool | None = None,
              link: int | None = None,
              ruler: str | None = None,
              sample_rate: float | None = None,
              tempo: float | None = None,
              beat_at: float | None = None,
              quant: float | None = None,
              sel_start: float | None = None,
              sel_len: float | None = None,
              playhead_at: float | None = None,
              playhead: float | None = None,
              y_start: float | None = None,
              y_len: float | None = None,
              label: str | None = None,
              **props) -> dict

The dedicated editor-grade pianoroll view: a piano keyboard gutter, a note grid, an optional velocity lane and an OSC-event lane — the timeline sibling of the compact clip piano-roll body, drawing the same notes with the same geometry (they share the host's pianoroll primitives), plus editing, rulers and navigation.

Content:

  • notes — an iterable of (start, dur, pitch) or (start, dur, pitch, velocity, channel) MIDI notes: times in timeline samples, pitch a MIDI note number drawn over the [min, max] window (default the 88-key range 21–108), velocity 0..127 (default 100), channel 0..15. The notes are the MIDI messages the roll represents.
  • osc — an iterable of (time, label) (or bare time) OSC events, drawn as flags in a lane below the grid — the OSC messages the roll carries alongside the notes.

Editing (native gestures; the browser keeps display + /gui_set parity): drag a note to move it in time/pitch, drag an edge to resize it, Ctrl+click to add a note or remove the one under the cursor; drag in the velocity lane to set a note's velocity; Ctrl+click the OSC lane to add/remove an event, drag one to move it. snap is the drag grid in timeline samples (0 = whole samples). An edit flows back as a flat "notes" event (start dur pitch velocity channel …) or "osc" event (time label …) — the edit-back pattern — so a driver updates the arrangement and re-renders.

Navigation and chrome mirror the heavy editor views: it is a timeline widget, so link joins/splits its navigation group (zoom with the wheel over the grid, pan with Shift+drag, all group-wide); ruler places a time ruler ("time"/"samples"/"beats", default "time") with sample_rate/tempo/beat_at/quant labelling it; sel_start/ sel_len mark a time selection; playhead_at sweeps a playhead from the engine clock (playhead sets a static cursor); y_start/y_len are the vertical pitch window (normalized 0..1 over [min, max]) for pitch zoom/pan. velocity=False hides the velocity lane; osc_lane=True opens the OSC lane even with no events (to author them). midi_in=True arms live MIDI painting in the native host: it opens a virtual MIDI input port ("clausters-gui") and paints incoming notes into this roll — at the running playhead, or step-entering on the snap grid when the transport is stopped — flowing back as the usual "notes" events (the standalone host's live input; a script can equally paint via a clausters.responders. MidiFunc and /gui_set).

graph

def graph(id: int,
          *,
          members=None,
          buses=None,
          wires=None,
          label: str | None = None,
          **props) -> dict

A graph patcher: a bus-wired node graph (a clausters.defs.GraphDef) drawn as member boxes, bus nodes, and a wire per connection — the logical side of a composition, where materials relate by processing rather than by time.

The view is deliberately bipartite, because that is what a GraphDef knows: a member's control touches a bus. Which end writes and which reads is the server's own analysis (it sorts the graph), so the patch shows the connection and leaves the direction to the engine.

  • members — the nodes, each (def_name, [control, …]): the def and the controls that are wired (each drawn as a port on its box).
  • buses — the internal bus names, plus "OUT" (the hardware) when used.
  • wires — the connections, each (member_index, control, bus).

Dragging a port onto a bus rewires that control; dropping it on empty space unwires it. Either way the edit flows back as /gui_event <id> "wire" <member> <control> <bus> (an empty bus = unwired), so a driver updates the logical group and re-renders it — the same edit-back pattern the clips use.

canvas

def canvas(id: int,
           shader: str | None = None,
           *,
           params=None,
           buses=None,
           label: str | None = None,
           **props) -> dict

A canvas running a script-supplied WGSL shader over the widget area -- custom visuals (ShaderToy-style).

shader is the body of a shade function the host wraps and runs::

fn shade(uv: vec2<f32>, frag: vec4<f32>) -> vec4<f32> { ... }

Inside it, the host exposes u.resolution (the viewport size in px), u.time (seconds), and u.params (a vec4<f32> of four values). The four params are driven two ways, which is the point of the widget:

  • from the script -- GuiHost.set(id, param0=...) sends an OSC value that lands in u.params.x (param0..param3 -> .x...w);
  • from a control bus, read straight from the audio server's shared memory each frame (zero messages) -- buses=[busA, busB, ...] maps each control bus onto the param of the same index; a -1 (or absent) slot stays script-driven. Needs the host started with --shm (like meter).

So a shader can animate from OSC parameters and from live server audio at once. Omitting shader uses a default moving color field. params is an optional initial list of floats.

to_json

def to_json(tree: dict) -> str

Serializes a GuiDef tree to the JSON string carried in /gui_def.

samples_to_blob

def samples_to_blob(samples) -> bytes

Packs an iterable of floats into a little-endian f32 blob, the bulk form a waveform reads via blob. Flat bytes at the boundary — the same rule the rest of the client follows.

samples_to_file

def samples_to_file(samples, path: str) -> str

Writes samples to path as raw little-endian f32 — the local shared resource a waveform(path=...) maps. Unlike samples_to_blob (which rides the /gui_def message and so must fit a datagram), a file has no size limit: this is how a multi-megabyte buffer reaches the host without OSC. Returns path.

peaks_cache_file

def peaks_cache_file(samples,
                     path: str,
                     base_bucket: int = 256,
                     channels: int = 1) -> str

Builds the peak-pyramid cache for samples (via the shared native core, so it is byte-identical to the host's own) and writes it to path — the most compact bulk path, mapped by a waveform(cache=...). The host renders the overview without ever loading the raw samples. With channels > 1 the samples are interleaved frames and the file is the multichannel cache (one resource, a pyramid per channel — the editor-grade stacked lanes). Returns path.

correlation

def correlation(left, right) -> float | None

The stereo correlation (Pearson's r) of two equal-length channels, in [-1, 1]+1 mono/in-phase, 0 decorrelated, -1 anti-phase — via the shared native core, so a headless capture reads the identical number the GUI phasescope draws. None when it is undefined (empty input or a constant channel: silence/DC). Pair it with Server.stream_taps to measure a live stereo signal without the GUI.

lissajous

def lissajous(left, right) -> list

The Lissajous / goniometer coordinates of stereo pairs (left, right): each maps to (x, y) with x the side (L - R)/√2 and y the mid (L + R)/√2 — the rotated stereo plane a goniometer draws. The geometry lives once in the shared native core (the phasescope draws the same points); useful for plotting or driving a stereo image in electroacoustic work. Returns a list of (x, y) tuples.

clausters.gui.editor

Editor: the bridge between the arrangement and the multitrack GUI.

The driver of the DAW-style view. It draws a clausters.form tree as a multitrack GuiDef (tracks of clips on one shared time axis), applies the clip edit-backs the host sends straight onto the arrangement, and re-renders it — the loop data ↔ graphic ↔ sound, which is what makes the composition editable at any granularity rather than merely displayable.

Three things are worth knowing about how it is built.

The dependency arrow points this way. clausters.form stays pure and transport-agnostic; the editor imports the arrangement, never the reverse. This module is the only one that knows both worlds.

Beats meet samples here. The arrangement places elements in beats; the multitrack view places clips in timeline samples, because a clip's body is audio data and its sample 0 sits at the clip's offset. The editor is the only converter: one beat is sample_rate / tempo timeline units, so an audio take placed at its own length sits 1:1 on the axis. A musical quant becomes the lane's drag grid, so the grid a clip is dropped on is the grid the arrangement re-schedules on. The arithmetic itself is the core's (beats_to_secssecs_to_samples), not a second implementation.

One mapping rule, not a heuristic per case. The root Group's members are the lanes; a lane's members are its clips; a Buffer clip draws its take, a element of events draws a piano-roll, and a nested Group draws as a labeled rectangle — its summary — until it is expanded into lanes of its own. That collapse/expand is the arrangement's base level (the zoom that summarizes a group or resolves it), so it needs no protocol of its own.

DEFAULT_PITCH

The pitch range a piano-roll lane falls back to when its notes give none (C3..C6 — the span a melodic line usually lives in).

PITCH_PAD

Semitones of headroom above and below the notes of a piano-roll clip.

Editor

class Editor()

A composition on screen: the arrangement tree drawn as a multitrack view, editable back into the tree.

Arguments:

  • element - the composition — a clausters.form.group.Group (its members become the lanes) or any single Element (one lane).

  • sample_rate - the engine's sample rate; with tempo it fixes the beats↔timeline-samples conversion.

  • tempo - the clock's tempo in beats per second (the TempoClock convention — 2.0 is 120 bpm).

  • quant - the musical drag grid in beats (0.25 = a sixteenth); 0 snaps to whole samples.

  • follow - re-render on every edit (the live editor).

  • extra - extra GuiDef nodes to place under the lanes (a transport panel, say). Their events are not the editor's: apply ignores them, so a script can handle them itself.

  • title - the window title.

  • base_id - the first widget id the editor allocates. The default sits well above the ids clausters.gui.host.GuiHost assigns to windows it opens (from 1000), so the two never collide.

    Usage::

    editor = Editor(song, sample_rate=server.sample_rate, tempo=clock.tempo, quant=0.25) editor.open(gui) # draw and open the window editor.apply(*gui.poll()) # a dragged clip moves the element editor.render(server, clock) # play the edited composition

Editor.units_per_beat

@property
def units_per_beat() -> float

Timeline samples per beat — the whole of the data↔view unit bridge. One timeline unit is one audio sample, so a take placed at its own frame count sits 1:1 on the axis.

Editor.beats_to_units

def beats_to_units(beats: float) -> float

Beats → timeline samples, through the core's own time arithmetic (the seconds→samples rounding every client shares).

Editor.units_to_beats

def units_to_beats(units: float) -> float

Timeline samples → beats: the inverse the edit-back path takes to turn a dragged clip back into a placement.

Editor.expand

def expand(element) -> "Editor"

Resolve a nested Group into lanes of its own (instead of the labeled rectangle that summarizes it). The arrangement's base level, made an edit.

Editor.collapse

def collapse(element) -> "Editor"

Summarize a nested Group back into one labeled rectangle.

Editor.draw

def draw() -> dict

The composition as a window-rooted GuiDef: one track lane per member of the root group, each holding its members as clips on the shared time axis — and a graph patch for every logical group, whose members relate by processing rather than by time (so a lane would be the wrong shape for it). Pure — it builds the tree and the id registry, and sends nothing.

Editor.open

def open(host, id: int | None = None) -> int

draw the composition and open it on host (a clausters.gui.host.GuiHost). Returns the window id.

Editor.open_pianoroll

def open_pianoroll(host, element=None, id: int | None = None) -> int

draw a single events element as a dedicated piano-roll window and open it on host — the editor-grade note view (a keyboard, an editable note grid, a velocity lane, an OSC-event lane) of one MIDI/OSC element, as opposed to open, where the same notes are only a clip body.

Edits write back through poll exactly as the multitrack does, when the element is editable — a clausters.form.Track (a clausters.seq.Timeline): a dragged, added or removed note is rebuilt onto its timeline. A generator (a Pbind/Routine) is forward-only, so its bounced notes are shown read-only (bounce it to a Track to edit). OSC events are shown but not edited back yet (a marker carries only its time and address, not the full message). Returns the window id.

Editor.extent

def extent(element=None) -> float

The composition's length in beats, read from the arrangement — the end of its last placed element. It is not a constant: move a clip past the end and the piece gets longer, which is exactly what a transport must ask (a hard-coded length would cut the playback short at the old end).

Editor.playhead

@property
def playhead()

The clausters.seq.Playhead playing the composition, or None before the first render — what a transport (play/pause/stop/locate) drives.

Editor.window

@property
def window()

The open window's id, or None once it is closed (a /gui_closed seen by apply/poll) — what a script's loop checks to stop.

Editor.update

def update()

Push the current arrangement back to the open window — a whole-tree redefine (GuiHost.define), the honest way to show a structural edit (an element added, a group expanded). A mere placement change needs no redefine: the host already moved the clip that was dragged.

Editor.apply

def apply(addr: str, args) -> bool

Apply one message from the host to the arrangement. Returns whether the composition changed.

The clip edit-back (/gui_event <id> "clip" <offset> <dur>, the payload a drag or a resize sends) is resolved through the widget registry to the placement it came from and written with Group.move. The clip's offset is absolute on the shared axis while a placement is relative to its group, so the position converts back through the base the clip was drawn at; and only what actually moved is written — a drag carries the clip's unchanged dur along, and snapping that to the grid would silently shorten the element. /gui_closed drops the window (its own — the payload names the window id); anything else is ignored, so a whole poll loop can be fed straight in — even one shared with a second editor (a dedicated piano-roll beside the multitrack, say): every route resolves through this editor's own registries, so another window's events fall through untouched.

Editor.poll

def poll(timeout: float = 0.0) -> bool

Drain the host's pending messages into the arrangement (apply each). Returns whether the composition changed. Call it from the script's loop — never from the clock thread, which a routine must never block.

Editor.render

def render(destination, clock=None, *, at: float = 0.0, quant=None)

Render the composition onto destination — RT (a Server and a running clock) or NRT (a score) — and anchor the lanes' playhead so the line sweeps the clips as it plays. Returns the clausters.seq.Playhead.

This is the arrangement's own render (flatten to absolute beats, play through a playhead): the editor adds no rendering path of its own, it only remembers the destination so rerender can re-schedule after an edit.

Editor.rerender

def rerender(*, at: float | None = None)

Re-schedule the (edited) composition from the playhead's current position: stop, re-flatten, play again.

The honest semantics are re-schedule from here, not a sample-exact splice — a synth already sounding keeps sounding, and what changes is what has not been scheduled yet. In NRT there is no "already", so it is simply a fresh score.

Editor.position

@property
def position() -> float

The transport's position in beats: where the playhead is while it plays, and where the next play starts when it does not.

Editor.play

def play(destination=None, clock=None, *, at: float | None = None)

Play (or resume) from the transport's position — a fresh render, so it plays the composition as it now stands (moved clips, new lengths, redrawn curves). Reuses the destination and clock of the last render when they are not given.

Editor.pause

def pause()

Halt where we are: the playhead stops scheduling and the position stays, so a play resumes from here. What is already sounding keeps sounding — stopping a playhead is not a panic button (the script owns its voices).

Editor.stop

def stop()

Halt and return to the top.

Editor.locate

def locate(beat: float)

Seek: put the transport at beat. Playing, it re-renders from there (so a seek also picks up any edit); stopped, it just moves the cursor the lanes draw. This is what a click on a lane's ruler does.

Editor.anchor

def anchor(server, *, at: float = 0.0) -> bool

Anchor every lane's playhead to the engine clock, so the line starts at beat at of the timeline and sweeps on with the audio. Returns whether it could (a destination with no clock — an NRT score — has no playhead).

playhead_at is the sample-clock value at timeline position 0, which is now minus the beats already played. The anchor is a query: it asks the server for its clock, and a server that does not answer leaves the lanes without a line — so the failure is reported, not swallowed (a playhead that silently never appears is the worst of both).

Editor.unanchor

def unanchor()

Take the sweeping playhead line off the lanes (the transport's cursor, if any, stays). The host's anchored playhead tracks the engine clock, so a line left anchored keeps sweeping after the music stopped.

clausters.gui.host

GuiHost: the client object that drives a clausters-gui host.

The GUI host is a sibling OSC front of the audio server: it speaks the same OSC encoding over the same transports, only the vocabulary is /gui_* instead of the audio commands. So GuiHost reuses the existing OSC interfaces (clausters.base.OscTcpInterface by default — the host listens on TCP at the same port, so a /gui_def tree is not bounded by a UDP datagram — clausters.base.OscUdpInterface with transport="udp") pointed at the host's port rather than the server's, and builds messages with the existing encoder — there is no parallel wire code here. Keep the split: building the GuiDef tree (see clausters.gui.guidef) is host-agnostic; only this object talks to the host.

This is the request/reply face used at the skeleton milestone: define sends a whole tree, set/free mutate it, and query round-trips a widget's state back through /gui_info. Event streams (/gui_event//gui_closed) flow through the responder model (clausters.responders.OscFunc) and are wired up as the interactive widgets land.

DEFAULT_PORT

The GUI host's default port, UDP and TCP alike (the host's transport::DEFAULT_PORT), clear of the audio server's family (UDP/TCP 57110, WebSocket 57120).

GuiHost

class GuiHost()

A connection to a running clausters-gui host.

transport picks the carrier: "tcp" (default — reliable, and a /gui_def tree with its blobs can be as large as the host's frame ceiling) or "udp" (each message must fit a datagram; for constrained setups or a host started with --no-tcp).

GuiHost.boot

@classmethod
def boot(cls,
         server: "str | None" = None,
         *,
         shm: "str | None" = None,
         port: "int | None" = None,
         transport: str = "tcp",
         verbose: int = 0,
         data_dir=None,
         extra_args=(),
         ready_timeout: float = 10.0) -> "GuiHost"

Start a clausters-gui visual-server process and return a GuiHost connected to and owning it.

The launcher's ergonomic non-Session entry point for the GUI: it spawns the host binary (its client leg pointed at server and, when given, mapping the audio server's shm segment), waits until it answers, and hands back a started GuiHost whose stop also stops the process (as does interpreter exit). Pass a clausters.defs.Server's address and its shm, or let clausters.Session.gui wire those for you.

Arguments:

  • server - the audio server address as "host:port", or None for a host with no client leg.
  • shm - the audio server's shared-memory segment path to map (Unix only), or None to skip it.
  • port - the GUI host's own port (UDP and TCP alike); None uses the default (57210).
  • transport - the carrier this GuiHost talks over — "tcp" (default) or "udp".
  • verbose - host log verbosity, like clausters.defs.Server.boot.
  • data_dir - the host's --data-dir for its GuiDef store.
  • extra_args - extra host CLI tokens.
  • ready_timeout - seconds to wait for the host to answer.

Returns:

A started, process-owning GuiHost.

GuiHost.stop

def stop()

Close the connection and, if this host boot-ed a clausters-gui process, stop it too.

GuiHost.open

def open(tree: dict, *blobs: bytes, id: "int | None" = None) -> int

Open a window from a window-rooted GuiDef and return its id.

A thin, id-managing wrapper over define: with id=None an id is assigned for you (and remembered so close / close_all can free it); pass an explicit id to name the root yourself (e.g. to set its children by their own ids later). Editing the open window is set; closing it is close. Any trailing blobs ride along exactly as in define.

GuiHost.close

def close(id: int)

Close a window opened with open (or any widget subtree): /gui_free destroys the subtree and, for a window root, its OS window. The counterpart to open; set edits a window in between.

GuiHost.close_all

def close_all()

Close every window still open through open. Handy at the end of a live session before dropping the host.

GuiHost.define

def define(id: int, tree: dict, *blobs: bytes)

/gui_def <id> <json> [blob…] — build a whole widget tree in one message. Any trailing blobs (e.g. waveform samples from clausters.gui.guidef.samples_to_blob) ride alongside the JSON and are referenced by index from a widget's blob property.

GuiHost.set

def set(id: int, **props)

/gui_set <id> <k> <v> ... — update one live widget. Property types are preserved: a Python int rides as an OSC int, a float as an OSC float.

GuiHost.free

def free(id: int)

/gui_free <id> — destroy a widget and its subtree.

GuiHost.bind

def bind(id: int, address: str, *prefix)

/gui_bind <id> "server" <address> <prefix…> — forward this widget's value straight to the audio server, bypassing this script.

On every change the host sends address (an OSC path like /n_set or /c_set) with the fixed prefix arguments followed by the widget's value — e.g. bind(10, "/n_set", node_id, "freq") makes knob 10 send /n_set <node_id> freq <value> to the server itself, so the control responds with no round-trip through Python (the low-latency path). A bound widget stops emitting /gui_event; unbind restores it. The host must have been started with --server for the value to reach the audio server. prefix items keep their type (an int rides as an OSC int, a str as a string).

GuiHost.unbind

def unbind(id: int)

/gui_bind <id> (no target) — remove a widget's binding, so its value flows back to this script as /gui_event again.

GuiHost.query

def query(id: int, timeout: float = 1.0)

/gui_query <id> -> the /gui_info reply as (type, props).

Returns None on timeout. An empty type ("") means the host has no such widget — it still answers, the way the server replies even on a miss.

GuiHost.poll

def poll(timeout: float = 0.0)

One inbound message as (addr, args), or None within timeout.

The receive side of the protocol: the host pushes /gui_event (a widget was interacted with) and /gui_closed (a window was closed) back to the script that built the window. Drive an interactive panel by polling this in a loop, or wrap it with a clausters.responders.OscFunc-style dispatch.

GuiHost.listen

def listen(duration: float, handler)

Polls events for duration seconds, calling handler(addr, args) for each. A small convenience for scripts and demos; for anything richer, use poll with your own loop or the responder model.