aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--docs/adr/0001-pcm-ring-buffer-over-opus-storage.md54
1 files changed, 54 insertions, 0 deletions
diff --git a/docs/adr/0001-pcm-ring-buffer-over-opus-storage.md b/docs/adr/0001-pcm-ring-buffer-over-opus-storage.md
new file mode 100644
index 0000000..24c9f98
--- /dev/null
+++ b/docs/adr/0001-pcm-ring-buffer-over-opus-storage.md
@@ -0,0 +1,54 @@
1# ADR 0001 — Mixed PCM ring buffer over per-SSRC Opus storage
2
3Date: 2026-07-08
4Status: Accepted
5
6## Context
7
8The voice-recording feature (`/record`) must keep a rolling, fixed-length
9window of everything spoken in the bot's current voice channel, so that a
10flush produces a single mixed audio file of (up to) the window length.
11
12songbird delivers incoming voice as one Opus stream per speaking SSRC. There
13were two viable storage strategies:
14
15- **(A) Mixed PCM ring buffer.** Decode each packet to PCM, sum all speakers
16 into one pre-allocated ring of mono i16 samples. Flush = encode the ring.
17- **(B) Raw Opus packets per SSRC.** Store each SSRC's compressed packets in a
18 timestamped deque. Flush = decode all, mix by timestamp, then encode.
19
20## Decision
21
22Store a single mixed mono-s16 PCM ring buffer (**A**), sized
23`sample_rate × window_seconds` (~86 MB at 48 kHz / 15 min), allocated only
24while the bot is connected and recording is enabled.
25
26## Rationale
27
28- **Matches the stated mental model.** The feature was specified as "a blob in
29 memory that has the newest 15 minutes, deletes the old, appends the new at
30 the end." (A) is the literal implementation of that; (B) stores N separate
31 per-user deques, which contradicts the "single blob, append at end" shape.
32- **Trivial flush.** (A) flushes by handing a known-size s16le buffer to one
33 ffmpeg encode. (B) requires bulk decode + timestamp-aligned mixing at flush
34 time — heavier and more failure-prone (drift, late packets, anchor resets).
35- **songbird already decodes.** With the `receive` feature and
36 `DecodeMode::Decode(Mono, 48kHz)`, songbird's `CoreEvent::VoiceTick` delivers
37 already-decoded, jitter-buffered mono i16 every 20 ms — so (A) needs no
38 per-SSRC Opus decoder, no `opus2` direct dependency, and no f32→s16 step.
39 (B) would re-derive work songbird already does.
40- **The memory cost is opt-in.** `RECORDING_ENABLED` defaults to `true` but a
41 memory-constrained host sets it `false` and pays 0. A host that enables
42 recording accepts the ~86 MB. `RECORDING_WINDOW_SECONDS` lets such a host
43 shrink the ring (e.g. 300 s → ~29 MB) without code changes.
44
45## Consequences
46
47- ~86 MB resident while recording (at the default 15 min / 48 kHz window).
48 Acceptable for a single-guild self-hosted bot; tunable down via the window.
49- Multi-speaker overlap is mixed with `i16::saturating_add`, which can clip
50 slightly on very loud overlaps rather than wrap to garbage. The right trade
51 for a 50 Hz × N-packet hot path.
52- No per-speaker attribution is possible from the stored buffer (the mix is
53 lossy by design). If attribution is ever needed, (B) would have to be
54 revisited — this decision is the main thing that makes that reversal costly.