[Recording] Move raw capture off the WAV container to headerless s16le #321

Closed
opened 2026-08-25 20:38:41 +00:00 by claude-bot · 1 comment
Contributor

Severity: HIGH. Found in the August 2026 session lifecycle review (#319). This is a required companion to the wall-clock anchoring fix — that fix is not shippable without it.

The defect

Once gaps are filled with silence, each raw track becomes wall-clock length rather than speech length. Raw 48 kHz stereo s16 is 192 KB/s, so a track reaches 4.15 GB at the default max_recording_hours = 6 (bot/questboard_bot/config.py:38-39). That exceeds WAV's 4 GB RIFF u32 size limit, and Python's wave module patches the RIFF/data sizes at close() — so the write fails at the very end, after the whole session has been captured.

Today this is latent, because tracks are compressed to speech only and never get near the limit.

Evidence

  • bot/questboard_bot/cogs/recording.py:130-143wave.open(..., "wb") per speaker, writeframesraw per packet
  • bot/questboard_bot/cogs/recording.py:222-231 — sizes patched once at close()
  • bot/questboard_bot/config.py:38-39max_recording_hours default 6
  • bot/questboard_bot/utils/audio.py:85-91 — the FFmpeg conversion to 16 kHz mono

Proposed fix

Write raw capture as a headerless .s16le file and convert with an explicit input format:

ffmpeg -f s16le -ar 48000 -ac 2 -i {raw} -ar 16000 -ac 1 {out}.wav

This removes the container size ceiling entirely, and it also allows seek()-based sparse holes for gap silence — on ext4/overlay2 that makes long silences cost no disk at all, which is a meaningful saving once every track is wall-clock length.

The shipped 16 kHz mono WAV is 32 KB/s, i.e. ~690 MB for a 6-hour session, comfortably inside RIFF limits. No change needed there.

Acceptance criteria

  • Raw per-speaker capture writes headerless .s16le, not WAV
  • Conversion passes -f s16le -ar 48000 -ac 2 explicitly on the input
  • A simulated 6-hour gap-filled capture completes without a container error
  • The crash-recovery path (see the raw-WAV recovery issue in v4.1.0) reads the headerless format
  • Optional but preferred: gap silence uses sparse seek() rather than writing zeroes
**Severity: HIGH.** Found in the August 2026 session lifecycle review (#319). This is a **required companion** to the wall-clock anchoring fix — that fix is not shippable without it. ## The defect Once gaps are filled with silence, each raw track becomes wall-clock length rather than speech length. Raw 48 kHz stereo s16 is 192 KB/s, so a track reaches **4.15 GB at the default `max_recording_hours = 6`** (`bot/questboard_bot/config.py:38-39`). That exceeds WAV's 4 GB RIFF `u32` size limit, and Python's `wave` module patches the RIFF/data sizes at `close()` — so the write fails at the very end, after the whole session has been captured. Today this is latent, because tracks are compressed to speech only and never get near the limit. ## Evidence - `bot/questboard_bot/cogs/recording.py:130-143` — `wave.open(..., "wb")` per speaker, `writeframesraw` per packet - `bot/questboard_bot/cogs/recording.py:222-231` — sizes patched once at `close()` - `bot/questboard_bot/config.py:38-39` — `max_recording_hours` default 6 - `bot/questboard_bot/utils/audio.py:85-91` — the FFmpeg conversion to 16 kHz mono ## Proposed fix Write raw capture as a **headerless `.s16le` file** and convert with an explicit input format: ``` ffmpeg -f s16le -ar 48000 -ac 2 -i {raw} -ar 16000 -ac 1 {out}.wav ``` This removes the container size ceiling entirely, and it also allows `seek()`-based sparse holes for gap silence — on ext4/overlay2 that makes long silences cost no disk at all, which is a meaningful saving once every track is wall-clock length. The shipped 16 kHz mono WAV is 32 KB/s, i.e. ~690 MB for a 6-hour session, comfortably inside RIFF limits. No change needed there. ## Acceptance criteria - [ ] Raw per-speaker capture writes headerless `.s16le`, not WAV - [ ] Conversion passes `-f s16le -ar 48000 -ac 2` explicitly on the input - [ ] A simulated 6-hour gap-filled capture completes without a container error - [ ] The crash-recovery path (see the raw-WAV recovery issue in v4.1.0) reads the headerless format - [ ] Optional but preferred: gap silence uses sparse `seek()` rather than writing zeroes
Author
Contributor

Shipped — closing as part of a v3.11.5 bookkeeping sweep. The work landed; the issues were never closed.

Evidence in the implementing code, bot/questboard_bot/cogs/recording.py:108:

Files are headerless .s16le ({session_id}_{user_id}_raw.s16le in the audio_temp base dir), not WAV: gap-filled 48 kHz stereo is 192 KB/s of wall clock, which reaches 4.15 GB at the 6-hour cap and overflows WAV's 4 GB RIFF size field (#321). _process hands them to FFmpeg with an explicit input format and resamples to 16 kHz mono.

The migration path is handled too: main.py:341,357 sweeps both *_raw.s16le and *_raw.wav, the latter explicitly for "pre-#321 builds [that] left behind and may still be on an upgraded volume".

Shipped — closing as part of a v3.11.5 bookkeeping sweep. The work landed; the issues were never closed. Evidence in the implementing code, `bot/questboard_bot/cogs/recording.py:108`: > Files are **headerless** `.s16le` (`{session_id}_{user_id}_raw.s16le` in the audio_temp base dir), not WAV: gap-filled 48 kHz stereo is 192 KB/s of wall clock, which reaches 4.15 GB at the 6-hour cap and overflows WAV's 4 GB RIFF size field (#321). `_process` hands them to FFmpeg with an explicit input format and resamples to 16 kHz mono. The migration path is handled too: `main.py:341,357` sweeps both `*_raw.s16le` and `*_raw.wav`, the latter explicitly for "pre-#321 builds [that] left behind and may still be on an upgraded volume".
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#321
No description provided.