Bouncing: the piece as a file

The last change of state: the whole piece, rendered offline. The promise made on the rendering page comes due here — RT and NRT are the same flattening, differing only in the destination, so the render is sample-identical to what you heard. There is no export path; there is render, pointed at an offline session.

The offline session

offline = Session.nrt(tempo=TEMPO)
offline.server.add_synthdef(sampler())
offline.server.add_synthdef(drone())
offline.server.read_buffer(wav)          # the take, on the offline server

The offline server needs everything the live one had: the defs, and the take loaded from its file (in NRT these are scored at time 0 — the renderer compiles and loads before time advances).

Server-bound state moves explicitly

One category of thing does not retarget itself: resources that live on a server — the take's buffer number, and the automation's control buffer and bus. The model is destination-agnostic; those numbers are not. Two of them matter here:

  • the take was the live server's first buffer, and the offline server's read_buffer above allocated its first — the numbers line up because both allocators started fresh;
  • the sweep was prepared on the live server, and the drone's voice captured the bus index when you built its event. Release the live resources, prepare against the offline server (scored at time 0), and re-point the voice at the bus the offline pass allocated — then render, the same verb, the same tree, an offline destination:
sweep.free(server)                           # return the live buffer and bus
sweep.prepare(offline.server)                # score its buffer + bus at time 0
voice.wraps["freq_bus"] = sweep.bus.index    # the voice reads the offline bus
playhead = song.render(offline.server, offline.clock)

Render and write

samples, frames = offline.render(sample_rate=SR, channels=2)
print(f"rendered {frames} frames ({frames / SR:.2f} s)")
import struct, wave

out_path = str(Path(tempfile.mkdtemp(prefix="clausters-")) / "piece.wav")
with wave.open(out_path, "wb") as w:
    w.setnchannels(2)
    w.setsampwidth(2)
    w.setframerate(int(SR))
    w.writeframes(b"".join(
        struct.pack("<h", int(max(-1.0, min(1.0, s)) * 32767))
        for s in samples))
print(f"wrote {out_path}")

Listen to it (ffplay -autoexit, or any player): the piece, with every edit you made on screen — the moved clips, the trimmed take, the redrawn sweep — because the offline pass flattened the same model your window was editing. Both renders converge on the same score; the engine that plays it is the same engine that played it.

To keep working live afterwards, give the sweep its live resources back the same way (sweep.free(offline.server) is not needed — the offline session is done — just sweep.prepare(server) and re-point voice.wraps["freq_bus"]). And when you are done for the day:

session.close()      # stops the clock, the GUI host and a server it booted

Where to go from here

  • The finished, self-contained script form of this piece — on-screen transport buttons, a poll loop, the same four lanes — is clients/python/examples/gui_composer.py. Read it next to this section and you will recognize every line.
  • Composition: the arrangement and the multitrack editor — the concepts, argued rather than exercised.
  • The glossary — every term this section used, pinned in one place.
  • The API reference for clausters.form, clausters.gui and clausters.seq; the wire protocol the editor speaks is in the Clausters server book.