Skip to main content

aether_nodes/
gate.rs

1//! Noise Gate — attenuates signal below threshold.
2//!
3//! Param layout:
4//!   0 = threshold  (dB, -80 – 0)
5//!   1 = ratio      (1:1 – inf:1, represented as 1.0 – 100.0)
6//!   2 = attack     (ms, 0.1 – 100)
7//!   3 = release    (ms, 10 – 2000)
8//!   4 = hold       (ms, 0 – 500)
9
10use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
11
12pub struct Gate {
13    /// RMS envelope follower state.
14    rms_env: f32,
15    /// Gain envelope (smoothed).
16    gain_env: f32,
17    /// Hold counter (samples remaining in hold state).
18    hold_counter: usize,
19}
20
21impl Gate {
22    pub fn new() -> Self {
23        Self {
24            rms_env: 0.0,
25            gain_env: 0.0,
26            hold_counter: 0,
27        }
28    }
29
30    #[inline(always)]
31    fn db_to_linear(db: f32) -> f32 {
32        10.0f32.powf(db / 20.0)
33    }
34
35    #[inline(always)]
36    fn linear_to_db(linear: f32) -> f32 {
37        if linear <= 1e-10 {
38            return -200.0;
39        }
40        20.0 * linear.log10()
41    }
42}
43
44impl Default for Gate {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl DspNode for Gate {
51    fn process(
52        &mut self,
53        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
54        output: &mut [f32; BUFFER_SIZE],
55        params: &mut ParamBlock,
56        sample_rate: f32,
57    ) {
58        let silence = [0.0f32; BUFFER_SIZE];
59        let input = inputs[0].unwrap_or(&silence);
60
61        let threshold_db = params.get(0).current.clamp(-80.0, 0.0);
62        let ratio = params.get(1).current.clamp(1.0, 100.0);
63        let attack_ms = params.get(2).current.clamp(0.1, 100.0);
64        let release_ms = params.get(3).current.clamp(10.0, 2000.0);
65        let hold_ms = params.get(4).current.clamp(0.0, 500.0);
66
67        let threshold_linear = Self::db_to_linear(threshold_db);
68        let attack_coeff = (-1.0 / (attack_ms * 0.001 * sample_rate)).exp();
69        let release_coeff = (-1.0 / (release_ms * 0.001 * sample_rate)).exp();
70        let hold_samples = (hold_ms * 0.001 * sample_rate) as usize;
71
72        for i in 0..BUFFER_SIZE {
73            let x = input[i];
74
75            // RMS envelope follower
76            let x2 = x * x;
77            self.rms_env = if x2 > self.rms_env {
78                attack_coeff * self.rms_env + (1.0 - attack_coeff) * x2
79            } else {
80                release_coeff * self.rms_env + (1.0 - release_coeff) * x2
81            };
82            let rms_linear = self.rms_env.sqrt();
83
84            // Gate logic with hold
85            let target_gain = if rms_linear > threshold_linear {
86                // Signal above threshold: gate open
87                self.hold_counter = hold_samples;
88                1.0
89            } else if self.hold_counter > 0 {
90                // In hold state: keep gate open
91                self.hold_counter -= 1;
92                1.0
93            } else {
94                // Signal below threshold: apply attenuation
95                let rms_db = Self::linear_to_db(rms_linear);
96                let attenuation_db = (rms_db - threshold_db) * (1.0 / ratio - 1.0);
97                Self::db_to_linear(attenuation_db).max(0.0)
98            };
99
100            // Smooth gain envelope
101            self.gain_env = if target_gain > self.gain_env {
102                attack_coeff * self.gain_env + (1.0 - attack_coeff) * target_gain
103            } else {
104                release_coeff * self.gain_env + (1.0 - release_coeff) * target_gain
105            };
106
107            output[i] = x * self.gain_env;
108            params.tick_all();
109        }
110    }
111
112    fn type_name(&self) -> &'static str {
113        "Gate"
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_gate_silence_attenuation() {
123        let mut gate = Gate::new();
124        let mut params = ParamBlock::new();
125        // threshold=-40dB, ratio=10, attack=1ms, release=100ms, hold=0ms
126        for &v in &[-40.0f32, 10.0, 1.0, 100.0, 0.0] {
127            params.add(v);
128        }
129        let input = [0.0f32; BUFFER_SIZE];
130        let inputs = [Some(&input); MAX_INPUTS];
131        let mut output = [0.0f32; BUFFER_SIZE];
132        gate.process(&inputs, &mut output, &mut params, 48000.0);
133        // Silence should be attenuated (close to zero)
134        for s in &output {
135            assert!(s.abs() < 1e-6, "silence should be attenuated");
136        }
137    }
138
139    #[test]
140    fn test_gate_passes_loud_signal() {
141        let mut gate = Gate::new();
142        let mut params = ParamBlock::new();
143        // threshold=-40dB, ratio=10, attack=1ms, release=100ms, hold=0ms
144        for &v in &[-40.0f32, 10.0, 1.0, 100.0, 0.0] {
145            params.add(v);
146        }
147        let input = [0.5f32; BUFFER_SIZE]; // Loud signal (~-6dB)
148        let inputs = [Some(&input); MAX_INPUTS];
149        let mut output = [0.0f32; BUFFER_SIZE];
150        gate.process(&inputs, &mut output, &mut params, 48000.0);
151        // After settling, loud signal should pass through
152        let last = output[BUFFER_SIZE - 1].abs();
153        assert!(
154            last > 0.3,
155            "loud signal should pass through gate, got {last}"
156        );
157    }
158
159    #[test]
160    fn test_gate_attenuates_quiet_signal() {
161        let mut gate = Gate::new();
162        let mut params = ParamBlock::new();
163        // threshold=-40dB, ratio=10, attack=1ms, release=100ms, hold=0ms
164        for &v in &[-40.0f32, 10.0, 1.0, 100.0, 0.0] {
165            params.add(v);
166        }
167        let input = [0.0001f32; BUFFER_SIZE]; // Very quiet signal (~-80dB)
168        let inputs = [Some(&input); MAX_INPUTS];
169        let mut output = [0.0f32; BUFFER_SIZE];
170        gate.process(&inputs, &mut output, &mut params, 48000.0);
171        // After settling, quiet signal should be attenuated (less than input)
172        let last = output[BUFFER_SIZE - 1].abs();
173        assert!(last < 0.01, "quiet signal should be attenuated, got {last}");
174    }
175}