Ring determinism
The behavioral contract for the seam between the audio thread and the render thread, and for the analysis that consumes it. It states what must be true, not how it is implemented.
Where it lives:
rlx-ring/(the lock-free SPSC ring, a zero-dependency workspace member so Miri can gate itsunsafe),core/src/audio.rs(format validation and the producer/consumer handles) andcore/src/dsp/(the analysis). Governing ADRs: 0001 (the core owns the analysis and the audio/render split), 0049 (normalization is analysis-layer state), 0050 (the beat clock and the gated bar trio) and 0139 (the waveform is levelled and its divisor published). How the contract got here is at the end, under Provenance.
Invariants
- The audio thread MUST NOT block, heap-allocate, lock a contended mutex, log, or do file I/O. It hands samples to the ring and returns. An underrun is an audible click; a blocked callback is a stutter. (CLAUDE.md non-negotiables)
- The seam between audio and render MUST be the lock-free SPSC ring buffer — exactly one producer (the audio thread) and one consumer (the render thread). Neither loop is driven directly off the other; the ring absorbs the cadence mismatch. (CLAUDE.md)
- The ring MUST be data-race-free under concurrent single-producer/single-consumer access.
It lives in
rlx-ring— a workspace member with no dependencies, so Miri can interpret itsunsafewithout compiling the wgpu/naga graph — and the CImirijob proves it on every push. (Plan 0005,.github/workflows/ci.yml) - DSP analysis (FFT bins, onset envelope, tempo/BPM estimate, the band axis, the normalized
levels, the levelled waveform trace and its published gain, the beat/bar clock) MUST be a
pure function of the input stream: no wall-clock reads, no unseeded randomness, no ambient
state. The same sequence of hops fed to a freshly
constructed
AnalyzerMUST produce a bit-identical sequence of analysis frames. (CLAUDE.md “determinism where it’s testable”) - That bit-identity is scoped to one build on one machine (ADR-0071, Plan 0060). It is exactly what
analysis_is_deterministicasserts, and it asserts it by running both analyzers in a single process. Reproduction of the same bits across architectures, toolchains or optimization levels is deliberately not claimed:f32::sinlowers to the platform libm andrustfftdispatches NEON on aarch64 where it dispatches AVX/SSE on x86_64, so identical input legitimately lands tens of ULP apart — themacos-26-arm64runner reads within2e-5relative of the x86_64*_rawlevels. A test that freezes measured bits is therefore a measurement pinned to its architecture, not a consequence of this invariant. - The unit of determinism is the stream, not the window (Plan 0048 / ADR-0049 + ADR-0050).
The
*_rawlevels and BPM still resolve from their window, butbass/mid/treb/onset, thespectrumarray and thewaveformtrace all divide by a running peak, andbeat_index/bar_indexcount, so the same window read at two points in a stream legitimately yields different frames.waveform_gainis that divisor made readable: it is history-dependent by the same mechanism, and multiplying the trace by it recovers the window’s own absolute amplitude. History-dependence is the contract here; ambient nondeterminism is still forbidden, and the distinction is whatanalysis_is_deterministicasserts by running the whole signal through two fresh analyzers. - Any visual jitter or randomness, when wanted, MUST be explicitly seeded so a scene is reproducible from its seed. (CLAUDE.md)
- Sample rate, channel count, and buffer size MUST be validated once where audio enters the core; the hot DSP path downstream trusts them. (CLAUDE.md “validate at the boundary”)
Scenarios
- WHEN the audio thread receives a block of PCM frames THEN it writes them into the ring and returns without allocating or locking; the render thread reads them on its own cadence.
- WHEN the render thread consumes faster than the audio thread produces (ring empties) THEN the consumer gets “no new data” and reuses the last analysis — it does not block the producer.
- WHEN the producer outruns the consumer (ring fills) THEN the overflow policy is applied at the ring (oldest samples dropped) rather than blocking the audio thread.
- WHEN a fixed sine-wave window is fed to the FFT path THEN the spectrum places energy in the expected bin(s) deterministically — the same window always yields the same bins (the behavioral claim the DSP tests defend).
- WHEN the same audio window is analyzed twice THEN the onset envelope, tempo/BPM estimate, and band energies are bit-for-bit identical (no wall-clock, no unseeded RNG in the path).
- WHEN
cargo +nightly miri test -p rlx-ringruns (the CImirijob, Plan 0005) THEN the SPSC ring’s cross-thread test reports no undefined behavior.
Known gaps / honest nulls
Nothing crosses the whole seam in a test.Closed by Plan 0032 Phase 1 (ADR-0033):core/tests/chain.rspushes synthetic PCM into a realaudio::intakepair in capture-callback-sized bursts, drains it throughpop_samples, feeds a realAnalyzerand renders — so the ring-to-pixels claim above is assertion, not architecture. What that suite still does not cover is the standalone’s own drain loop, which is shell code outsidecore.- This spec does not contract the tempo estimator’s accuracy (how close BPM is to ground truth) — only its determinism. Better tempo tracking is a named later roadmap item.
- The overflow/underrun policy is stated behaviorally here; the exact capacity (~100 ms at
48 kHz per the ring’s sizing) and drop mechanics live in
core/src/audio.rs.
Provenance
How this contract reached its current shape. None of it is a rule — the invariants above are — and it is here so a reader who wants the history has it in one place rather than in the header.
Reconciled 2026-08-28, through five plans:
- Plan 0005 extracted the ring into
rlx-ringand put the Miri UB gate in CI. - Plan 0032 gave the ring-to-analyzer-to-renderer seam a test.
- Plan 0048 brought analysis v2 — the dual-resolution axis, running normalization and the beat/downbeat clock — which is what moved the determinism invariant from window to stream.
- Plan 0060 scoped the bit-identity clause to one build on one machine, after a frozen-literal test read as though cross-architecture reproduction followed from it.
- Plan 0127 brought the waveform into the levelled outputs, so the trace is history-dependent too and publishes its divisor.
Built from a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.