1use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
9
10const LOOKAHEAD_MS: f32 = 5.0; const MAX_LOOKAHEAD_SAMPLES: usize = 512; pub struct Limiter {
14 lookahead_buffer: [f32; MAX_LOOKAHEAD_SAMPLES],
16 write_pos: usize,
18 gain_env: f32,
20 lookahead_samples: usize,
22}
23
24impl Limiter {
25 pub fn new() -> Self {
26 Self {
27 lookahead_buffer: [0.0; MAX_LOOKAHEAD_SAMPLES],
28 write_pos: 0,
29 gain_env: 1.0,
30 lookahead_samples: 240, }
32 }
33
34 #[inline(always)]
35 fn db_to_linear(db: f32) -> f32 {
36 10.0f32.powf(db / 20.0)
37 }
38
39 #[allow(dead_code)]
41 #[inline(always)]
42 fn linear_to_db(linear: f32) -> f32 {
43 if linear <= 1e-10 {
44 return -200.0;
45 }
46 20.0 * linear.log10()
47 }
48}
49
50impl Default for Limiter {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl DspNode for Limiter {
57 fn process(
58 &mut self,
59 inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
60 output: &mut [f32; BUFFER_SIZE],
61 params: &mut ParamBlock,
62 sample_rate: f32,
63 ) {
64 let silence = [0.0f32; BUFFER_SIZE];
65 let input = inputs[0].unwrap_or(&silence);
66
67 let threshold_db = params.get(0).current.clamp(-24.0, 0.0);
68 let release_ms = params.get(1).current.clamp(10.0, 1000.0);
69 let ceiling_db = params.get(2).current.clamp(-12.0, 0.0);
70
71 let threshold_linear = Self::db_to_linear(threshold_db);
72 let ceiling_linear = Self::db_to_linear(ceiling_db);
73 let release_coeff = (-1.0 / (release_ms * 0.001 * sample_rate)).exp();
74
75 self.lookahead_samples =
77 ((LOOKAHEAD_MS * 0.001 * sample_rate) as usize).min(MAX_LOOKAHEAD_SAMPLES);
78
79 for i in 0..BUFFER_SIZE {
80 let x = input[i];
81
82 self.lookahead_buffer[self.write_pos] = x;
84 self.write_pos = (self.write_pos + 1) % self.lookahead_samples;
85
86 let read_pos = self.write_pos; let delayed = self.lookahead_buffer[read_pos];
89
90 let peak = x.abs();
92
93 let target_gain = if peak > threshold_linear {
95 let reduction = threshold_linear / peak;
97 reduction.min(1.0)
98 } else {
99 1.0 };
101
102 self.gain_env = if target_gain < self.gain_env {
104 target_gain } else {
106 release_coeff * self.gain_env + (1.0 - release_coeff) * target_gain
107 };
108
109 let limited = delayed * self.gain_env;
111 output[i] = limited.clamp(-ceiling_linear, ceiling_linear);
112
113 params.tick_all();
114 }
115 }
116
117 fn type_name(&self) -> &'static str {
118 "Limiter"
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_limiter_silence_passthrough() {
128 let mut limiter = Limiter::new();
129 let mut params = ParamBlock::new();
130 for &v in &[-0.0f32, 100.0, 0.0] {
132 params.add(v);
133 }
134 let input = [0.0f32; BUFFER_SIZE];
135 let inputs = [Some(&input); MAX_INPUTS];
136 let mut output = [0.0f32; BUFFER_SIZE];
137 limiter.process(&inputs, &mut output, &mut params, 48000.0);
138 for s in &output {
139 assert!(s.abs() < 1e-6, "silence should pass through");
140 }
141 }
142
143 #[test]
144 fn test_limiter_reduces_peaks() {
145 let mut limiter = Limiter::new();
146 let mut params = ParamBlock::new();
147 for &v in &[-6.0f32, 100.0, 0.0] {
149 params.add(v);
150 }
151
152 let mut input = [0.0f32; BUFFER_SIZE];
154 for i in 0..BUFFER_SIZE {
155 input[i] = if i % 64 < 32 { 0.8 } else { 0.2 }; }
157
158 let inputs = [Some(&input); MAX_INPUTS];
159 let mut output = [0.0f32; BUFFER_SIZE];
160 limiter.process(&inputs, &mut output, &mut params, 48000.0);
161
162 let max_output = output.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
164 assert!(
165 max_output < 0.8,
166 "limiter should reduce peaks, got {max_output}"
167 );
168 }
169
170 #[test]
171 fn test_limiter_respects_ceiling() {
172 let mut limiter = Limiter::new();
173 let mut params = ParamBlock::new();
174 for &v in &[-12.0f32, 50.0, -3.0] {
176 params.add(v);
177 }
178
179 let input = [1.0f32; BUFFER_SIZE]; let inputs = [Some(&input); MAX_INPUTS];
181 let mut output = [0.0f32; BUFFER_SIZE];
182 limiter.process(&inputs, &mut output, &mut params, 48000.0);
183
184 let ceiling_linear = 10.0f32.powf(-3.0 / 20.0); let max_output = output.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
186 assert!(
187 max_output <= ceiling_linear + 0.01,
188 "output should not exceed ceiling"
189 );
190 }
191}