# ADR 0001 — Mixed PCM ring buffer over per-SSRC Opus storage Date: 2026-07-08 Status: Accepted ## Context The voice-recording feature (`/record`) must keep a rolling, fixed-length window of everything spoken in the bot's current voice channel, so that a flush produces a single mixed audio file of (up to) the window length. songbird delivers incoming voice as one Opus stream per speaking SSRC. There were two viable storage strategies: - **(A) Mixed PCM ring buffer.** Decode each packet to PCM, sum all speakers into one pre-allocated ring of mono i16 samples. Flush = encode the ring. - **(B) Raw Opus packets per SSRC.** Store each SSRC's compressed packets in a timestamped deque. Flush = decode all, mix by timestamp, then encode. ## Decision Store a single mixed mono-s16 PCM ring buffer (**A**), sized `sample_rate × window_seconds` (~86 MB at 48 kHz / 15 min), allocated only while the bot is connected and recording is enabled. ## Rationale - **Matches the stated mental model.** The feature was specified as "a blob in memory that has the newest 15 minutes, deletes the old, appends the new at the end." (A) is the literal implementation of that; (B) stores N separate per-user deques, which contradicts the "single blob, append at end" shape. - **Trivial flush.** (A) flushes by handing a known-size s16le buffer to one ffmpeg encode. (B) requires bulk decode + timestamp-aligned mixing at flush time — heavier and more failure-prone (drift, late packets, anchor resets). - **songbird already decodes.** With the `receive` feature and `DecodeMode::Decode(Mono, 48kHz)`, songbird's `CoreEvent::VoiceTick` delivers already-decoded, jitter-buffered mono i16 every 20 ms — so (A) needs no per-SSRC Opus decoder, no `opus2` direct dependency, and no f32→s16 step. (B) would re-derive work songbird already does. - **The memory cost is opt-in.** `RECORDING_ENABLED` defaults to `true` but a memory-constrained host sets it `false` and pays 0. A host that enables recording accepts the ~86 MB. `RECORDING_WINDOW_SECONDS` lets such a host shrink the ring (e.g. 300 s → ~29 MB) without code changes. ## Consequences - ~86 MB resident while recording (at the default 15 min / 48 kHz window). Acceptable for a single-guild self-hosted bot; tunable down via the window. - Multi-speaker overlap is mixed with `i16::saturating_add`, which can clip slightly on very loud overlaps rather than wrap to garbage. The right trade for a 50 Hz × N-packet hot path. - No per-speaker attribution is possible from the stored buffer (the mix is lossy by design). If attribution is ever needed, (B) would have to be revisited — this decision is the main thing that makes that reversal costly.