diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-30 15:09:51 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-30 15:09:51 +0200 |
| commit | 7a565f377203ba8e01574952014a23dfa8d94fa3 (patch) | |
| tree | 9bf25a5f618fd79b9c9911ea670f9d4b3d2bba99 /docs/adr | |
| parent | fix assistant and senator (diff) | |
| download | senator-sperma-7a565f377203ba8e01574952014a23dfa8d94fa3.tar.gz senator-sperma-7a565f377203ba8e01574952014a23dfa8d94fa3.zip | |
fix: auto completion timing out before ready + fix preexisting warns
Diffstat (limited to '')
| -rw-r--r-- | docs/adr/0001-pcm-ring-buffer-over-opus-storage.md | 54 |
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 | |||
| 3 | Date: 2026-07-08 | ||
| 4 | Status: Accepted | ||
| 5 | |||
| 6 | ## Context | ||
| 7 | |||
| 8 | The voice-recording feature (`/record`) must keep a rolling, fixed-length | ||
| 9 | window of everything spoken in the bot's current voice channel, so that a | ||
| 10 | flush produces a single mixed audio file of (up to) the window length. | ||
| 11 | |||
| 12 | songbird delivers incoming voice as one Opus stream per speaking SSRC. There | ||
| 13 | were 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 | |||
| 22 | Store a single mixed mono-s16 PCM ring buffer (**A**), sized | ||
| 23 | `sample_rate × window_seconds` (~86 MB at 48 kHz / 15 min), allocated only | ||
| 24 | while 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. | ||
