Previously each WebSocket client tuning to a DAB multiplex spawned its own
EtiDecoder (OFDM demodulator). With 5 concurrent clients, 5 identical
demodulators competed for CPU (~52-55% on a Pi 5), causing OFDM lock-loss
(coarse time shift ~130,000) and audible breakup for all listeners.
Fix: introduce DabDecoderManager (singleton) that maintains one
SharedDabDecoder per (sdr_id, center_freq) pair, reference-counted by
client connects/disconnects. Each client gets an independent pycsdr.Buffer
reader into the shared ETI output (same fan-out mechanism as SpectrumThread
in owrx/fft.py). Per-client DablinModule and MetaForwarder are retained so
service selection and programme metadata remain per-client.
Results measured on Raspberry Pi 5 with 4 concurrent clients:
- CPU: ~52-55% → ~35%
- Coarse time shift: ~130,000 (losing lock) → 1-8 (solid lock)
- Lock lost events: frequent → zero
- Programme list in browser: unaffected
Adds docs/shared-dab-decoder.md explaining the problem, architecture, and
known limitations (service-switch race when last client changes service).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>