Skip to main content

rill_digital_effects/
limiter.rs

1//! Limiter with lookahead using Delay + envelope detection
2
3use crate::delay::Delay;
4use rill_core::{
5    buffer::DelayLine,
6    math::Transcendental,
7    traits::{Node, NodeCategory, NodeMetadata, NodeState, Processor},
8    NodeId, ParamValue, ParameterId, Port, ProcessError, ProcessResult, RenderContext,
9};
10
11/// Maximum lookahead time in seconds (10 ms)
12const MAX_LOOKAHEAD_TIME: f32 = 0.01;
13/// Maximum sample rate we support (192 kHz)
14const MAX_SAMPLE_RATE: f32 = 192_000.0;
15/// Maximum lookahead samples at max sample rate
16const MAX_LOOKAHEAD_SAMPLES: usize = (MAX_LOOKAHEAD_TIME * MAX_SAMPLE_RATE) as usize;
17/// Size of analysis buffer (double the max lookahead)
18const ANALYSIS_BUF_SIZE: usize = MAX_LOOKAHEAD_SAMPLES * 2;
19
20/// Limiter with lookahead using Delay + envelope detection
21pub struct Limiter<T: Transcendental, const BUF_SIZE: usize> {
22    /// Node identifier
23    id: NodeId,
24    /// Node metadata
25    metadata: NodeMetadata,
26    /// Input ports
27    inputs: Vec<Port<T, BUF_SIZE>>,
28    /// Output ports
29    outputs: Vec<Port<T, BUF_SIZE>>,
30    /// Control ports
31    controls: Vec<Port<T, BUF_SIZE>>,
32    /// Node state
33    state: NodeState<T, BUF_SIZE>,
34    /// Delay line for lookahead
35    delay: Delay<T, BUF_SIZE>,
36    /// Buffer for envelope detection
37    analysis_buffer: DelayLine<T, ANALYSIS_BUF_SIZE>,
38    /// Threshold in dB
39    threshold_db: f32,
40    /// Threshold in linear scale
41    threshold_linear: T,
42    /// Output gain after limiting
43    output_gain: f32,
44    /// Attack time in seconds
45    attack: f32,
46    /// Release time in seconds
47    release: f32,
48    /// Lookahead time in seconds
49    lookahead: f32,
50    /// Lookahead in samples
51    lookahead_samples: usize,
52    /// Current gain reduction
53    current_gain: f32,
54    /// Attack coefficient
55    attack_coeff: f32,
56    /// Release coefficient
57    release_coeff: f32,
58    /// Sample rate
59    sample_rate: f32,
60    /// Current write position
61    position: usize,
62    /// Buffer for direct passthrough during initialization
63    init_buffer: Vec<T>,
64    /// Whether we're in initialization phase
65    initializing: bool,
66    /// Whether we're in warmup phase after initialization
67    warming_up: bool,
68}
69
70impl<T: Transcendental, const BUF_SIZE: usize> Limiter<T, BUF_SIZE> {
71    /// Create a new limiter
72    pub fn new(
73        sample_rate: f32,
74        threshold_db: f32,
75        attack: f32,
76        release: f32,
77        output_gain: f32,
78    ) -> Self {
79        let threshold_db = threshold_db.clamp(-60.0, 0.0);
80        let threshold_linear = T::from_f32(10.0_f32.powf(threshold_db / 20.0));
81
82        let attack = attack.clamp(0.001, 0.1);
83        let release = release.clamp(0.01, 1.0);
84
85        let attack_coeff = (-1.0 / (attack * sample_rate)).exp();
86        let release_coeff = (-1.0 / (release * sample_rate)).exp();
87
88        let lookahead = 0.005; // 5ms default
89        let lookahead_samples = (lookahead * sample_rate) as usize;
90
91        // Delay with needed delay, feedback=0, mix=1.0 (100% wet)
92        let delay = Delay::with_params(sample_rate, lookahead, 0.0, 1.0);
93
94        // Buffer for analysis
95        let analysis_buffer = DelayLine::new(sample_rate);
96
97        // Buffer for temporary storage during initialization
98        let init_buffer = Vec::with_capacity(lookahead_samples);
99
100        let metadata = NodeMetadata::new("Limiter", NodeCategory::Processor);
101        let mut inputs = Vec::new();
102        let mut outputs = Vec::new();
103        inputs.push(Port::input(NodeId(0), 0, "signal_in"));
104        outputs.push(Port::output(NodeId(0), 0, "signal_out"));
105
106        Self {
107            id: NodeId(0),
108            metadata,
109            inputs,
110            outputs,
111            controls: Vec::new(),
112            state: NodeState::new(sample_rate),
113            delay,
114            analysis_buffer,
115            threshold_db,
116            threshold_linear,
117            output_gain: output_gain.clamp(0.0, 2.0),
118            attack,
119            release,
120            lookahead,
121            lookahead_samples,
122            current_gain: 1.0,
123            attack_coeff,
124            release_coeff,
125            sample_rate,
126            position: 0,
127            init_buffer,
128            initializing: true,
129            warming_up: false,
130        }
131    }
132
133    /// Process a single sample
134    pub fn process_sample(&mut self, input: T) -> T {
135        self.position += 1;
136
137        // 1. Write input to analysis_buffer
138        self.analysis_buffer.write(input);
139
140        // 2. Get delayed signal from Delay
141        let delayed = self.delay.process_sample(input);
142
143        // 3. During initialization phase
144        if self.initializing {
145            // Save input to init_buffer
146            self.init_buffer.push(input);
147
148            // Check if initialization is complete
149            if self.position >= self.lookahead_samples {
150                self.initializing = false;
151                self.warming_up = true;
152
153                // Clear Delay
154                self.delay.reset();
155
156                // Debug
157                // println!("Initialization complete, starting warmup...");
158            }
159
160            // During initialization output = input
161            return input;
162        }
163
164        // 4. During warmup phase (first lookahead_samples after initialization)
165        if self.warming_up {
166            // Still use input as output while Delay fills with real data
167            if self.position < self.lookahead_samples * 2 {
168                // Fill Delay with real values
169                if self.position - self.lookahead_samples <= self.init_buffer.len() {
170                    let idx = self.position - self.lookahead_samples - 1;
171                    if idx < self.init_buffer.len() {
172                        let sample = self.init_buffer[idx];
173                        let _ = self.delay.process_sample(sample);
174                    }
175                }
176
177                // Check if warmup is complete
178                if self.position >= self.lookahead_samples * 2 - 1 {
179                    self.warming_up = false;
180                    // println!("Warmup complete at pos {}", self.position);
181                }
182
183                return input;
184            }
185        }
186
187        // 5. Analyze signal in analysis_buffer
188        // Look for maximum amplitude within lookahead window
189        let mut max_amp = T::ZERO;
190        for offset in 0..self.lookahead_samples {
191            let sample = self.analysis_buffer.read_delayed(offset);
192            let abs_sample = sample.abs();
193            if abs_sample > max_amp {
194                max_amp = abs_sample;
195            }
196        }
197
198        // 6. Compute target gain
199        let target_gain = if max_amp > self.threshold_linear {
200            self.threshold_linear.div(max_amp).to_f32()
201        } else {
202            1.0
203        };
204
205        // 7. Smooth gain
206        if target_gain < self.current_gain {
207            self.current_gain =
208                self.current_gain * self.attack_coeff + target_gain * (1.0 - self.attack_coeff);
209        } else {
210            self.current_gain =
211                self.current_gain * self.release_coeff + target_gain * (1.0 - self.release_coeff);
212        }
213
214        // 8. Apply gain to delayed signal
215        let output = delayed.mul(T::from_f32(self.current_gain * self.output_gain));
216
217        // Debug for high signal
218        // if input > T::ONE && self.position > self.lookahead_samples * 2 {
219        //     println!("PROC: pos={}, in={:.3}, max={:.3}, target={:.3}, gain={:.3}, delay={:.3}, out={:.3}",
220        //              self.position, input.to_f32(), max_amp.to_f32(), target_gain, self.current_gain, delayed.to_f32(), output.to_f32());
221        // }
222
223        output.clamp(T::from_f32(-2.0), T::from_f32(2.0))
224    }
225
226    /// Process a block of samples
227    pub fn process_block(&mut self, input: &[T], output: &mut [T]) {
228        for i in 0..input.len().min(output.len()) {
229            output[i] = self.process_sample(input[i]);
230        }
231    }
232
233    /// Get current gain reduction
234    pub fn current_gain(&self) -> f32 {
235        self.current_gain
236    }
237
238    /// Get lookahead samples count
239    pub fn lookahead_samples(&self) -> usize {
240        self.lookahead_samples
241    }
242
243    /// Set threshold in dB
244    pub fn set_threshold(&mut self, db: f32) {
245        self.threshold_db = db.clamp(-60.0, 0.0);
246        self.threshold_linear = T::from_f32(10.0_f32.powf(self.threshold_db / 20.0));
247    }
248
249    /// Set attack time
250    pub fn set_attack(&mut self, attack: f32) {
251        self.attack = attack.clamp(0.001, 0.1);
252        self.attack_coeff = (-1.0 / (self.attack * self.sample_rate)).exp();
253    }
254
255    /// Set release time
256    pub fn set_release(&mut self, release: f32) {
257        self.release = release.clamp(0.01, 1.0);
258        self.release_coeff = (-1.0 / (self.release * self.sample_rate)).exp();
259    }
260
261    /// Set lookahead time
262    pub fn set_lookahead(&mut self, lookahead: f32) {
263        self.lookahead = lookahead.clamp(0.0, 0.01);
264        self.lookahead_samples = (self.lookahead * self.sample_rate) as usize;
265        self.delay.set_delay_time(lookahead);
266        self.analysis_buffer.clear();
267        self.current_gain = 1.0;
268        self.position = 0;
269        self.init_buffer.clear();
270        self.initializing = true;
271        self.warming_up = false;
272    }
273
274    /// Reset internal state - now with forced buffer filling
275    pub fn reset(&mut self) {
276        self.current_gain = 1.0;
277        self.position = 0;
278        self.init_buffer.clear();
279        self.initializing = true;
280        self.warming_up = false;
281        self.delay.reset();
282        self.analysis_buffer.clear();
283    }
284
285    /// Force finish initialization and warmup (for tests)
286    pub fn force_ready(&mut self) {
287        if self.initializing || self.warming_up {
288            // Fill buffers with test values
289            for _ in 0..self.lookahead_samples * 2 {
290                let test_val = T::from_f32(0.1);
291                self.analysis_buffer.write(test_val);
292                let _ = self.delay.process_sample(test_val);
293            }
294            self.initializing = false;
295            self.warming_up = false;
296            self.position = self.lookahead_samples * 2;
297            // println!("Force ready completed");
298        }
299    }
300}
301
302impl<T: Transcendental, const BUF_SIZE: usize> Node<T, BUF_SIZE> for Limiter<T, BUF_SIZE> {
303    fn node_type_id(&self) -> rill_core::NodeTypeId
304    where
305        Self: 'static + Sized,
306    {
307        rill_core::NodeTypeId::of::<Self>()
308    }
309
310    fn id(&self) -> NodeId {
311        self.id
312    }
313
314    fn set_id(&mut self, id: NodeId) {
315        self.id = id;
316    }
317
318    fn metadata(&self) -> NodeMetadata {
319        self.metadata.clone()
320    }
321
322    fn init(&mut self, sample_rate: f32) {
323        self.sample_rate = sample_rate;
324        self.attack_coeff = (-1.0 / (self.attack * sample_rate)).exp();
325        self.release_coeff = (-1.0 / (self.release * sample_rate)).exp();
326
327        self.lookahead_samples = (self.lookahead * sample_rate) as usize;
328        self.analysis_buffer = DelayLine::new(sample_rate);
329        self.current_gain = 1.0;
330        self.position = 0;
331        self.init_buffer.clear();
332        self.initializing = true;
333        self.warming_up = false;
334
335        self.delay.init(sample_rate);
336        self.delay.set_delay_time(self.lookahead);
337    }
338
339    fn reset(&mut self) {
340        self.state.sample_pos = 0;
341        self.state.blocks_processed = 0;
342        Limiter::reset(self);
343    }
344
345    fn get_parameter(&self, id: &ParameterId) -> Option<ParamValue> {
346        let name = id.as_str();
347        match name {
348            "threshold" => Some(ParamValue::Float(self.threshold_db)),
349            "attack" => Some(ParamValue::Float(self.attack)),
350            "release" => Some(ParamValue::Float(self.release)),
351            "output_gain" => Some(ParamValue::Float(self.output_gain)),
352            "lookahead" => Some(ParamValue::Float(self.lookahead)),
353            "current_gain" => Some(ParamValue::Float(self.current_gain)),
354            _ => None,
355        }
356    }
357
358    fn set_parameter(&mut self, id: &ParameterId, value: ParamValue) -> ProcessResult<()> {
359        let name = id.as_str();
360        if let Some(v) = value.as_f32() {
361            match name {
362                "threshold" => {
363                    self.set_threshold(v);
364                    Ok(())
365                }
366                "attack" => {
367                    self.set_attack(v);
368                    Ok(())
369                }
370                "release" => {
371                    self.set_release(v);
372                    Ok(())
373                }
374                "output_gain" => {
375                    self.output_gain = v.clamp(0.0, 2.0);
376                    Ok(())
377                }
378                "lookahead" => {
379                    self.set_lookahead(v);
380                    Ok(())
381                }
382                _ => Err(ProcessError::parameter(format!(
383                    "Unknown parameter: {}",
384                    name
385                ))),
386            }
387        } else {
388            Err(ProcessError::parameter("Expected float value"))
389        }
390    }
391
392    fn input_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
393        self.inputs.get(index)
394    }
395
396    fn input_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
397        self.inputs.get_mut(index)
398    }
399
400    fn output_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
401        self.outputs.get(index)
402    }
403
404    fn output_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
405        self.outputs.get_mut(index)
406    }
407
408    fn control_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
409        self.controls.get(index)
410    }
411
412    fn control_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
413        self.controls.get_mut(index)
414    }
415
416    fn num_inputs(&self) -> usize {
417        self.inputs.len()
418    }
419
420    fn num_outputs(&self) -> usize {
421        self.outputs.len()
422    }
423
424    fn num_signal_inputs(&self) -> usize {
425        self.inputs.len()
426    }
427
428    fn num_signal_outputs(&self) -> usize {
429        self.outputs.len()
430    }
431
432    fn state(&self) -> &NodeState<T, BUF_SIZE> {
433        &self.state
434    }
435
436    fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
437        &mut self.state
438    }
439}
440
441impl<T: Transcendental, const BUF_SIZE: usize> Processor<T, BUF_SIZE> for Limiter<T, BUF_SIZE> {
442    fn process(
443        &mut self,
444        _ctx: &RenderContext,
445        _signal_inputs: &[&[T; BUF_SIZE]],
446        _control_inputs: &[T],
447        _clock_inputs: &[RenderContext],
448        _feedback_inputs: &[&[T; BUF_SIZE]],
449    ) -> ProcessResult<()> {
450        for i in 0..BUF_SIZE {
451            let sample = self.inputs[0].read()[i];
452            self.outputs[0].write()[i] = self.process_sample(sample);
453        }
454        self.state.advance();
455        Ok(())
456    }
457
458    fn latency(&self) -> usize {
459        0
460    }
461}