Sessions
A Session is the client's ergonomic entry point: one object that owns a Server and a TempoClock together and drives them as a unit. It exists so that the everyday case — "set up a place to make sound, play a pattern, hear it or render it" — is a couple of lines, without giving up the design that makes this client flexible.
Why it exists
SuperCollider's sclang is convenient largely because of globals: a default Server, a default clock, an implicit "current environment". You type Synth("x") and it just goes somewhere. That convenience has a cost — there is only ever one of each, so you cannot, say, run a live take and an offline render in the same script without them fighting over the same global state.
This client deliberately has none of those globals. The clock does timing and nothing else, a Server is an ordinary object you construct, and routines find their clock through thread-local context rather than a global. That keeps real-time and offline work independent, but it means the convenient one-liner is gone: you would otherwise wire a server, a clock and a timebase together by hand every time.
Session gives the convenience back explicitly. It is just an object that holds a Server and a TempoClock and offers play / render / run, plus two factories that pick sensible defaults. Because it is a plain object and not a global, you can have as many as you like.
Kinds of session
You almost always build a session with one of the factories rather than the constructor. They differ only in where the bytes go — offline into a score, over the network to a separate server, or by function call to a server running inside this process — and otherwise behave identically.
Session.nrt() is an offline (non-real-time) session. Its server accumulates a timetagged score instead of sending anything, and render() turns that score into samples through the renderer bundled with the package. No server process and no audio device are involved.
from clausters import Session, main
from clausters.seq import Pbind, Pseq, Pwhite
main.seed(1) # one root seed reproduces every random draw in the script
session = Session.nrt(tempo=2.0)
session.play(Pbind(
degree=Pseq([0, 2, 4, 7, 4, 2], repeats=2),
dur=0.25,
amp=Pwhite(0.1, 0.2),
))
samples, frames = session.render(sample_rate=48000.0, channels=2)
print(f"{frames} frames; peak {max(abs(s) for s in samples):.3f}")
Randomness (Pwhite, Prand, clausters.uniform/choice/…) always draws
from one seedable context — the running routine's generator, derived from
the context that created it — never from per-pattern seeds, so main.seed(n)
makes a whole piece reproducible end to end (see
Routines and clocks).
Session.live() is a real-time session that sounds on a device over the network. By default it ensures a server the way nrt() ensures a renderer: if one already answers 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. So the everyday live case is one line, whether or not a server is already up; a server the session started is stopped when the session is closed or the interpreter exits, and one it merely attached to is left alone.
The two protocols have two roles: UDP finds the server, TCP talks to it. The boot-or-attach probe rides UDP (discovery stays zero-config, and any scsynth-style tool can do the same), and the session's command interface then connects over TCP by default — reliable, ordered, and not bounded by the ~64 KB UDP datagram, so a large def, a whole GuiDef tree or a megabyte buffer read travels as one frame (the ceiling is the server's --max-frame, advertised in /server_info). Pass transport="udp" (or set [client].transport in the config) for a datagram-only setup — e.g. a server started with --no-tcp — or transport="ws" for a --ws server. Timing is unaffected either way: it rides on bundle timetags and /sched, never on arrival time.
from clausters import Session
from clausters.seq import Pbind, Pseq, Pwhite
with Session.live(tempo=2.0, latency=0.1) as session: # attaches, or boots one
session.play(Pbind(
instrument="default",
degree=Pseq([0, 2, 4, 7, 4, 2], repeats=2),
dur=0.25,
amp=Pwhite(0.1, 0.2),
))
session.run(3.5) # advance the clock in real time, then stop
Arguments worth knowing on live():
boot— whether to start a server when none is up (defaultTrue). Passboot=Falsefor plain attach-only behavior: connect to a server you launched yourself (possibly remote), never starting a process. When booting,options(aServerOptionsthat sizes the launched server and this client's allocators),shm("auto", a path, orNone), andverbose/data_dir/server_argsshape the launched process.latency— seconds added to each event's timetag so it arrives a touch ahead of its play time and the server sounds it on time rather than late.0.0means "as soon as possible"; a small value such as0.1is typical for a live take.timebase— the clock's pacing source. The default paces in wall-clock seconds (monotonic); passing aSampleClockTimebaseanchors timing to the server's own sample clock, for drift-free, sample-accurate scheduling. See The client, layer by layer for the timebase distinction.
Session.embed() is a real-time session whose server runs inside this process. It opens the whole engine — audio device and all — through the native library bundled with the package, and OSC is delivered by function call rather than over a socket. There is no separate process to start and no port to connect to, yet it is real-time: it sounds on a device just like live().
from clausters import Session
from clausters.seq import Pbind, Pseq, Pwhite
with Session.embed(tempo=2.0, latency=0.1) as session:
session.play(Pbind(
instrument="default",
degree=Pseq([0, 2, 4, 7, 4, 2], repeats=2),
dur=0.25,
amp=Pwhite(0.1, 0.2),
))
session.run(3.5)
It takes the same latency and timebase as live(), plus workers (engine threads for parallel node processing) and an optional server= to reuse an existing clausters.Clausters handle instead of opening a fresh one. Because the server lives in this process, you can read its sample clock and control buses directly through session.server.interface.server (the Clausters handle), with no OSC round trip.
There is deliberately no separate "spawn" factory: launching a server is not a different kind of session, just live()'s default behavior, so the option lives on live rather than multiplying constructors. See Launching the server and the GUI below for the details and the object-level Server.boot / GuiHost.boot.
Which factory to reach for:
| Factory | Server | Use it when |
|---|---|---|
Session.nrt() | none (a score + renderer) | rendering offline — a plot, an analysis, a .wav; no device. |
Session.embed() | in-process (bundled library) | making sound from one script, no setup — but the engine shares this process. |
Session.live() | a separate process (booted if needed) | the everyday real-time / live-coding case — a real, separate server a GUI or another client can also talk to. Boots one if none is up; boot=False attaches only. |
Driving a session
Once you have a session, a small set of methods drives it. Some are offline-only, some live-only — the table makes the split explicit.
| Call | Kind | What it does |
|---|---|---|
play(pattern, quant=None) | all | Plays an event pattern (e.g. a Pbind) on this session's clock and server. Returns the EventStreamPlayer. quant is the beat grid to start on; None starts immediately. |
render(sample_rate, channels) | offline | Drains the clock logically (no waiting), then renders the score. Returns (samples, frames) — interleaved float32 in a stdlib array('f'), and the frame count. |
run(seconds) | real-time | Starts the clock, advances it in real time for seconds, then stops. Returns self. (live and embed.) |
start() / stop() | real-time | Start or stop the real-time clock yourself when run (which does both) is not enough. Both return self. |
close() | all | Closes the underlying Server and its interface (for embed, shuts the in-process server down). |
play is the one call shared by both kinds, and that is the whole point — see the next section.
Because close() releases the server, the idiomatic shape for a live session is a context manager, which closes it for you even if the block raises:
with Session.live(tempo=2.0) as session:
session.play(my_pattern)
session.run(4.0)
# server closed here
An offline session holds no socket and renders synchronously, so the context manager is optional there — though harmless, and tidy if you mix both.
One rule carries over from the rest of the client: a routine must never block the clock thread. render() and run() are driver calls you make from your own (main) thread, not from inside a routine — they advance the clock, so calling them from a routine the clock is running would deadlock it.
The same code, live or offline
The reason a session draws no line between "play" and "render" is the client's central design property, the seam: a Server holds one communication interface, and which interface it holds — not your pattern, not your clock — decides where the bytes go. A live session's server holds a network interface (TCP by default); an offline session's server holds a score-accumulating one. Everything above the server is identical.
So the only thing that changes between a live take and an offline render is which factory you called. You can write the pattern once and run it both ways:
from clausters import Session
from clausters.seq import Pbind, Pseq, Pwhite
def phrase():
return Pbind(
instrument="default",
degree=Pseq([0, 2, 4, 7, 4, 2], repeats=2),
dur=0.25,
amp=Pwhite(0.1, 0.2),
)
# Offline: capture it to samples.
offline = Session.nrt(tempo=2.0)
offline.play(phrase())
samples, frames = offline.render()
# Live: hear the very same phrase.
with Session.live(tempo=2.0, latency=0.1) as live:
live.play(phrase())
live.run(3.5)
This is exactly what the two shipped examples do — offline_render.py and live_udp.py share their pattern and differ only in the session factory. See Examples.
Several sessions at once
Because a session is an ordinary object rather than a global, more than one can be live at the same time. The common case is rendering a score offline (for a plot, an analysis or a .wav) right next to a live session you are listening to, in a single script:
live = Session.live(tempo=2.0, latency=0.1)
plot = Session.nrt(tempo=2.0)
live.play(phrase())
plot.play(phrase())
live.run(2.0) # heard in real time
samples, frames = plot.render() # captured offline, no audio device
live.close()
The two never interfere: each has its own server, its own clock and its own interface. With globals this is impossible; here it is the default.
Launching the server and the GUI
Live coding wants the whole system reachable from one interpreter: a separate audio server (so it survives a client restart, is shared, and keeps the audio thread out of Python) and, often, the visual server beside it — without opening three terminals or spelling out a shared-memory path. Session.live and Session.gui do exactly that, and everything they start is torn down when the session is closed or the interpreter exits — a normal exit, an unhandled exception, or an abandoned handle garbage-collected. Nothing is left running.
session.gui() launches the clausters-gui visual server and returns a GuiHost connected to it — the GUI parallel of live() booting a server. You never spell out an address or a segment: the host is started with its client leg pointed at this session's server and mapping the same shared-memory segment the server was booted with, so meters, scopes and playheads read the engine with no per-frame messages. The host is owned by the session and stopped on close.
from clausters import Session
from clausters.gui import window, label
session = Session.live() # attaches, or boots a server (segment auto-chosen)
gui = session.gui() # clausters-gui, wired to that server + same segment
win = gui.open(window(label(1, text="hello"), title="Panel", w=320, h=120))
gui.set(1, text="edited live") # edit the open window
gui.close(win) # close it
session.close() # stops whatever the session started; leaving the interpreter would too
GuiHost opens, edits and closes windows: open sends a window-rooted GuiDef and returns its id, set edits a live widget, and close closes it (close_all closes every window still open). Repeated session.gui() calls return the same host.
The visual server binary ships bundled in the same package as the audio server (built from the independent clients/gui workspace, stripped), so there is nothing extra to install — the launcher finds it out of the box. In a source checkout a binary built under clients/gui/target is used, and CLAUSTERS_GUI_BIN overrides the lookup. See Getting started for building a lighter, server-only install.
Without a Session: Server.boot and GuiHost.boot
If you are not using a Session, the server and the GUI host each carry their own launch and teardown, so you don't juggle a separate process object. Server.boot() starts a server process and returns a connected Server that owns it (its close() stops the process); GuiHost.boot() does the same for the visual server, returning a started GuiHost (its stop() stops the process). Both also die with the interpreter.
from clausters.defs import Server
from clausters.gui import GuiHost, window, label
server = Server.boot() # a server process starts
gui = GuiHost.boot(server=f"{server.target.host}:{server.target.port}", shm=server.shm)
gui.open(window(label(1, text="hi"), title="Panel", w=320, h=120))
# ...
gui.stop() # stops the clausters-gui process
server.close() # stops the server process
This is exactly what Session.live/gui use internally — the session just bundles them with a clock.
The raw processes
One level lower, clausters.launch exposes the processes themselves — ServerProcess and GuiProcess — for when you want to own them directly (e.g. a custom Server/GuiHost wiring). Both are context managers, both register the same exit hooks, and default_shm_path() picks a segment (None on platforms where shared memory does not apply); server_is_up() is the probe live uses to decide boot-or-attach.
from clausters import ServerProcess, GuiProcess
from clausters.gui import GuiHost
with ServerProcess() as server_proc: # clausters --shm <auto>
with GuiProcess(server=f"{server_proc.host}:{server_proc.port}",
shm=server_proc.shm) as gui_proc:
host = GuiHost(port=gui_proc.port).start()
... # both processes stop when the `with` blocks exit
When you don't need a Session
A Session is sugar over two objects you can always build yourself. When you want more control — several servers behind one clock, a clock shared across subsystems, or a custom interface — skip the factory and wire them directly:
from clausters.base import TempoClock
from clausters.defs import Server
server = Server("127.0.0.1", 57110, latency=0.1)
clock = TempoClock(tempo=2.0)
phrase().play(clock, server) # what Session.play does for you
clock.run(3.5)
server.close()
Session adds no behaviour of its own — it only bundles these two and forwards to them — so reaching for the longer form costs nothing and loses nothing.
See also
- API reference — the generated reference for
Sessionand every method. - The client, layer by layer — where the
Server, the clock and the seam fit in the whole client. - Routines and clocks — the level below a session: driving a
Routine, aTempoClockand aServeryourself. - Timing models — the ways a clock keeps time (wall-clock, sample-locked, shared transport) and how to observe each.
- Examples —
offline_render.pyandlive_udp.py, the session in runnable form.