F6: honour compressor.enabled in the DSP
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.
This commit is contained in:
parent
244367ccb9
commit
03edb17180
2 changed files with 40 additions and 0 deletions
|
|
@ -150,6 +150,7 @@ impl Profile {
|
|||
MakeupGain::Db(v) => Some(v),
|
||||
};
|
||||
CompressorConfig {
|
||||
enabled: self.compressor.enabled,
|
||||
threshold_db: self.compressor.threshold_db,
|
||||
ratio: self.compressor.ratio,
|
||||
knee_db: self.compressor.knee_db,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ pub enum Detector {
|
|||
/// Compressor parameters.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CompressorConfig {
|
||||
/// Master enable. When `false`, [`Compressor::process_frame`]
|
||||
/// returns the input unchanged and reports zero gain reduction.
|
||||
/// The compressor's envelope state is *not* reset while disabled,
|
||||
/// so a stale envelope can briefly affect the first few samples
|
||||
/// after re-enabling — but with typical release time constants
|
||||
/// (tens to hundreds of ms) any residual transient is below the
|
||||
/// audibility threshold.
|
||||
pub enabled: bool,
|
||||
/// Threshold in dBFS. Inputs above this start compressing.
|
||||
pub threshold_db: f32,
|
||||
/// Compression ratio (>= 1.0).
|
||||
|
|
@ -40,6 +48,7 @@ pub struct CompressorConfig {
|
|||
impl Default for CompressorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
threshold_db: -24.0,
|
||||
ratio: 2.5,
|
||||
knee_db: 6.0,
|
||||
|
|
@ -120,6 +129,13 @@ impl Compressor {
|
|||
|
||||
/// Process one stereo frame.
|
||||
pub fn process_frame(&mut self, left: f32, right: f32) -> (f32, f32) {
|
||||
if !self.cfg.enabled {
|
||||
// Pass through untouched and report no reduction, so the
|
||||
// bus meters reflect "compressor off" rather than the
|
||||
// last value before disable.
|
||||
self.last_gr_db = 0.0;
|
||||
return (left, right);
|
||||
}
|
||||
let det_lin = match self.cfg.detector {
|
||||
Detector::Peak => left.abs().max(right.abs()),
|
||||
Detector::Rms => {
|
||||
|
|
@ -256,6 +272,29 @@ mod tests {
|
|||
assert_eq!(cfg.ratio, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_compressor_passes_signal_through_unchanged() {
|
||||
// Same hot input that would compress hard in the enabled
|
||||
// test above. With `enabled: false`, output equals input
|
||||
// exactly (no makeup gain, no reduction), and the reporter
|
||||
// shows zero GR — so the `transparent` and `bypass-all`
|
||||
// profiles actually do what their name claims.
|
||||
let cfg = CompressorConfig {
|
||||
enabled: false,
|
||||
threshold_db: -20.0,
|
||||
ratio: 4.0,
|
||||
makeup_db: Some(12.0),
|
||||
..CompressorConfig::default()
|
||||
};
|
||||
let mut c = Compressor::new(cfg, 48_000.0);
|
||||
for _ in 0..1_000 {
|
||||
let (l, r) = c.process_frame(0.5, 0.5);
|
||||
assert_eq!(l, 0.5);
|
||||
assert_eq!(r, 0.5);
|
||||
}
|
||||
assert_eq!(c.gain_reduction_db(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_curve_at_threshold_with_soft_knee() {
|
||||
// At exactly threshold, soft knee contributes exactly half the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue