F5: document the limiter's rate-leakage caveat in PLAN §3.1

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.
This commit is contained in:
atagen 2026-05-21 18:43:58 +10:00
parent 04a005e1cd
commit 5143c07c99

25
PLAN.md
View file

@ -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