Skip to main content

embedded_dsp/
dynamics.rs

1//! Dynamics Range Control: Compressor, Limiter, Expander, and Noise Gate.
2//!
3//! Provides real-time dynamics processing with soft-knee curves, decoupled attack/release
4//! ballistics, and integration with the [`DspNode`] streaming framework.
5
6#[allow(unused_imports)]
7use crate::math::FloatMath;
8use crate::pipeline::DspNode;
9
10/// Dynamics Range Compressor with soft knee and make-up gain.
11#[derive(Debug, Clone, Copy)]
12pub struct DynamicsCompressor {
13    threshold_db: f32,
14    ratio: f32,
15    knee_db: f32,
16    makeup_gain_linear: f32,
17    attack_coeff: f32,
18    release_coeff: f32,
19    envelope_db: f32,
20}
21
22impl DynamicsCompressor {
23    /// Creates a new dynamics compressor.
24    ///
25    /// - `threshold_db`: Threshold level in dBFS (e.g. `-20.0`).
26    /// - `ratio`: Compression ratio (e.g. `4.0` for 4:1).
27    /// - `knee_db`: Soft knee width in dB (e.g. `6.0` for smooth transition, `0.0` for hard knee).
28    /// - `attack_s`: Attack time in seconds (e.g. `0.005` for 5 ms).
29    /// - `release_s`: Release time in seconds (e.g. `0.1` for 100 ms).
30    /// - `makeup_gain_db`: Post-compression make-up gain in dB (e.g. `4.0`).
31    /// - `sample_rate_hz`: Audio sample rate in Hz (e.g. `48000.0`).
32    pub fn new(
33        threshold_db: f32,
34        ratio: f32,
35        knee_db: f32,
36        attack_s: f32,
37        release_s: f32,
38        makeup_gain_db: f32,
39        sample_rate_hz: f32,
40    ) -> Self {
41        let attack_coeff = (-1.0 / (attack_s.max(1e-5) * sample_rate_hz)).exp();
42        let release_coeff = (-1.0 / (release_s.max(1e-5) * sample_rate_hz)).exp();
43        let makeup_gain_linear = (10.0f32).powf(makeup_gain_db / 20.0);
44
45        Self {
46            threshold_db,
47            ratio: ratio.max(1.0),
48            knee_db: knee_db.max(0.0),
49            makeup_gain_linear,
50            attack_coeff,
51            release_coeff,
52            envelope_db: 0.0,
53        }
54    }
55
56    /// Process a single audio/signal sample through the compressor.
57    pub fn process(&mut self, input: f32) -> f32 {
58        let abs_in = input.abs();
59        let input_db = if abs_in > 1e-6 {
60            20.0 * abs_in.log10()
61        } else {
62            -120.0
63        };
64
65        // Static compression characteristic with quadratic soft knee
66        let target_gain_db = if self.knee_db > 0.0
67            && (2.0 * (input_db - self.threshold_db)).abs() <= self.knee_db
68        {
69            let delta = input_db - self.threshold_db + self.knee_db / 2.0;
70            -(1.0 - 1.0 / self.ratio) * delta * delta / (2.0 * self.knee_db)
71        } else if input_db > self.threshold_db {
72            -(input_db - self.threshold_db) * (1.0 - 1.0 / self.ratio)
73        } else {
74            0.0
75        };
76
77        // Smooth gain change via attack/release ballistics
78        if target_gain_db < self.envelope_db {
79            // Attack (gain decreasing / compressing)
80            self.envelope_db = self.attack_coeff * self.envelope_db + (1.0 - self.attack_coeff) * target_gain_db;
81        } else {
82            // Release (gain restoring)
83            self.envelope_db = self.release_coeff * self.envelope_db + (1.0 - self.release_coeff) * target_gain_db;
84        }
85
86        let gain_linear = (10.0f32).powf(self.envelope_db / 20.0) * self.makeup_gain_linear;
87        input * gain_linear
88    }
89
90    /// Reset compressor internal state.
91    pub fn reset(&mut self) {
92        self.envelope_db = 0.0;
93    }
94}
95
96impl DspNode<f32> for DynamicsCompressor {
97    #[inline(always)]
98    fn process_sample(&mut self, input: f32) -> f32 {
99        self.process(input)
100    }
101}
102
103/// Noise Gate for ambient noise and low-level hum suppression.
104#[derive(Debug, Clone, Copy)]
105pub struct NoiseGate {
106    threshold_db: f32,
107    reduction_linear: f32,
108    attack_coeff: f32,
109    release_coeff: f32,
110    envelope_linear: f32,
111}
112
113impl NoiseGate {
114    /// Create a new noise gate.
115    ///
116    /// - `threshold_db`: Gate open threshold (e.g. `-45.0` dBFS).
117    /// - `reduction_db`: Maximum attenuation when closed (e.g. `-40.0` dB).
118    /// - `attack_s`: Opening time (e.g. `0.002` s).
119    /// - `release_s`: Closing time (e.g. `0.05` s).
120    pub fn new(threshold_db: f32, reduction_db: f32, attack_s: f32, release_s: f32, sample_rate_hz: f32) -> Self {
121        let attack_coeff = (-1.0 / (attack_s.max(1e-5) * sample_rate_hz)).exp();
122        let release_coeff = (-1.0 / (release_s.max(1e-5) * sample_rate_hz)).exp();
123        let reduction_linear = (10.0f32).powf(reduction_db / 20.0);
124
125        Self {
126            threshold_db,
127            reduction_linear,
128            attack_coeff,
129            release_coeff,
130            envelope_linear: 0.0,
131        }
132    }
133
134    /// Process a sample through the noise gate.
135    pub fn process(&mut self, input: f32) -> f32 {
136        let abs_in = input.abs();
137        let input_db = if abs_in > 1e-6 {
138            20.0 * abs_in.log10()
139        } else {
140            -120.0
141        };
142
143        let target_gain = if input_db >= self.threshold_db {
144            1.0
145        } else {
146            self.reduction_linear
147        };
148
149        if target_gain > self.envelope_linear {
150            self.envelope_linear = self.attack_coeff * self.envelope_linear + (1.0 - self.attack_coeff) * target_gain;
151        } else {
152            self.envelope_linear = self.release_coeff * self.envelope_linear + (1.0 - self.release_coeff) * target_gain;
153        }
154
155        input * self.envelope_linear
156    }
157
158    /// Reset noise gate internal states.
159    pub fn reset(&mut self) {
160        self.envelope_linear = 0.0;
161    }
162}
163
164impl DspNode<f32> for NoiseGate {
165    #[inline(always)]
166    fn process_sample(&mut self, input: f32) -> f32 {
167        self.process(input)
168    }
169}