Skip to main content

aether_nodes/
limiter.rs

1//! Limiter — brick-wall limiter with lookahead.
2//!
3//! Param layout:
4//!   0 = threshold  (dB, -24 – 0)
5//!   1 = release    (ms, 10 – 1000)
6//!   2 = ceiling    (dB, -12 – 0)
7
8use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
9
10const LOOKAHEAD_MS: f32 = 5.0; // 5ms lookahead
11const MAX_LOOKAHEAD_SAMPLES: usize = 512; // At 96kHz: 5ms = 480 samples
12
13pub struct Limiter {
14    /// Lookahead buffer (ring buffer).
15    lookahead_buffer: [f32; MAX_LOOKAHEAD_SAMPLES],
16    /// Write position in lookahead buffer.
17    write_pos: usize,
18    /// Gain reduction envelope.
19    gain_env: f32,
20    /// Lookahead size in samples.
21    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, // 5ms @ 48kHz
31        }
32    }
33
34    #[inline(always)]
35    fn db_to_linear(db: f32) -> f32 {
36        10.0f32.powf(db / 20.0)
37    }
38
39    // Kept for potential future metering/display features
40    #[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        // Update lookahead size based on sample rate
76        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            // Write to lookahead buffer
83            self.lookahead_buffer[self.write_pos] = x;
84            self.write_pos = (self.write_pos + 1) % self.lookahead_samples;
85
86            // Read from lookahead buffer (delayed signal)
87            let read_pos = self.write_pos; // Current position is oldest sample
88            let delayed = self.lookahead_buffer[read_pos];
89
90            // Peak detection on current input (lookahead)
91            let peak = x.abs();
92
93            // Calculate required gain reduction
94            let target_gain = if peak > threshold_linear {
95                // Calculate gain to bring peak down to threshold
96                let reduction = threshold_linear / peak;
97                reduction.min(1.0)
98            } else {
99                1.0 // No reduction needed
100            };
101
102            // Smooth gain envelope (instant attack, slow release)
103            self.gain_env = if target_gain < self.gain_env {
104                target_gain // Instant attack
105            } else {
106                release_coeff * self.gain_env + (1.0 - release_coeff) * target_gain
107            };
108
109            // Apply gain reduction and ceiling
110            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        // threshold=0dB, release=100ms, ceiling=0dB
131        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        // threshold=-6dB, release=100ms, ceiling=0dB
148        for &v in &[-6.0f32, 100.0, 0.0] {
149            params.add(v);
150        }
151
152        // Create signal with peaks above threshold
153        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 }; // Peaks at 0.8 (~-2dB)
156        }
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        // After settling, peaks should be reduced
163        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        // threshold=-12dB, release=50ms, ceiling=-3dB
175        for &v in &[-12.0f32, 50.0, -3.0] {
176            params.add(v);
177        }
178
179        let input = [1.0f32; BUFFER_SIZE]; // Very loud signal
180        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); // -3dB in linear
185        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}