1use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
11
12pub struct Gate {
13 rms_env: f32,
15 gain_env: f32,
17 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 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 let target_gain = if rms_linear > threshold_linear {
86 self.hold_counter = hold_samples;
88 1.0
89 } else if self.hold_counter > 0 {
90 self.hold_counter -= 1;
92 1.0
93 } else {
94 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 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 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 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 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]; 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 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 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]; 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 let last = output[BUFFER_SIZE - 1].abs();
173 assert!(last < 0.01, "quiet signal should be attenuated, got {last}");
174 }
175}