API reference
- clausters.session
- clausters.responders
- clausters.ipc
- clausters.errors
- clausters.base.stream
- clausters.base.clock
- clausters.base.timebase
- clausters.base.main
- clausters.seq.event
- clausters.seq.pattern
- clausters.seq.timeline
- clausters.defs.signals
- clausters.defs.boxes
- clausters.defs.ugens
- Ugen
- Control
- control
- sin_osc
- impulse
- white_noise
- in_
- in_ctl
- out_ctl
- out
- replace_out
- send_trig
- send_reply
- poll
- fft
- ifft
- pv_mag_above
- pv_mag_below
- pv_brick_wall
- play_buf
- buf_rd
- local_in
- local_out
- mul_add
- sum3
- sum4
- lag
- var_lag
- sample_rate
- buf_frames
- rand
- dseq
- demand
- DoneAction
- NONE
- PAUSE_SELF
- FREE_SELF
- FREE_SELF_AND_PREV
- FREE_SELF_AND_NEXT
- FREE_SELF_AND_FREE_ALL_IN_PREV
- FREE_SELF_AND_FREE_ALL_IN_NEXT
- FREE_SELF_TO_HEAD
- FREE_SELF_TO_TAIL
- FREE_SELF_PAUSE_PREV
- FREE_SELF_PAUSE_NEXT
- FREE_SELF_AND_DEEP_FREE_PREV
- FREE_SELF_AND_DEEP_FREE_NEXT
- FREE_ALL_IN_GROUP
- FREE_GROUP
- FREE_SELF_RESUME_NEXT
- Env
- env_gen
- env_to_points
- points_to_env
- clausters.defs.faustdef
- clausters.defs.synthdef
- clausters.defs.graphdef
- clausters.defs.server
- ServerOptions
- ServerInfo
- Server
- boot
- shm
- send_msg
- send_bundle
- play_event
- request
- query_info
- query_tree
- node_query
- dump_graph
- add_faustdef
- add_synthdef
- add_graphdef
- graph
- graph_voice
- u_cmd
- run
- pause
- resume
- stream_buses
- tap
- stream_taps
- alloc_buffer
- gen_buffer
- read_buffer
- read_into
- write_buffer
- zero_buffer
- query_buffer
- get_samples
- render
- sync
- sample_clock
- transport
- set_transport
- transport_state
- transport_play
- transport_stop
- transport_locate
- close
- clausters.defs.node
- clausters.defs.bus
- clausters.defs.clocksync
- clausters.form.element
- clausters.form.group
- clausters.form.render
- clausters.gui.guidef
- clausters.gui.editor
- clausters.gui.host
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- theServerto drive -- a live one, or one holding anOscNrtInterfacefor offline rendering. -
clock- theTempoClockthat 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;Nonetakes the config file's[client].host(default127.0.0.1). Booting is local.port- the server's UDP port;Nonetakes[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.Nonetakes the config file's[client].latency.timebase- the clock's pacing source. The default (monotonic) paces in wall-clock seconds; aSampleClockTimebaseanchors timing to the server's sample clock for drift-free scheduling.boot- start a server if none is already answering (default).Falseattaches only, never launching a process.options- aclausters.defs.ServerOptionssizing a launched server and this client's allocators alike;Noneuses the defaults.shm- the shared-memory segment for a launched server —"auto"picks one, a path forces it,Nonelaunches without one. The path is remembered soguimaps the same segment.transport- the command carrier —"tcp"(default),"udp"or"ws";Nonetakes[client].transportfrom the config.verbose- launched-server log verbosity (1/2/3->-v/-vv/-vvv; negative ->-q).data_dir- a launched server's--data-dir;Noneuses 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 existingclausters.Claustershandle to reuse; when omitted the session opens and owns a fresh embedded server and closes it onclose.
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);Noneuses the host default (57210).transport- the carrier this session talks to the host over —"tcp"(default; a/gui_deftree is not bounded by a datagram) or"udp".verbose- host log verbosity (1/2/3->-v/-vv/-vvv).data_dir- the host's--data-dirfor 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. aPbind.quant- optional quantization handed to the player -- the beat grid the routine starts on;Nonestarts 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 callbackfunc(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 ofNonematches 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, orNone(matches anything). Shorter than the message is fine — only the listed positions are checked. -
recv- theclausters.base.OscReceiverto register with; defaults todefault_osc_receiver.The responder is enabled on creation. Call
free(ordisable) 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 callbackfunc(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, orNone(matches anything). -
recv- theclausters.base.MidiReceiverto register with; defaults todefault_midi_receiver.Enabled on creation;
free(ordisable) 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 withcargo 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 withclausters --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- theTempoClockto run on; whenNoneit falls back tomain.default_clockand raises if that is unset.quant- start quantization, forwarded to the clock (seeTempoClock.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 thetimebase, 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 aSampleClockTimebaseto 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- aRoutine, anyStream, or a one-shot callable.quant- start quantization -- the routine starts on the next beat that is a multiple ofquant(e.g.4= the next bar in 4/4).Noneor0starts 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.sampleis any callable returning the current sample count (e.g.Clausters.clockorShmClient.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- theTimelineto play.clock- theTempoClockthat drives it (start it for live playback;renderit for offline).destination- where items go — aServeror aMidiServer.
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 tofaustare 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 aBoxcall are composition-stage: boxes wired as the box's signal inputs, sugar forseq(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_setholds for one block, then the server resets it to 0 (drives anenv_gengate, a sample-and-hold).rate="ir"— a scalar: read once at init and frozen; a later/n_setis ignored. Asirit may feed anirinput (rand, buffer-info UGens).lag(seconds) — smooth akrcontrol's changes with an implicit one-pole (alag/var_lagUGen the server inserts);lag_downgives 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 duration — levels
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- aServerOptionssizing the launched server and this handle's allocators alike;Noneuses the server's defaults.shm- the shared-memory segment —"auto"picks one, a path forces it,Nonelaunches without one. Remembered for a GUI to map.transport- the carrier this handle talks over —"tcp"(default),"udp"or"ws"(a--wsserver). 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;Noneuses 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:
Event— event/clip: parameters grouped into one action (internally simultaneous), with its own onset/duration. Wrapsclausters.seq.Event.Sequence— List: strict order with no concrete time, only sequence. Wraps a Python list or aPattern.Buffer— Buffer: a list at constant time (audio or control samples). Wrapsclausters.defs.Buffer.Track— Set: mixed placement of elements, a DAW track. Wrapsclausters.seq.Timeline.Generator— Function: 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 (orNonefor a pure container like aGroup).onset- start in beats relative to the context, orNone.duration- length in beats, orNone.
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- theclausters.defs.Bufferon the server.instrument- the def that plays it (itsbufcontrol gets the buffer number), orNonefor a buffer that is data only.controls- extra event parameters passed to that def (amp,rate…).onset- start in beats relative to the context, orNone.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 (astrmatching aGroupbus), or"OUT"(hardware). Used byGroup.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 bareElement(placed at offset 0).kind-CONCRETE(default) orLOGICAL.name- the composition's name — the GraphDef name for a logical group.buses- internal buses for a logical group — each aname(audio, 1 channel) or a(name, rate)/(name, rate, channels)tuple.onset- the group's own onset in its parent context, orNone.duration- the group's own duration, orNone.
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'soffset(and any nested group's) accumulates into the child's absolute beat.Track— itsTimeline's items are shifted by the placement beat.Event— placed as a single item at its beat.Sequence/Generatorwrapping an event pattern (aPbind) — bounced in the same pass (its change of state); aSequenceof 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).
menu
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 (seepeaks_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 withchannels > 1holds every channel in the one file.path— a path to a file of raw little-endianf32samples (seesamples_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_defmessage (seesamples_to_blobandGuiHost.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.tempo — beat_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-endianf32(seesamples_to_file, or an NRT render written out) the host memory-maps; the bulk path, no OSC.channelsde-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 (seesamples_to_blobandGuiHost.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_ratelabels real time, andtempo/beat_at/quantlabel 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 thewaveformuses; read the clock withServer.request("/clock")). Set it live withGuiHost.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-roll —
notes, 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-gradepianorollwidget draws the same notes with a keyboard and editing; or - an automation curve —
points, break-points over the clip's span (thebpfeditor's break-points and shape math, placed on a lane): times relative to the clip in samples, values over[min, max](exp=Truegives 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 thebpfview sends, so anclausters.seq.Automationconsumes 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 (seepeaks_cache_file); the most compact bulk path, raw samples never loaded.path— a file of raw little-endianf32the host maps (seesamples_to_file);channelsde-interleaves it,base_bucketsizes 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 — seesamples_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,pitcha MIDI note number drawn over the[min, max]window (default the 88-key range 21–108),velocity0..127(default 100),channel0..15. The notes are the MIDI messages the roll represents.osc— an iterable of(time, label)(or baretime) 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 inu.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(likemeter).
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_secs →
secs_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 — aclausters.form.group.Group(its members become the lanes) or any singleElement(one lane). -
sample_rate- the engine's sample rate; withtempoit fixes the beats↔timeline-samples conversion. -
tempo- the clock's tempo in beats per second (theTempoClockconvention — 2.0 is 120 bpm). -
quant- the musical drag grid in beats (0.25= a sixteenth);0snaps 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:applyignores 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 idsclausters.gui.host.GuiHostassigns 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", orNonefor a host with no client leg.shm- the audio server's shared-memory segment path to map (Unix only), orNoneto skip it.port- the GUI host's own port (UDP and TCP alike);Noneuses the default (57210).transport- the carrier thisGuiHosttalks over —"tcp"(default) or"udp".verbose- host log verbosity, likeclausters.defs.Server.boot.data_dir- the host's--data-dirfor 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.