From 5143c07c9931ce29fa8734183c368c99658eaa65 Mon Sep 17 00:00:00 2001 From: atagen Date: Thu, 21 May 2026 18:43:58 +1000 Subject: [PATCH] =?UTF-8?q?F5:=20document=20the=20limiter's=20rate-leakage?= =?UTF-8?q?=20caveat=20in=20PLAN=20=C2=A73.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex's review of the v0 design correctly pointed out that PLAN's "hard −0.1 dBTP ceiling" claim only holds at the filter's output, not at the speaker, when the real sink runs at a non-48 kHz native rate. PipeWire inserts a resampler at the `filter.playback → real-sink` edge, and any polynomial / windowed-sinc reconstruction can elevate inter-sample peaks slightly through its own math. The elevation is small in practice (a few tenths of a dB for a clean band-limited resampler) and the contract still holds where headroom is in control of the graph, but the README and §3 had been silent on the leak. This commit only edits docs: - §3.1 grows a "Contract scope (caveat)" paragraph next to the "Never bypassed, never disabled" hard-tier description, naming the leak, its magnitude, and the fix-for-real (filter rate matching). - §11 picks up a new tracked follow-up entry alongside the other dormant items. Scope: dynamic `FILTER_SAMPLE_RATE`, kernel rebuild on real-sink change, Layer A's block-period constant goes dynamic too. Gated on a multi-rate hardware test bench — no point shipping the refactor without something to validate it against. **v1 scope.** No code changes; no tests; clippy and tests are unaffected. --- PLAN.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/PLAN.md b/PLAN.md index 4cc64a1..4fedb21 100644 --- a/PLAN.md +++ b/PLAN.md @@ -233,6 +233,23 @@ downsampling) guarantee the contract numerically — the envelope can misbehave and the contract still holds. Never bypassed, never disabled. +**Contract scope (caveat).** The ≤ −0.1 dBTP guarantee holds at the +*filter's output*, not at the speaker. The bus filter is hardcoded +F32 stereo @ 48 kHz (`headroom-dsp::limiter`'s 4× oversampler is +sized for 48 k); when the real sink negotiates a different rate +(44.1 kHz, 96 kHz, 192 kHz), PipeWire inserts a downstream +resampler between `filter.playback` and the sink. Polynomial / +windowed-sinc resamplers can elevate inter-sample peaks slightly +through their own reconstruction, so the limiter's true-peak +guarantee leaks across that resampling stage. In practice the +elevation is small (a few tenths of a dB worst case for a clean +band-limited resampler), and the contract still holds at the bus +output where headroom is in control. **For the contract to hold +end-to-end the filter would need to match the real sink's rate +and rebuild its DSP coefficients on rate-change** — that's the +v1 work tracked as PLAN §11 "filter rate matching" (deferred from +8d, gated on a multi-rate hardware test bench). + **Soft tier — the comfort cap.** Targets a *dynamic* ceiling computed as `program_lufs + max_psr_db`. Smooth attack/release envelope so the gain reduction sounds like volume riding, not a slap. Pulls transients @@ -903,6 +920,14 @@ lost. Pick up by name when the trigger that gates them fires. for simplicity. Revisit if real users ask for it; the store-level change is a flag on the setter methods. **Dormant** — no user has asked through Phase 8. +- **Filter rate matching to the real sink.** *(F5 follow-up.)* §3.1 + documents the contract leak when the real sink runs at a + non-48 kHz native rate. Closing it requires dynamic + `FILTER_SAMPLE_RATE`, kernel rebuild on real-sink change + (compressor + limiter coefficients are rate-dependent), and + Layer A's `LAYER_A_BLOCK_DT_S` constant becoming dynamic too. + Gated on a multi-rate hardware test bench — no point shipping + the refactor without something to validate it against. **v1 scope.** - ~~**Filter playback BUSY spikes (periodic, ~10 s cadence).**~~ **Closed in 8e (`d52cd6d`).** The instrumentation added by 8e did not reproduce the ~8×-baseline outlier pattern in a ~3 min