From one node to a game soundtrack
tono builds sound from nodes that generate and process signals. Songs lower to those graphs; instruments and live hosts reuse the same DSP and playback interfaces. Follow those shared layers to understand the CLI, the web Sound Studio, and Python bindings. A Song composes, a SoundDoc renders, a Program ships, and an Engine runs sounds.
The current renderer supports schema 2 and engine 5. The same validated document
renders the same bytes, and streaming playback matches the offline bounce where supported.
Golden PCM hashes in crates/tono-core/tests/golden.rs and block-size sweeps
guard that agreement. det.rs provides deterministic f64 kernels and a
fixed-order FFT. CI verifies Linux with pinned and latest Rust; Windows/macOS builds are
no longer funded. Explicit unsupported format revisions fail validation. Investigate a
changed golden hash before updating it: a code reorganisation must preserve sound.
the smallest unit, and the ladder up
Read the ladder to follow the main authoring and playback interfaces. The module map below shows their consumers.
-
0
A sample — f32
The atom of audio. “Deterministic” in tono means literally these bits: two renders of the same document produce identical
f32bit patterns, which is why sounds are testable, diffable, and cacheable like any other build artifact.in the code: type Signal = Vec<f32> · dsp.rs (RNG, loudness, limiting) -
1
A Value — a parameter over time
Many source parameters accept a
Value: a constant, a note name resolving to Hz, or a modulator evaluated per sample. Other fields are fixed configuration. Track gain and pan automation use beat-positioned lanes in the mixer."freq": 880 // constant "freq": "A5" // note name → Hz "freq": { "slide": { "from": 880, "to": 180, "secs": 0.18, "curve": "exp" } } // per-sample modulatorin the code: dsl/mod.rs::Value + Modulator · streaming/value.rs shares value evaluation with the offline renderer -
2
A Node — the smallest sound
One enum variant = one DSP unit that evaluates to a mono signal. Two kinds: sources make signal (sine, square, noise, fm, impact, seq…); processors transform an incoming one (lowpass, reverb, drive, compress…). That’s the entire vocabulary — there is nothing else to learn at the bottom.
{ "type": "sine", "freq": 880 } // a source: the smallest complete sound { "type": "lowpass", "cutoff": 1200 } // a processor: only meaningful inside a chainin the code: dsl/node.rs (the enum + docs) · dsl/validate.rs (its rules) · render/ (the offline kernel per variant) -
3
Composition — mix · mul · chain · tracks
Combinators turn atoms into designs.
mixlayers signals,mulgates one by another (an envelope multiplies a source), andchainruns processors in series.tracksadds a mixing console with named layers, pan/gain lanes, routing, buses, and a master chain.{ "type": "mul", "inputs": [ // a “bleep”: { "type": "sine", "freq": 880 }, // tone … { "type": "env", "a": 0.002, "d": 0.08, // … shaped by an envelope "s": 0.0, "r": 0.05 } ] }in the code: Node::Mix / Mul / Chain / Tracks in dsl/node.rs · recursion in render/mod.rs::render_node -
4
A SoundDoc — the unit the whole project trades in
A root node plus metadata: name,
duration,sample_rate,seed, and schema/engine pins. The CLI renders SoundDocs, Python accepts them, and songs compile through them into Programs. The web Sound Studio edits a Song draft with grid state, then builds SoundDocs from it for playback. These are distinct authoring and runtime formats.{ "name": "blip", "duration": 0.3, "version": 2, "engine": 5, "root": { …the mul above… } }in the code: dsl/mod.rs::SoundDoc · validate() is pure (no filesystem) — loaders check sf2_paths() themselves -
5
One evaluator, two block sizes — streaming/ and render/
The node tree has one evaluator,
streaming/, which carries oscillator, filter, and mixer state between blocks.render/is the bounce: it runs that evaluator as one block as long as the document and adds the output stage, so a stream at any block size is byte-identical to the offline output by construction. The tracks mixer handles lanes, sidechain duck envelopes, buses, and insert chains. Whole-buffer or unsupported combinations — normalization, loop playback, wide stereo treatments and the SoundFont sampler — use a pre-renderedPlayerfallback.StreamGraph::blockersreports the exact blockers for a document.in the code: render/mod.rs::render_product · streaming/{value,source,proc,tracks}.rs · streaming/tests.rs::fuzz_streamed_matches_offline_byte_for_byte -
6
An AudioSource — the run-time atom
The playback seam fills interleaved stereo buffers. Engine, Mixer, Instrument, AdaptiveMusic, Performance, and the streaming source implement
AudioSource. Output adapters consume that trait. It also has a defaultresetmethod for sources that can rewind. The core does not open an audio device.pub trait AudioSource { // primary method; reset omitted here fn fill(&mut self, out: &mut [f32]) -> usize; // L,R,L,R… whole buffer, every call }in the code: runtime/source.rs · runtime/ring.rs (the wait-free Engine::split seam for game audio threads) -
7
The layers — authoring and playback over rungs 4–6
A Patch is a document plus named parameters edited by path. An Instrument turns notes into voices with the shared render/streaming machinery. A Song contains tracks, patterns, arrangement, tempo/meter maps, sections, automation, and buses; it lowers to a SoundDoc and compiles into a Program. Pattern algebra transforms notes without introducing another DSP engine. AdaptiveMusic combines rendered stems and stingers; the catalog and presets supply voices; music supplies pitches, keys, chords, and voicings.
in the code: patch.rs · instrument/ · song/ (to_doc, compile, pattern) · music.rs · adaptive/ · catalog.rs · presets.rs -
8
A Program — the immutable compiled artifact
A Program packages a resolved SoundDoc, compile target, musical metadata, resource estimates, and an integrity hash. The document owns name, sample rate, duration, and schema/engine pins. Metadata owns tempo/meter maps, pickup, sections, markers, length in bars, and named tracks in declaration order. Inspection indices are roster positions, not persistent identities. Warnings and streaming capabilities are derived. The hash covers every serialized field except itself using canonical sorted-key JSON and FNV-1a. Loading verifies the exact bundle revision, integrity, document validation, and target constraints without recompiling.
in the code: song/compile.rs::Song::compile → program.rs (Program, ProgramMeta, ResourceEstimates) · diag.rs (T-coded diagnostics) · units.rs (exact Beat; beat→frame conversion) -
9
A Performance — a running Program
A Performance combines a Program, Transport, and bounded command queue. It resolves frames, beats, bars, markers, or sections at scheduling time and executes commands during
fill, in submission order for timestamp ties. Gain changes ramp and swaps crossfade; invalid replacements leave the current source running. Stingers and replacements are prepared when scheduling. The queue holds 4096 commands and rejects/counts overflow. Normal blocks use pre-sized scratch; larger blocks can grow it once.tests/rt_alloc.rschecks steady-state fills after warm-up. Python live output pumps samples on a control thread and drains the sample ring through Speaker; the wrapper uses a nonblocking source lock and grows scratch on first use. Recorded commands replay deterministically.in the code: runtime/transport.rs (the clock) · runtime/performance.rs (the queue, swaps, metrics) · tests/rt_alloc.rs (the zero-allocation gate)
the big picture: Song → Program → Engine
A Song holds the composition. A SoundDoc is its resolved render graph. A Program adds musical metadata, estimates, and a hash to that document. Engine provides reusable sound resources and live instances; Performance runs a compiled song. Offline renderers and live sources share the underlying DSP, while the output adapter handles the device.
you compose the compiler you get back
────────────────────── ───────────────────────────────── ────────────────────────────
Song + Patches + assets ──► validate: every problem in one ──► diagnostics (stable T-codes,
(song.json, .patch.json, pass, each naming its fix each with a remediation)
optional SoundFonts) lower: the Song → a SoundDoc ──► estimates (frames, events,
stamp: the schema / engine / peak voices, memory)
program pins + canonical hash ──► offline render ──► wav, stems,
│ images, stats
▼
Program — the immutable artifact (rung 8)
▼
Renderers / Performance — the same Program on each face:
native runtime · Python
three current format revisions
Three constants describe the current formats. Schema
(SCHEMA_VERSION = 2) defines SoundDoc/Song structure.
Engine (ENGINE_VERSION = 5) defines the current DSP math.
Program (PROGRAM_VERSION = 3) defines the bundle and its
semantic hash boundary. A document stores the first two; its Program stores the bundle
revision. Loaders accept these exact revisions, reject unsupported bundles (T3001), and
verify the hash (T3002) plus document validity. Historical render paths and migrations
have been removed.
the two loops, on the ladder
The authoring loop is rungs 2→5 plus your eyes; the live loop is rungs 5→6 (or 8→9 for a compiled song) plus a speaker. Same document either way.
you write the engine you look at
───────────── ────────────────────────── ─────────────────
blip.json ──► dsl: parse + validate ──► render ──► blip.wav
(rungs 1–4) (loud errors, NaN-proof) │ blip.png ◄─ spectrogram
│ blip_wave.png
└──► analysis ──► blip.stats.json
▲ │
└────────────── refine the JSON, re-render ◄─────────┘
game loop ──► runtime: Engine / Mixer / Instrument (rung 7)
└── fill() ──► streaming (rung 5) ──► Speaker / ring ──► 🔊
song host ──► runtime: Performance (rung 9) — scheduled at bars, executed at frames
└── fill() ──► streaming (rung 5) ──► Speaker / ring ──► 🔊
the map
Cargo workspace: a virtual root manifest; the CLI, the engine and the faces
live under crates/. Everything but the CLI and tono-core is off the default
build; the workspace-wide Cargo gate still compiles and tests every
member explicitly. Row labels below are the ladder rungs each module group implements.
Faces — thin shells, each one entry point
cpal feature.start here
-
Build and prove the invariant to yourself
Start with the default core/CLI gate below. The root README lists full-workspace Linux checks and native prerequisites;
.github/workflows/ci-rust.ymladditionally checks all features, soaks, release builds, Rust docs, and installed wheels.cargo fmt --all -- --check cargo clippy --locked --all-targets -- -D warnings cargo test --locked -
Climb rungs 1–4 by hand: render your first document
Write the bleep from the ladder, render it, open the PNGs. This is the authoring loop every face wraps.
cat > blip.json <<'EOF' { "name": "blip", "duration": 0.3, "version": 2, "engine": 5, "root": { "type": "mul", "inputs": [ { "type": "sine", "freq": 880 }, { "type": "env", "a": 0.002, "d": 0.08, "s": 0.0, "r": 0.05 } ] } } EOF cargo run -p tono -- render blip.json -o out/ xdg-open out/blip.png # the spectrogram; out/blip.stats.json has the numbersThen change
"freq": 880to"A5", then to aslidemodulator, and re-render — you have now used every form of rung 1. -
Learn the node vocabulary
The SoundDoc reference covers node types; sound-effect design and song composition show their use. Read them alongside
crates/tono-core/src/dsl/node.rs; Rust API docs describe the public types. -
Hear rungs 6–7 live from code
The tono-core speaker examples (feature
cpal) are the guided tour of the layers — sounds, instruments, a live band, buses, songs, adaptive game music.cargo run -p tono-core --features cpal --example playground # walkthrough cargo run -p tono-core --features cpal --example lofi # catalog + fluent song builder cargo run -p tono-core --features cpal --example adaptive # intensity-driven stems + stinger ls crates/tono-core/examples/ # all five, plus the headless ones -
Compile a song into a Program — rungs 8–9
A song is JSON too. Compiling it validates every reference in one pass and writes the immutable bundle;
--inspectshows the pins, hash, estimates, and capabilities instead.cat > groove.song.json <<'EOF' { "name": "groove", "bpm": 120.0, "version": 2, "tracks": [ { "name": "bass", "wave": "bass", "env": { "a": 0.005, "d": 0.1, "s": 0.8, "r": 0.2, "punch": 0.0 } } ], "patterns": [ { "name": "riff", "bars": 1, "notes": [ { "step": 0, "len": 4, "pitch": "C2" }, { "step": 8, "len": 4, "pitch": "G2" } ] } ], "arrangement": [ { "track": "bass", "pattern": "riff", "bar": 0 } ] } EOF cargo run -p tono -- compile groove.song.json # writes groove.program.json cargo run -p tono -- compile groove.song.json --inspect # pins, hash, estimates, capabilitiesThen run rung 9:
cargo run -p tono-core --example composecomposes a song, compiles it and runs it through a Performance, headless and deterministic. -
Read the core in ladder order
Start at
dsl/mod.rs(SoundDoc) andrender/mod.rs(render_product), then followstreaming/and theruntime/audio seam.song/compile.rsbuilds Programs;runtime/performance.rsruns them. The decision records explain determinism, time conversion, bundle integrity, Python, and queues. -
Make a change safely
A node change touches
dsl/node.rs,dsl/validate.rs, and itsrender/kernel. Add streaming support or an explicit fallback blocker; mixer-visible behavior also touchesstreaming/tracks.rs. Run golden, streaming, validation, and applicable runtime proofs. Changing sound deliberately requires a revision decision and updated fixtures; code cleanup must preserve the current PCM pins. Deterministic math lives indet.rs.cargo fmt --all -- --check cargo clippy --locked --all-targets -- -D warnings cargo test --locked
where things live
| I want to… | Go to |
|---|---|
| add / change a node type | dsl/node.rs + dsl/validate.rs + render/ (+ streaming/ for the live path) |
| change synthesis math | current kernels live in det.rs; investigate changed PCM pins and make an explicit revision decision for intentional sound changes |
| drive audio from a game | runtime/ — Engine::load/play, Engine::split for the wait-free audio-thread seam, Mixer for buses |
| run a compiled song live | runtime/performance.rs (schedule at frames/beats/bars/sections, swaps, metrics) + runtime/transport.rs; the zero-allocation gate is tests/rt_alloc.rs |
| compile / ship a song | song/compile.rs::Song::compile → program.rs (bundle, hash, estimates); tono compile SONG.json --inspect |
| build playable instruments | instrument/ (design + live voice pool), presets.rs for factory patches |
| write music in code | song/ (tracks / patterns / buses + the pattern algebra) + music.rs (harmony) + catalog.rs — compiles to a Program |
| adaptive game music | adaptive/ — intensity stems, beat-quantized transitions, stingers, ducking |
| deterministic PCM | det.rs — current engine-5 kernels; Linux golden tests pin their output |
| speaker output in Rust | speaker.rs (tono_core::speaker::Speaker, feature cpal) — the one cpal shim; tono play, desktop and py stream through it too |
| analyze / grade a render | analysis.rs (numbers + images), review.rs (pass/warn/fail against an archetype) |
| understand any workflow | the root README, CONTRIBUTING.md, and .github/workflows/ |