Two self-review follow-ups from the F1/F2 commits surfaced by an audio-correctness pass over `244367c..HEAD`. Both are low-risk, high-signal fixes — the kind that prevent users complaining about "weird little blips" when changing profiles or unmuting the compressor. ## Audio-gap on `PwCommand::ReevaluateAll` `enqueue_route` used to unconditionally drop `managed_route_links[node_id]` before the next 50 ms drain tick rebuilt. With `object.linger="false"` on the link-factory props, dropping the `Link` proxy destroys the actual graph link immediately. Result: every profile reload / route set / route unset / bypass toggle caused a 21–42 ms audio dropout on every already-correctly-routed stream — even when nothing about the stream's routing had actually changed. `managed_route_links` now carries the target sink name alongside the `Link` proxies (new `ManagedRoute` struct: `target_sink_name` + `links`). `enqueue_route` only drops when the target name differs from the stored one; the unchanged case leaves the live links intact, and `apply_pending_routes`' destroy/create loop sees its `want_set` already satisfied and exits as a no-op. Live verification: pw-cat /tmp/sine streaming through processed, issue `route set firefox bypass` (rule that doesn't touch pw-cat). Before this fix the link IDs would flip; after, link IDs 83 + 122 stayed identical across `reevaluating all known streams streams=1` in the daemon log. Listener-visible gap goes from one quantum to zero. The path that *does* change target (real bypass toggle, real-sink hot-swap, a rule edit that flipped the stream's decision) still drops + rebuilds — the gap there is unavoidable without a core-sync barrier or a "transition through both old and new links" choreography. That's acceptable: the user explicitly asked for the route change in those cases. ## Compressor envelope reset across `enabled` transition F6 made `compressor.enabled = false` actually skip processing, but didn't touch the envelope or RMS state — which kept ticking forward during enabled periods, sat stale during disabled periods, and then bled out via release on the first re-enable. With long release times this meant up to ~100 ms of artificial gain reduction after switching from a `transparent` profile back to a compressing one, for no acoustic reason. `Compressor::set_config` now detects the `disabled → enabled` transition and resets `envelope_db`, `rms_state`, and `last_gr_db` so the compressor starts from a clean state — same behaviour as a freshly-constructed `Compressor::new(...)`. Same-enabled transitions (parameter tweaks while enabled, or no-op `set_config` while disabled) leave the envelope alone, so live tweaks still don't pop. Regression test `compressor::tests::enable_transition_resets_stale_envelope` winds the envelope hot, toggles disable+enable via two `set_config` calls, then asserts the next quiet sample produces zero GR. Without the reset that assertion would fail by ~5+ dB. ## Verified 190 tests pass (+1 for the envelope reset; +0 for the link fix — exercised by live-smoke since it's about side-effect timing not value); clippy clean at `-D warnings --all-targets`. |
||
|---|---|---|
| .. | ||
| benches | ||
| src | ||
| Cargo.toml | ||
| README.md | ||
headroom-dsp
DSP kernels for Headroom. Pure Rust, no dependencies.
Limiter— feed-forward true-peak brickwall with configurable oversampling (1/2/4/8×), lookahead, hold, and release.Compressor— log-domain feed-forward with peak or RMS detector, soft knee, attack/release, and optional auto-makeup.AttackRelease— exponential envelope follower (peak / inverse-gain modes).DelayLine,SlidingMaxBuffer,PolyphaseUpsampler,PolyphaseDownsampler— supporting building blocks.
All processors are allocation-free in their process_* methods.
Construction allocates; do not construct in the audio thread.
License
MPL-2.0.