atagen

headroom

public
Star0
Watch0
65250d1c123cd41b77f4c598369085e0cca83f90
1
ssh://brewery@pls.lobotomise.me:33/atagen/headroom.git

Blame: crates/headroom-dsp/src/agc.rs

614b4e43f9f3atagen2026-05-27 15:49 UTC1
//! anti-zipper smoother toward the control thread's agc target.
614b4e43f9f3atagen2026-05-27 15:49 UTC2
614b4e43f9f3atagen2026-05-27 15:49 UTC3
use crate::util::{db_to_lin, time_to_alpha};
614b4e43f9f3atagen2026-05-27 15:49 UTC4
614b4e43f9f3atagen2026-05-27 15:49 UTC5
#[derive(Debug, Clone, Copy, PartialEq)]
614b4e43f9f3atagen2026-05-27 15:49 UTC6
pub struct AgcGainConfig {
614b4e43f9f3atagen2026-05-27 15:49 UTC7
    /// smoother tau (ms). small enough to chase a 50 ms control tick without zippering,
614b4e43f9f3atagen2026-05-27 15:49 UTC8
    /// large enough not to itself act as a gain envelope.
614b4e43f9f3atagen2026-05-27 15:49 UTC9
    pub anti_zipper_ms: f32,
614b4e43f9f3atagen2026-05-27 15:49 UTC10
}
614b4e43f9f3atagen2026-05-27 15:49 UTC11
614b4e43f9f3atagen2026-05-27 15:49 UTC12
impl Default for AgcGainConfig {
614b4e43f9f3atagen2026-05-27 15:49 UTC13
    fn default() -> Self {
614b4e43f9f3atagen2026-05-27 15:49 UTC14
        Self {
614b4e43f9f3atagen2026-05-27 15:49 UTC15
            anti_zipper_ms: 5.0,
614b4e43f9f3atagen2026-05-27 15:49 UTC16
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC17
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC18
}
614b4e43f9f3atagen2026-05-27 15:49 UTC19
614b4e43f9f3atagen2026-05-27 15:49 UTC20
pub struct AgcGain {
614b4e43f9f3atagen2026-05-27 15:49 UTC21
    cfg: AgcGainConfig,
614b4e43f9f3atagen2026-05-27 15:49 UTC22
    sample_rate: f32,
614b4e43f9f3atagen2026-05-27 15:49 UTC23
    target_db: f32,
614b4e43f9f3atagen2026-05-27 15:49 UTC24
    current_db: f32,
614b4e43f9f3atagen2026-05-27 15:49 UTC25
    alpha: f32,
614b4e43f9f3atagen2026-05-27 15:49 UTC26
    enabled: bool,
614b4e43f9f3atagen2026-05-27 15:49 UTC27
}
614b4e43f9f3atagen2026-05-27 15:49 UTC28
614b4e43f9f3atagen2026-05-27 15:49 UTC29
impl AgcGain {
614b4e43f9f3atagen2026-05-27 15:49 UTC30
    #[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC31
    pub fn new(cfg: AgcGainConfig, sample_rate: f32) -> Self {
614b4e43f9f3atagen2026-05-27 15:49 UTC32
        Self {
614b4e43f9f3atagen2026-05-27 15:49 UTC33
            cfg,
614b4e43f9f3atagen2026-05-27 15:49 UTC34
            sample_rate,
614b4e43f9f3atagen2026-05-27 15:49 UTC35
            target_db: 0.0,
614b4e43f9f3atagen2026-05-27 15:49 UTC36
            current_db: 0.0,
614b4e43f9f3atagen2026-05-27 15:49 UTC37
            alpha: time_to_alpha(cfg.anti_zipper_ms, sample_rate),
614b4e43f9f3atagen2026-05-27 15:49 UTC38
            enabled: true,
614b4e43f9f3atagen2026-05-27 15:49 UTC39
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC40
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC41
614b4e43f9f3atagen2026-05-27 15:49 UTC42
    pub fn set_target_db(&mut self, db: f32) {
614b4e43f9f3atagen2026-05-27 15:49 UTC43
        if db.is_finite() {
614b4e43f9f3atagen2026-05-27 15:49 UTC44
            self.target_db = db;
614b4e43f9f3atagen2026-05-27 15:49 UTC45
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC46
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC47
614b4e43f9f3atagen2026-05-27 15:49 UTC48
    /// disabling pushes target to 0 dB so an active boost/cut unwinds at the smoother rate
614b4e43f9f3atagen2026-05-27 15:49 UTC49
    /// rather than snapping.
614b4e43f9f3atagen2026-05-27 15:49 UTC50
    pub fn set_enabled(&mut self, enabled: bool) {
614b4e43f9f3atagen2026-05-27 15:49 UTC51
        self.enabled = enabled;
614b4e43f9f3atagen2026-05-27 15:49 UTC52
        if !enabled {
614b4e43f9f3atagen2026-05-27 15:49 UTC53
            self.target_db = 0.0;
614b4e43f9f3atagen2026-05-27 15:49 UTC54
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC55
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC56
614b4e43f9f3atagen2026-05-27 15:49 UTC57
    pub fn set_config(&mut self, cfg: AgcGainConfig) {
614b4e43f9f3atagen2026-05-27 15:49 UTC58
        self.cfg = cfg;
614b4e43f9f3atagen2026-05-27 15:49 UTC59
        self.alpha = time_to_alpha(cfg.anti_zipper_ms, self.sample_rate);
614b4e43f9f3atagen2026-05-27 15:49 UTC60
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC61
614b4e43f9f3atagen2026-05-27 15:49 UTC62
    #[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC63
    pub fn config(&self) -> AgcGainConfig {
614b4e43f9f3atagen2026-05-27 15:49 UTC64
        self.cfg
614b4e43f9f3atagen2026-05-27 15:49 UTC65
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC66
614b4e43f9f3atagen2026-05-27 15:49 UTC67
    #[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC68
    pub fn current_db(&self) -> f32 {
614b4e43f9f3atagen2026-05-27 15:49 UTC69
        self.current_db
614b4e43f9f3atagen2026-05-27 15:49 UTC70
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC71
614b4e43f9f3atagen2026-05-27 15:49 UTC72
    #[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC73
    pub fn target_db(&self) -> f32 {
614b4e43f9f3atagen2026-05-27 15:49 UTC74
        self.target_db
614b4e43f9f3atagen2026-05-27 15:49 UTC75
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC76
614b4e43f9f3atagen2026-05-27 15:49 UTC77
    #[must_use]
614b4e43f9f3atagen2026-05-27 15:49 UTC78
    pub fn enabled(&self) -> bool {
614b4e43f9f3atagen2026-05-27 15:49 UTC79
        self.enabled
614b4e43f9f3atagen2026-05-27 15:49 UTC80
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC81
614b4e43f9f3atagen2026-05-27 15:49 UTC82
    pub fn process_frame(&mut self, l: f32, r: f32) -> (f32, f32) {
614b4e43f9f3atagen2026-05-27 15:49 UTC83
        self.current_db += self.alpha * (self.target_db - self.current_db);
614b4e43f9f3atagen2026-05-27 15:49 UTC84
        if l == 0.0 && r == 0.0 {
614b4e43f9f3atagen2026-05-27 15:49 UTC85
            return (0.0, 0.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC86
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC87
        let gain = db_to_lin(self.current_db);
614b4e43f9f3atagen2026-05-27 15:49 UTC88
        (l * gain, r * gain)
614b4e43f9f3atagen2026-05-27 15:49 UTC89
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC90
}
614b4e43f9f3atagen2026-05-27 15:49 UTC91
614b4e43f9f3atagen2026-05-27 15:49 UTC92
#[cfg(test)]
614b4e43f9f3atagen2026-05-27 15:49 UTC93
mod tests {
614b4e43f9f3atagen2026-05-27 15:49 UTC94
    use super::*;
614b4e43f9f3atagen2026-05-27 15:49 UTC95
    use crate::util::lin_to_db;
614b4e43f9f3atagen2026-05-27 15:49 UTC96
614b4e43f9f3atagen2026-05-27 15:49 UTC97
    const SR: f32 = 48_000.0;
614b4e43f9f3atagen2026-05-27 15:49 UTC98
614b4e43f9f3atagen2026-05-27 15:49 UTC99
    #[test]
614b4e43f9f3atagen2026-05-27 15:49 UTC100
    fn unity_at_zero_db() {
614b4e43f9f3atagen2026-05-27 15:49 UTC101
        let mut agc = AgcGain::new(AgcGainConfig::default(), SR);
614b4e43f9f3atagen2026-05-27 15:49 UTC102
        for _ in 0..100 {
614b4e43f9f3atagen2026-05-27 15:49 UTC103
            let (l, r) = agc.process_frame(0.5, -0.3);
614b4e43f9f3atagen2026-05-27 15:49 UTC104
            assert!((l - 0.5).abs() < 1e-6);
614b4e43f9f3atagen2026-05-27 15:49 UTC105
            assert!((r - -0.3).abs() < 1e-6);
614b4e43f9f3atagen2026-05-27 15:49 UTC106
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC107
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC108
614b4e43f9f3atagen2026-05-27 15:49 UTC109
    #[test]
614b4e43f9f3atagen2026-05-27 15:49 UTC110
    fn smooths_toward_target() {
614b4e43f9f3atagen2026-05-27 15:49 UTC111
        let mut agc = AgcGain::new(AgcGainConfig::default(), SR);
614b4e43f9f3atagen2026-05-27 15:49 UTC112
        agc.set_target_db(6.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC113
        // After ~5 ms (one anti-zipper tau), current_db should be in
614b4e43f9f3atagen2026-05-27 15:49 UTC114
        // the ~63% region.
614b4e43f9f3atagen2026-05-27 15:49 UTC115
        let samples = (0.005 * SR) as usize;
614b4e43f9f3atagen2026-05-27 15:49 UTC116
        for _ in 0..samples {
614b4e43f9f3atagen2026-05-27 15:49 UTC117
            let _ = agc.process_frame(0.0, 0.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC118
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC119
        let cur = agc.current_db();
614b4e43f9f3atagen2026-05-27 15:49 UTC120
        assert!(
614b4e43f9f3atagen2026-05-27 15:49 UTC121
            (cur - 6.0 * 0.63).abs() < 0.5,
614b4e43f9f3atagen2026-05-27 15:49 UTC122
            "expected ~3.8 dB after one tau, got {cur}"
614b4e43f9f3atagen2026-05-27 15:49 UTC123
        );
614b4e43f9f3atagen2026-05-27 15:49 UTC124
        // Settle.
614b4e43f9f3atagen2026-05-27 15:49 UTC125
        for _ in 0..(SR as usize) {
614b4e43f9f3atagen2026-05-27 15:49 UTC126
            let _ = agc.process_frame(0.0, 0.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC127
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC128
        assert!((agc.current_db() - 6.0).abs() < 0.01);
614b4e43f9f3atagen2026-05-27 15:49 UTC129
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC130
614b4e43f9f3atagen2026-05-27 15:49 UTC131
    #[test]
614b4e43f9f3atagen2026-05-27 15:49 UTC132
    fn applies_gain_to_signal() {
614b4e43f9f3atagen2026-05-27 15:49 UTC133
        let mut agc = AgcGain::new(AgcGainConfig::default(), SR);
614b4e43f9f3atagen2026-05-27 15:49 UTC134
        agc.set_target_db(6.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC135
        // Run long enough to settle.
614b4e43f9f3atagen2026-05-27 15:49 UTC136
        for _ in 0..(SR as usize) {
614b4e43f9f3atagen2026-05-27 15:49 UTC137
            let _ = agc.process_frame(0.0, 0.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC138
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC139
        let (l, r) = agc.process_frame(0.5, 0.5);
614b4e43f9f3atagen2026-05-27 15:49 UTC140
        // +6 dB = factor of ~2.0.
614b4e43f9f3atagen2026-05-27 15:49 UTC141
        assert!((l / 0.5 - 2.0).abs() < 0.05, "got {l}");
614b4e43f9f3atagen2026-05-27 15:49 UTC142
        assert!((r / 0.5 - 2.0).abs() < 0.05);
614b4e43f9f3atagen2026-05-27 15:49 UTC143
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC144
614b4e43f9f3atagen2026-05-27 15:49 UTC145
    #[test]
614b4e43f9f3atagen2026-05-27 15:49 UTC146
    fn disable_unwinds_back_to_unity() {
614b4e43f9f3atagen2026-05-27 15:49 UTC147
        let mut agc = AgcGain::new(AgcGainConfig::default(), SR);
614b4e43f9f3atagen2026-05-27 15:49 UTC148
        agc.set_target_db(6.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC149
        for _ in 0..(SR as usize) {
614b4e43f9f3atagen2026-05-27 15:49 UTC150
            let _ = agc.process_frame(0.0, 0.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC151
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC152
        assert!((agc.current_db() - 6.0).abs() < 0.01);
614b4e43f9f3atagen2026-05-27 15:49 UTC153
614b4e43f9f3atagen2026-05-27 15:49 UTC154
        agc.set_enabled(false);
614b4e43f9f3atagen2026-05-27 15:49 UTC155
        for _ in 0..(SR as usize) {
614b4e43f9f3atagen2026-05-27 15:49 UTC156
            let _ = agc.process_frame(0.0, 0.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC157
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC158
        assert!(agc.current_db().abs() < 0.01, "got {}", agc.current_db());
614b4e43f9f3atagen2026-05-27 15:49 UTC159
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC160
614b4e43f9f3atagen2026-05-27 15:49 UTC161
    #[test]
614b4e43f9f3atagen2026-05-27 15:49 UTC162
    fn rejects_non_finite_target() {
614b4e43f9f3atagen2026-05-27 15:49 UTC163
        let mut agc = AgcGain::new(AgcGainConfig::default(), SR);
614b4e43f9f3atagen2026-05-27 15:49 UTC164
        agc.set_target_db(3.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC165
        agc.set_target_db(f32::NAN);
614b4e43f9f3atagen2026-05-27 15:49 UTC166
        assert!((agc.target_db() - 3.0).abs() < 1e-6);
614b4e43f9f3atagen2026-05-27 15:49 UTC167
        agc.set_target_db(f32::INFINITY);
614b4e43f9f3atagen2026-05-27 15:49 UTC168
        assert!((agc.target_db() - 3.0).abs() < 1e-6);
614b4e43f9f3atagen2026-05-27 15:49 UTC169
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC170
614b4e43f9f3atagen2026-05-27 15:49 UTC171
    #[test]
614b4e43f9f3atagen2026-05-27 15:49 UTC172
    fn lin_round_trip_check() {
614b4e43f9f3atagen2026-05-27 15:49 UTC173
        // Sanity: after settling, gain at target_db should produce
614b4e43f9f3atagen2026-05-27 15:49 UTC174
        // peak that matches lin_to_db.
614b4e43f9f3atagen2026-05-27 15:49 UTC175
        let mut agc = AgcGain::new(AgcGainConfig::default(), SR);
614b4e43f9f3atagen2026-05-27 15:49 UTC176
        agc.set_target_db(-6.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC177
        for _ in 0..(SR as usize) {
614b4e43f9f3atagen2026-05-27 15:49 UTC178
            let _ = agc.process_frame(0.0, 0.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC179
        }
614b4e43f9f3atagen2026-05-27 15:49 UTC180
        let (l, _) = agc.process_frame(1.0, 1.0);
614b4e43f9f3atagen2026-05-27 15:49 UTC181
        assert!((lin_to_db(l) - -6.0).abs() < 0.05);
614b4e43f9f3atagen2026-05-27 15:49 UTC182
    }
614b4e43f9f3atagen2026-05-27 15:49 UTC183
}