Contributing
Working on Clausters itself. The internals — threads, memory lifecycle, invariants, how to add a UGen — are in Architecture; this chapter is the practical setup around it.
Build and test
cargo build # core, no audio device needed to compile
cargo test # the full core suite
cargo clippy --all-targets # lints (kept clean)
cargo doc --no-deps # the API reference
The core must always build and test without any feature and without libfaust installed, and with any combination of the def-family features synth/faust (both, either alone, or neither — most integration suites are gated on synth, so the featureless run is thin by design).
System build dependencies (Ubuntu 26.04)
# default build (PipeWire audio + ALSA-seq MIDI)
sudo apt install build-essential pkg-config libasound2-dev libpipewire-0.3-dev clang
# only for the matching optional feature:
sudo apt install libjack-jackd2-dev # --features midi-jack
# plain-ALSA build (no PipeWire libs):
# cargo build --no-default-features --features synth,realtime,midi
pipewire is a default feature (the target systems always ship PipeWire), so
the default binary hard-links libpipewire and expects it at runtime. clang
is only used at build time by bindgen (it parses the PipeWire C headers to
generate bindings); the toolchain itself stays rustc + gcc. The midi-jack
build links against jackd2's libjack but resolves to PipeWire's libjack
under pw-jack at runtime.
The faust feature (default)
The FaustDef family is on by default, so a plain cargo build / cargo test needs libfaust built with the LLVM backend on the machine. Distro packages (e.g. Ubuntu's libfaust2t64) ship without it and without headers, so it is built from source and installed under ~/.local, once. The reproducible recipe is in BUILD.md (the "Building libfaust from source" section). build.rs locates the library through FAUST_PREFIX, falling back to ~/.local, then /usr/local.
FAUST_PREFIX=~/.local cargo test # the prefix is only needed if it is not ~/.local
cargo test --no-default-features --features synth,realtime # no libfaust needed
Every compilation FFI call goes through faust::compiler::ffi_lock(), which serializes libfaust within the process (instantiating from an already-compiled factory is concurrency-safe). That lock is what lets the Faust suites run in the ordinary parallel test harness; a SIGSEGV in a faust_* suite is the signature of an FFI path that skipped it.
build.rs also writes a DT_RPATH of $ORIGIN, $ORIGIN/../_libs and the build prefix, so the artifacts are relocatable and a distribution can bundle libfaust and its libLLVM beside them (which is exactly what the Python wheel does — see clients/python/build_native.py). DT_RPATH rather than DT_RUNPATH: only the former is inherited by transitive dependencies, and it is libfaust, not our binary, that needs to find libLLVM.
Real-time safety (non-negotiable)
Engine::process_block and everything it calls must never allocate, free, lock or do I/O. Commands arrive fully pre-built over a lock-free FIFO; freed memory leaves through the garbage FIFO and is dropped on the network thread. tests/rt_safety.rs guards this with assert_no_alloc — run it after touching anything on the audio path. Denormals are flushed to zero on every processing thread (dsp::denormals::flush_to_zero() plus -ftz 2 for Faust); see the RT-safety notes in Architecture.
End-to-end testing in a sandboxed shell
Some CI/sandbox environments isolate the network between shell invocations: a server started in one invocation is unreachable from the next, and UDP packets to localhost are silently lost. Always run the server and client in the same invocation — server in the background with &, then the client, then kill it:
(./target/debug/clausters & PID=$!; sleep 1.5; \
./target/debug/examples/osc_ping status quit; kill $PID 2>/dev/null)
OSC decoding
All incoming OSC bytes decode through osc::decode_packet, the single entry point (every transport funnels through it). It is a thin wrapper over rosc::decoder::decode_udp — keep that one door so decoding and any future hardening stay in one place.
Continuous integration
GitHub Actions runs the same checks this page asks for by hand
(.github/workflows/ci.yml); every job maps to a local command, so a red job
is reproducible with the same line:
- lint —
cargo fmt --checkandcargo clippy --workspace --all-targets -- -D warnings, for the root workspace and forclients/gui(its own workspace). The tree is clippy-clean; keep it that way. - test —
cargo test --workspaceacross the def-family feature matrix: default,--no-default-features,--no-default-features --features synth, and default plusembed. - gui —
cargo testinclients/guiplus the wasm build gate (clients/gui/check-wasm.sh). - python —
python clients/python/build_native.py --debug, thenpytestinclients/python. - docs — both mdBooks with the same mdBook version Read the Docs uses, and the pydoc-markdown API page for the client book.
- faust — the default
cargo testcovers it, with libfaust built from source at the commit pinned in the workflow (the recipe inthird_party/BUILD-FAUST.md) and cached; a cache hit makes the job cheap. Upgrading libfaust = bumpingFAUST_SHAthere after verifying locally. The featureless / SynthDef-only runs (--no-default-features) are what keep the build green on a machine with no libfaust.
Releases and publishing
- Tagged releases (
.github/workflows/release.yml): pushing av*tag builds the self-contained Python wheel (client + embedded server + standalone binary; Linux x86_64 for now) and a server-binary tarball, publishes the wheel to PyPI and attaches both to a GitHub release. PyPI auth is Trusted Publishing (OIDC — no stored token): the PyPI project must list this repository with workflowrelease.ymland environmentpypias a trusted publisher, and the repository needs apypienvironment. There is deliberately no sdist: the package compiles cdylibs from the Rust workspace, which an sdist ofclients/pythonwould not contain. - Read the Docs hosts the two books as two projects pointing at the same
repository, each selecting its config under Settings → Advanced → Path to
configuration file: the server/workspace book uses the repo-root
.readthedocs.yaml, the Python client book usesclients/python/.readthedocs.yaml.
Conventions
- Language: everything under
src/,tests/andexamples/(code, comments, strings, test names) is in English, as are the roadmap filesPLAN.md/clients/PLAN.mdand the design recorddocs/decisions.md.GUIA.md(root +clients/python/GUIA.md) stays in Spanish (maintainer-facing manual smoke checklists); this book and the rustdoc are the English documentation. - Closing a milestone means, where applicable: code plus tests, a clear commit message (that is the record of what shipped — there is no separate per-milestone log), the
PLAN.mdroadmap checkbox updated, developer/user docs where the feature touches them (docs/architecture.md,docs/schemas.md, module docs), and a commentedexamples/entry for user-facing features. Add a short entry todocs/decisions.mdonly when a choice has non-obvious context, and aGUIA.mdsmoke step only when a new human-audible/visual behavior appears — neither is a per-milestone obligation.
Project skills
Domain knowledge lives in .claude/skills/: realtime-audio (RT thread rules, lock-free patterns, cpal), scsynth-osc (the OSC protocol and node-tree model), ugen-dsp (UGen DSP algorithms), audio-testing (testing audio without ears: NRT, golden files, signal asserts, no-alloc), faust-embedding (the libfaust C API and lifecycles), and faust-language (writing Faust and transposing it to the Signal/Box APIs — sample-level feedback, physical modeling). Process skills: clausters-python (idiomatic client use), clausters-gui (the GUI host and GuiDefs) and documentation (how to write and place docs — the Diataxis split, the generated API references, and the dev/decision docs).
Editing this book
The book sources are the Markdown files in docs/; docs/SUMMARY.md is the table of contents and book.toml the config. Build and preview with mdBook:
cargo install mdbook
mdbook serve # live preview at http://localhost:3000
mdbook build # generates ./book (git-ignored)
mdbook test # type-checks Rust code snippets