feat: duration and seeking for in-progress FLAC recordings

FLAC duration cannot be derived from byte size (variable compression),
so unlike WAV the header cannot be patched from st_size alone. Instead,
every FLAC frame header carries its own frame/sample number: read the
last 64 KB of the growing file, scan backwards for a frame sync,
CRC-8-verify the header to reject false matches in compressed data,
and compute the exact samples recorded so far. STREAMINFO
total_samples (36 bits at a fixed offset) is rewritten in the served
bytes only - the on-disk file is never touched.

Overhead: one tail read per /stream request, active files only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 12:37:55 +02:00
parent fa055fc80a
commit 16dd7cbe51
3 changed files with 128 additions and 6 deletions
+1 -1
View File
@@ -38,6 +38,6 @@ Dependencies: `requests` (streams), `numpy` + `soundfile` (FLAC output and FLAC/
- **Analysis cache:** results stored as `<analyses-dir>/<file>.analysis.json` keyed by threshold+min_gap; orphans pruned at web startup. In Docker the recordings mount is **read-only** for the web container, so the cache uses a separate `./analyses` bind mount. The `threshold` and `min_gap` keys MUST stay first in the cache JSON — `_cached_analysis_params()` reads only the first 256 bytes to avoid parsing the large embedded result.
- **Analyze responses:** `/api/analyze` returns `rms_display` (~800 points), never the full per-window RMS list — the UI doesn't use it and it is ~45x larger.
- **HTTP/1.1 keep-alive:** `_Handler.protocol_version = 'HTTP/1.1'`; every response path must set an accurate `Content-Length`. `_copy_to_response()` force-closes the connection if it under-delivers (file truncated mid-serve).
- **Live playback:** for files listed in status.json, `/stream/` patches the WAV header on the fly (`_live_wav_header`) so the browser sees the duration recorded so far and can seek; responses get `Cache-Control: no-store`.
- **Live playback:** for files listed in status.json, `/stream/` patches the header on the fly so the browser sees the duration recorded so far and can seek; responses get `Cache-Control: no-store`. WAV: `_live_wav_header` derives sizes from the byte count. FLAC: `_live_flac_header` parses the sample count out of the last frame header in the file tail (CRC-8-verified to reject false sync matches) and rewrites STREAMINFO total_samples — duration is NOT derivable from byte size for FLAC.
- **Path safety:** every file parameter in `web.py` goes through `_safe_path()`, which resolves and verifies the path stays inside the recordings dir.
- **dsnoop in Docker:** sharing the soundcard requires `asound.conf` on the host *and* `ipc: host` in docker-compose (dsnoop uses shared memory across the container boundary).