The profile schema accepted `[compressor] enabled = false` (and the
`transparent` and `bypass-all` profiles set it) but the flag was
parsed and dropped — `build_compressor_config()` never threaded
it through to `CompressorConfig`, and `Compressor::process_frame`
had no enable branch. Result: the "compressor and AGC bypassed"
claim in `transparent.toml`'s description was a lie; the
compressor ran on every sample regardless of the profile knob.
Surfaced by Codex's review of the project.
Changes
- `headroom_dsp::CompressorConfig` gains `pub enabled: bool`
(default true). `Compressor::process_frame` early-returns
`(left, right)` and resets `last_gr_db = 0.0` when disabled,
so bus meters / `gain_reduction_db()` report the truthful
"compressor off" state instead of the stale last value.
- `headroom_core::profile::Profile::build_compressor_config`
threads `self.compressor.enabled` into the materialised
`CompressorConfig`. Live profile reload picks this up
automatically — the next `set_config` push from
`setting.set` / `profile.use` flips the audio thread.
- Regression unit test `disabled_compressor_passes_signal_through_unchanged`:
drive a -6 dBFS sine that would compress hard with enabled +
aggressive thresholds, assert output equals input exactly and
GR is zero.
What this does NOT change
- **Limiter has no `enabled` flag** and intentionally remains
always-on. It is the daemon's hard contract (the -0.1 dBTP
ceiling on the processed route, advertised in the README and
in PLAN §3). Users who don't want limiting should route
bypass; the `bypass-all.toml` profile's own comment confirms
the limiter is "still configured as a fail-safe in case a
stream lands on the processed sink anyway."
Verified
186 tests pass (+1 for the disable path); clippy clean at
-D warnings --all-targets.
Live A/B against `pw-cat /tmp/sine` (-6 dBFS sine into
processed): default profile compresses at -4.5 dB GR;
`headroom profile use transparent` flips to 0.00 dB GR
exactly on the next meter tick.
Closes the last gap before Phase 5's monitor TUI: per-app meter
events already publish on the meters topic via the registry watcher;
bus-level DSP meters now also publish.
4g — Bus meters
headroom_core::meters::BusMetrics is an Arc<parking_lot::Mutex<...>>
snapshot owned by the playback callback (try_lock; skip on
contention) and read by the AGC controller on each 50 ms tick.
Carries: compressor GR, limiter total/soft/hard GR, true peak. The
AGC controller combines these with its ebur128 readings (momentary,
short-term, integrated) and the current smoothed AGC target, then
publishes a headroom_ipc::MeterTick on Topic::Meters.
Publish cadence honours profile.meters.publish_hz, capped at the
AGC tick rate (20 Hz). Lower publish_hz throttles to every Nth
tick.
Mode::I added to the AGC's EbuR128 so loudness_global() is
available without a second ebur128 instance. Bounded cost — a
histogram walk per call, <=20 Hz.
LUFS values are sanitised to a -200.0 dB floor via
finite_or_floor() — ebur128 returns -inf (not Err) for "no usable
measurement yet," and non-finite f32 can't survive JSON
serialisation (serde_json renders as null).
Housekeeping shipped alongside
headroom-client moved from [dependencies] to [dev-dependencies] in
headroom-core — it's only used inside ipc::server's tests. Verified
by full clippy + test run; production builds no longer pull it in.
Pre-existing clippy nits cleared (limiter.rs x5, app_level.rs,
ipc/ops.rs, pw/filter.rs). All field_reassign_with_default or
assign_op_pattern in test code; stage-6 commit ran clippy without
--all-targets so these slipped through.
Verified
178 tests passing (28 dsp + 48 dsp + 20 ipc + 106 core including
+2 new meters tests + 4 client). Clippy clean at default level with
-D warnings --all-targets.
Smoke test: monitor meters subscription receives 20 Hz MeterTick
events with the expected JSON shape (all fields finite).