Skip to main content

oximedia_graph/filters/audio/
limiter.rs

1//! Audio limiter filter.
2//!
3//! This module provides a brickwall limiter with true peak limiting
4//! and soft limiting options.
5
6#![forbid(unsafe_code)]
7#![allow(clippy::cast_sign_loss)]
8#![allow(clippy::cast_possible_wrap)]
9#![allow(clippy::cast_lossless)]
10#![allow(clippy::needless_pass_by_value)]
11#![allow(clippy::needless_range_loop)]
12#![allow(clippy::get_first)]
13#![allow(clippy::doc_markdown)]
14
15use bytes::{Bytes, BytesMut};
16use std::collections::VecDeque;
17
18use crate::error::{GraphError, GraphResult};
19use crate::frame::FilterFrame;
20use crate::node::{Node, NodeId, NodeState, NodeType};
21use crate::port::{AudioPortFormat, InputPort, OutputPort, PortFormat, PortId, PortType};
22
23use oximedia_audio::{AudioBuffer, AudioFrame, ChannelLayout};
24use oximedia_core::SampleFormat;
25
26/// Limiter mode.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
28pub enum LimiterMode {
29    /// Brickwall limiting - hard ceiling.
30    #[default]
31    Brickwall,
32    /// Soft limiting with saturation curve.
33    Soft,
34}
35
36/// Configuration for the limiter.
37#[derive(Clone, Debug)]
38pub struct LimiterConfig {
39    /// Ceiling level in dB (maximum output level).
40    pub ceiling_db: f64,
41    /// Release time in milliseconds.
42    pub release_ms: f64,
43    /// Limiter mode.
44    pub mode: LimiterMode,
45    /// Enable true peak limiting (oversampled detection).
46    pub true_peak: bool,
47    /// Lookahead time in milliseconds.
48    pub lookahead_ms: f64,
49}
50
51impl Default for LimiterConfig {
52    fn default() -> Self {
53        Self {
54            ceiling_db: -0.3,
55            release_ms: 100.0,
56            mode: LimiterMode::Brickwall,
57            true_peak: true,
58            lookahead_ms: 5.0,
59        }
60    }
61}
62
63impl LimiterConfig {
64    /// Create a new limiter configuration.
65    #[must_use]
66    pub fn new(ceiling_db: f64) -> Self {
67        Self {
68            ceiling_db,
69            ..Default::default()
70        }
71    }
72
73    /// Set release time.
74    #[must_use]
75    pub fn with_release(mut self, release_ms: f64) -> Self {
76        self.release_ms = release_ms;
77        self
78    }
79
80    /// Set limiter mode.
81    #[must_use]
82    pub fn with_mode(mut self, mode: LimiterMode) -> Self {
83        self.mode = mode;
84        self
85    }
86
87    /// Enable or disable true peak limiting.
88    #[must_use]
89    pub fn with_true_peak(mut self, enabled: bool) -> Self {
90        self.true_peak = enabled;
91        self
92    }
93
94    /// Set lookahead time.
95    #[must_use]
96    pub fn with_lookahead(mut self, lookahead_ms: f64) -> Self {
97        self.lookahead_ms = lookahead_ms;
98        self
99    }
100
101    /// Create a brickwall limiter.
102    #[must_use]
103    pub fn brickwall(ceiling_db: f64) -> Self {
104        Self::new(ceiling_db).with_mode(LimiterMode::Brickwall)
105    }
106
107    /// Create a soft limiter.
108    #[must_use]
109    pub fn soft(ceiling_db: f64) -> Self {
110        Self::new(ceiling_db).with_mode(LimiterMode::Soft)
111    }
112
113    /// Convert dB to linear gain.
114    #[must_use]
115    pub fn db_to_linear(db: f64) -> f64 {
116        10.0_f64.powf(db / 20.0)
117    }
118
119    /// Convert linear to dB.
120    #[must_use]
121    pub fn linear_to_db(linear: f64) -> f64 {
122        if linear <= 0.0 {
123            f64::NEG_INFINITY
124        } else {
125            20.0 * linear.log10()
126        }
127    }
128}
129
130/// True peak detector using oversampling.
131#[derive(Clone, Debug)]
132struct TruePeakDetector {
133    /// Oversampling factor.
134    oversample: usize,
135    /// FIR filter coefficients for upsampling.
136    filter_coeffs: Vec<f64>,
137    /// History buffer for each channel.
138    history: Vec<VecDeque<f64>>,
139}
140
141impl TruePeakDetector {
142    /// Create a new true peak detector.
143    fn new(channels: usize) -> Self {
144        // 4x oversampling with simple linear interpolation
145        let oversample = 4;
146        let filter_coeffs = Self::create_filter_coeffs();
147        let history = vec![VecDeque::with_capacity(filter_coeffs.len()); channels];
148
149        Self {
150            oversample,
151            filter_coeffs,
152            history,
153        }
154    }
155
156    /// Create FIR filter coefficients for upsampling.
157    fn create_filter_coeffs() -> Vec<f64> {
158        // Simple windowed sinc filter for 4x oversampling
159        vec![
160            0.0, 0.0625, 0.0, 0.125, 0.0, 0.25, 0.0, 0.5, 1.0, 0.5, 0.0, 0.25, 0.0, 0.125, 0.0,
161            0.0625,
162        ]
163    }
164
165    /// Detect true peak for a sample.
166    fn detect(&mut self, channel: usize, sample: f64) -> f64 {
167        if channel >= self.history.len() {
168            return sample.abs();
169        }
170
171        // Add sample to history
172        self.history[channel].push_back(sample);
173        if self.history[channel].len() > self.filter_coeffs.len() {
174            self.history[channel].pop_front();
175        }
176
177        // Find peak across oversampled positions
178        let mut peak = sample.abs();
179
180        for phase in 0..self.oversample {
181            let mut sum = 0.0;
182            for (i, &coeff) in self.filter_coeffs.iter().enumerate() {
183                let idx = (i * self.oversample + phase) / self.oversample;
184                if idx < self.history[channel].len() {
185                    let hist_sample = self.history[channel].get(idx).copied().unwrap_or(0.0);
186                    sum += hist_sample * coeff;
187                }
188            }
189            peak = peak.max(sum.abs());
190        }
191
192        peak
193    }
194
195    /// Reset detector state.
196    fn reset(&mut self) {
197        for hist in &mut self.history {
198            hist.clear();
199        }
200    }
201}
202
203/// Lookahead buffer for the limiter.
204#[derive(Clone, Debug)]
205struct LookaheadBuffer {
206    /// Buffer per channel.
207    buffers: Vec<VecDeque<f64>>,
208    /// Peak buffer for the lookahead window.
209    peak_buffer: VecDeque<f64>,
210    /// Delay in samples.
211    delay_samples: usize,
212}
213
214impl LookaheadBuffer {
215    /// Create a new lookahead buffer.
216    fn new(lookahead_ms: f64, sample_rate: f64, channels: usize) -> Self {
217        let delay_samples = ((lookahead_ms * 0.001 * sample_rate) as usize).max(1);
218
219        Self {
220            buffers: vec![VecDeque::with_capacity(delay_samples + 1); channels],
221            peak_buffer: VecDeque::with_capacity(delay_samples + 1),
222            delay_samples,
223        }
224    }
225
226    /// Push sample and get delayed sample.
227    fn process(&mut self, channel: usize, sample: f64, peak: f64) -> (f64, f64) {
228        if channel >= self.buffers.len() {
229            return (sample, peak);
230        }
231
232        self.buffers[channel].push_back(sample);
233        if channel == 0 {
234            self.peak_buffer.push_back(peak);
235        }
236
237        let delayed_sample = if self.buffers[channel].len() > self.delay_samples {
238            self.buffers[channel].pop_front().unwrap_or(sample)
239        } else {
240            0.0
241        };
242
243        // Find maximum peak in lookahead window
244        let lookahead_peak = if channel == 0 && self.peak_buffer.len() > self.delay_samples {
245            self.peak_buffer.pop_front();
246            self.peak_buffer.iter().copied().fold(0.0_f64, f64::max)
247        } else {
248            self.peak_buffer.iter().copied().fold(0.0_f64, f64::max)
249        };
250
251        (delayed_sample, lookahead_peak)
252    }
253
254    /// Reset buffer.
255    fn reset(&mut self) {
256        for buffer in &mut self.buffers {
257            buffer.clear();
258        }
259        self.peak_buffer.clear();
260    }
261}
262
263/// Gain smoothing state.
264#[derive(Clone, Debug)]
265struct GainSmoother {
266    /// Current gain value.
267    current_gain: f64,
268    /// Release coefficient.
269    release_coeff: f64,
270}
271
272impl GainSmoother {
273    /// Create a new gain smoother.
274    fn new(release_ms: f64, sample_rate: f64) -> Self {
275        let release_coeff = if release_ms > 0.0 {
276            (-1.0 / (release_ms * 0.001 * sample_rate)).exp()
277        } else {
278            0.0
279        };
280
281        Self {
282            current_gain: 1.0,
283            release_coeff,
284        }
285    }
286
287    /// Update gain for a target gain value.
288    fn update(&mut self, target_gain: f64) -> f64 {
289        if target_gain < self.current_gain {
290            // Instant attack
291            self.current_gain = target_gain;
292        } else {
293            // Smooth release
294            self.current_gain =
295                self.release_coeff * self.current_gain + (1.0 - self.release_coeff) * target_gain;
296        }
297        self.current_gain
298    }
299
300    /// Reset state.
301    fn reset(&mut self) {
302        self.current_gain = 1.0;
303    }
304}
305
306/// Limiter internal state.
307struct LimiterState {
308    /// Ceiling level (linear).
309    ceiling: f64,
310    /// True peak detector.
311    true_peak_detector: TruePeakDetector,
312    /// Lookahead buffer.
313    lookahead: LookaheadBuffer,
314    /// Gain smoother.
315    gain_smoother: GainSmoother,
316    /// Current gain reduction in dB (for metering).
317    gain_reduction_db: f64,
318    /// Limiter mode.
319    mode: LimiterMode,
320}
321
322impl LimiterState {
323    /// Create new limiter state.
324    fn new(config: &LimiterConfig, sample_rate: f64, channels: usize) -> Self {
325        let ceiling = LimiterConfig::db_to_linear(config.ceiling_db);
326
327        Self {
328            ceiling,
329            true_peak_detector: TruePeakDetector::new(channels),
330            lookahead: LookaheadBuffer::new(config.lookahead_ms, sample_rate, channels),
331            gain_smoother: GainSmoother::new(config.release_ms, sample_rate),
332            gain_reduction_db: 0.0,
333            mode: config.mode,
334        }
335    }
336
337    /// Apply soft limiting curve.
338    fn soft_limit(sample: f64, ceiling: f64) -> f64 {
339        if sample.abs() <= ceiling {
340            sample
341        } else {
342            let sign = sample.signum();
343            let excess = sample.abs() - ceiling;
344            let range = 1.0 - ceiling;
345            let saturated = ceiling + range * (excess / range).tanh();
346            sign * saturated.min(1.0)
347        }
348    }
349
350    /// Process samples.
351    fn process(&mut self, samples: &mut [Vec<f64>], config: &LimiterConfig) {
352        let sample_count = samples.get(0).map_or(0, Vec::len);
353        let channels = samples.len();
354
355        for i in 0..sample_count {
356            // Detect peak across all channels
357            let mut peak = 0.0_f64;
358            for ch in 0..channels {
359                if i < samples[ch].len() {
360                    let sample_peak = if config.true_peak {
361                        self.true_peak_detector.detect(ch, samples[ch][i])
362                    } else {
363                        samples[ch][i].abs()
364                    };
365                    peak = peak.max(sample_peak);
366                }
367            }
368
369            // Calculate required gain
370            let target_gain = if peak > self.ceiling {
371                self.ceiling / peak
372            } else {
373                1.0
374            };
375
376            // Apply to all channels with lookahead
377            for ch in 0..channels {
378                if i < samples[ch].len() {
379                    let (delayed_sample, lookahead_peak) =
380                        self.lookahead.process(ch, samples[ch][i], peak);
381
382                    // Calculate gain based on lookahead peak
383                    let lookahead_gain = if lookahead_peak > self.ceiling {
384                        self.ceiling / lookahead_peak
385                    } else {
386                        1.0
387                    };
388
389                    // Use the more aggressive gain
390                    let final_target = target_gain.min(lookahead_gain);
391                    let smoothed_gain = self.gain_smoother.update(final_target);
392
393                    // Apply gain
394                    let output = match self.mode {
395                        LimiterMode::Brickwall => {
396                            (delayed_sample * smoothed_gain).clamp(-self.ceiling, self.ceiling)
397                        }
398                        LimiterMode::Soft => {
399                            Self::soft_limit(delayed_sample * smoothed_gain, self.ceiling)
400                        }
401                    };
402
403                    samples[ch][i] = output;
404                }
405            }
406
407            // Update metering
408            if target_gain < 1.0 {
409                self.gain_reduction_db = LimiterConfig::linear_to_db(target_gain);
410            } else {
411                // Slow release of meter
412                self.gain_reduction_db *= 0.999;
413            }
414        }
415    }
416
417    /// Reset state.
418    fn reset(&mut self) {
419        self.true_peak_detector.reset();
420        self.lookahead.reset();
421        self.gain_smoother.reset();
422        self.gain_reduction_db = 0.0;
423    }
424}
425
426/// Audio limiter filter.
427///
428/// This filter provides brickwall or soft limiting with true peak detection
429/// and lookahead for transparent gain reduction.
430///
431/// # Example
432///
433/// ```ignore
434/// use oximedia_graph::filters::audio::limiter::{LimiterFilter, LimiterConfig};
435///
436/// // Create a brickwall limiter at -0.3 dBFS
437/// let config = LimiterConfig::brickwall(-0.3)
438///     .with_release(100.0)
439///     .with_true_peak(true);
440/// let filter = LimiterFilter::new(NodeId(0), "limiter", config);
441/// ```
442pub struct LimiterFilter {
443    id: NodeId,
444    name: String,
445    state: NodeState,
446    config: LimiterConfig,
447    limiter_state: Option<LimiterState>,
448    inputs: Vec<InputPort>,
449    outputs: Vec<OutputPort>,
450}
451
452impl LimiterFilter {
453    /// Create a new limiter filter.
454    #[must_use]
455    pub fn new(id: NodeId, name: impl Into<String>, config: LimiterConfig) -> Self {
456        let audio_format = PortFormat::Audio(AudioPortFormat::any());
457
458        Self {
459            id,
460            name: name.into(),
461            state: NodeState::Idle,
462            config,
463            limiter_state: None,
464            inputs: vec![InputPort::new(PortId(0), "input", PortType::Audio)
465                .with_format(audio_format.clone())],
466            outputs: vec![
467                OutputPort::new(PortId(0), "output", PortType::Audio).with_format(audio_format)
468            ],
469        }
470    }
471
472    /// Get the current configuration.
473    #[must_use]
474    pub fn config(&self) -> &LimiterConfig {
475        &self.config
476    }
477
478    /// Update the configuration.
479    pub fn set_config(&mut self, config: LimiterConfig) {
480        self.config = config;
481        self.limiter_state = None; // Reset state
482    }
483
484    /// Get current gain reduction in dB.
485    #[must_use]
486    pub fn gain_reduction_db(&self) -> f64 {
487        self.limiter_state
488            .as_ref()
489            .map_or(0.0, |s| s.gain_reduction_db)
490    }
491
492    /// Convert audio frame to f64 samples per channel.
493    fn frame_to_samples(frame: &AudioFrame) -> Vec<Vec<f64>> {
494        let channels = frame.channels.count();
495        let sample_count = frame.sample_count();
496
497        if sample_count == 0 {
498            return vec![Vec::new(); channels];
499        }
500
501        let mut output = vec![Vec::with_capacity(sample_count); channels];
502
503        match &frame.samples {
504            AudioBuffer::Interleaved(data) => {
505                Self::convert_interleaved(data, frame.format, channels, &mut output);
506            }
507            AudioBuffer::Planar(planes) => {
508                Self::convert_planar(planes, frame.format, &mut output);
509            }
510        }
511
512        output
513    }
514
515    /// Convert interleaved samples.
516    fn convert_interleaved(
517        data: &Bytes,
518        format: SampleFormat,
519        channels: usize,
520        output: &mut [Vec<f64>],
521    ) {
522        let bytes_per_sample = format.bytes_per_sample();
523        if bytes_per_sample == 0 || channels == 0 {
524            return;
525        }
526
527        let sample_count = data.len() / (bytes_per_sample * channels);
528
529        for i in 0..sample_count {
530            for ch in 0..channels {
531                let offset = (i * channels + ch) * bytes_per_sample;
532                if offset + bytes_per_sample <= data.len() {
533                    let sample =
534                        Self::bytes_to_f64(&data[offset..offset + bytes_per_sample], format);
535                    output[ch].push(sample);
536                }
537            }
538        }
539    }
540
541    /// Convert planar samples.
542    fn convert_planar(planes: &[Bytes], format: SampleFormat, output: &mut [Vec<f64>]) {
543        let bytes_per_sample = format.bytes_per_sample();
544        if bytes_per_sample == 0 {
545            return;
546        }
547
548        for (ch, plane) in planes.iter().enumerate() {
549            if ch >= output.len() {
550                break;
551            }
552            let sample_count = plane.len() / bytes_per_sample;
553            for i in 0..sample_count {
554                let offset = i * bytes_per_sample;
555                if offset + bytes_per_sample <= plane.len() {
556                    let sample =
557                        Self::bytes_to_f64(&plane[offset..offset + bytes_per_sample], format);
558                    output[ch].push(sample);
559                }
560            }
561        }
562    }
563
564    /// Convert bytes to f64 sample.
565    fn bytes_to_f64(bytes: &[u8], format: SampleFormat) -> f64 {
566        match format {
567            SampleFormat::U8 => {
568                if bytes.is_empty() {
569                    return 0.0;
570                }
571                (f64::from(bytes[0]) - 128.0) / 128.0
572            }
573            SampleFormat::S16 => {
574                if bytes.len() < 2 {
575                    return 0.0;
576                }
577                let sample = i16::from_le_bytes([bytes[0], bytes[1]]);
578                f64::from(sample) / f64::from(i16::MAX)
579            }
580            SampleFormat::S32 => {
581                if bytes.len() < 4 {
582                    return 0.0;
583                }
584                let sample = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
585                f64::from(sample) / f64::from(i32::MAX)
586            }
587            SampleFormat::F32 => {
588                if bytes.len() < 4 {
589                    return 0.0;
590                }
591                f64::from(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
592            }
593            SampleFormat::F64 => {
594                if bytes.len() < 8 {
595                    return 0.0;
596                }
597                f64::from_le_bytes([
598                    bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
599                ])
600            }
601            _ => 0.0,
602        }
603    }
604
605    /// Convert f64 samples to audio frame.
606    fn samples_to_frame(
607        samples: Vec<Vec<f64>>,
608        format: SampleFormat,
609        sample_rate: u32,
610        channels: ChannelLayout,
611    ) -> AudioFrame {
612        let channel_count = channels.count();
613        if samples.is_empty() || samples[0].is_empty() || channel_count == 0 {
614            return AudioFrame::new(format, sample_rate, channels);
615        }
616
617        let sample_count = samples[0].len();
618        let bytes_per_sample = format.bytes_per_sample();
619        let mut buffer = BytesMut::with_capacity(sample_count * channel_count * bytes_per_sample);
620
621        for i in 0..sample_count {
622            for ch in 0..channel_count {
623                let sample = if ch < samples.len() && i < samples[ch].len() {
624                    samples[ch][i]
625                } else {
626                    0.0
627                };
628                Self::f64_to_bytes(sample, format, &mut buffer);
629            }
630        }
631
632        let mut frame = AudioFrame::new(format, sample_rate, channels);
633        frame.samples = AudioBuffer::Interleaved(buffer.freeze());
634        frame
635    }
636
637    /// Convert f64 sample to bytes.
638    fn f64_to_bytes(sample: f64, format: SampleFormat, buffer: &mut BytesMut) {
639        let clamped = sample.clamp(-1.0, 1.0);
640
641        match format {
642            SampleFormat::U8 => {
643                let value = ((clamped * 128.0) + 128.0) as u8;
644                buffer.extend_from_slice(&[value]);
645            }
646            SampleFormat::S16 => {
647                let value = (clamped * f64::from(i16::MAX)) as i16;
648                buffer.extend_from_slice(&value.to_le_bytes());
649            }
650            SampleFormat::S32 => {
651                let value = (clamped * f64::from(i32::MAX)) as i32;
652                buffer.extend_from_slice(&value.to_le_bytes());
653            }
654            SampleFormat::F32 => {
655                #[allow(clippy::cast_possible_truncation)]
656                let value = clamped as f32;
657                buffer.extend_from_slice(&value.to_le_bytes());
658            }
659            SampleFormat::F64 => {
660                buffer.extend_from_slice(&clamped.to_le_bytes());
661            }
662            _ => {}
663        }
664    }
665}
666
667impl Node for LimiterFilter {
668    fn id(&self) -> NodeId {
669        self.id
670    }
671
672    fn name(&self) -> &str {
673        &self.name
674    }
675
676    fn node_type(&self) -> NodeType {
677        NodeType::Filter
678    }
679
680    fn state(&self) -> NodeState {
681        self.state
682    }
683
684    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
685        if !self.state.can_transition_to(state) {
686            return Err(GraphError::InvalidStateTransition {
687                node: self.id,
688                from: self.state.to_string(),
689                to: state.to_string(),
690            });
691        }
692        self.state = state;
693        Ok(())
694    }
695
696    fn inputs(&self) -> &[InputPort] {
697        &self.inputs
698    }
699
700    fn outputs(&self) -> &[OutputPort] {
701        &self.outputs
702    }
703
704    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
705        let frame = match input {
706            Some(FilterFrame::Audio(frame)) => frame,
707            Some(_) => {
708                return Err(GraphError::PortTypeMismatch {
709                    expected: "Audio".to_string(),
710                    actual: "Video".to_string(),
711                });
712            }
713            None => return Ok(None),
714        };
715
716        // Initialize limiter state if needed
717        if self.limiter_state.is_none() {
718            let channels = frame.channels.count();
719            self.limiter_state = Some(LimiterState::new(
720                &self.config,
721                f64::from(frame.sample_rate),
722                channels,
723            ));
724        }
725
726        // Convert to f64 samples
727        let mut samples = Self::frame_to_samples(&frame);
728
729        // Apply limiting
730        if let Some(ref mut limiter_state) = self.limiter_state {
731            limiter_state.process(&mut samples, &self.config);
732        }
733
734        // Convert back to frame
735        let output_frame = Self::samples_to_frame(
736            samples,
737            frame.format,
738            frame.sample_rate,
739            frame.channels.clone(),
740        );
741
742        Ok(Some(FilterFrame::Audio(output_frame)))
743    }
744
745    fn reset(&mut self) -> GraphResult<()> {
746        if let Some(ref mut state) = self.limiter_state {
747            state.reset();
748        }
749        self.set_state(NodeState::Idle)
750    }
751}
752
753#[cfg(test)]
754mod tests {
755    use super::*;
756
757    #[test]
758    fn test_limiter_mode_default() {
759        assert_eq!(LimiterMode::default(), LimiterMode::Brickwall);
760    }
761
762    #[test]
763    fn test_limiter_config() {
764        let config = LimiterConfig::new(-0.3)
765            .with_release(100.0)
766            .with_mode(LimiterMode::Soft)
767            .with_true_peak(true)
768            .with_lookahead(5.0);
769
770        assert!((config.ceiling_db - (-0.3)).abs() < f64::EPSILON);
771        assert!((config.release_ms - 100.0).abs() < f64::EPSILON);
772        assert_eq!(config.mode, LimiterMode::Soft);
773        assert!(config.true_peak);
774        assert!((config.lookahead_ms - 5.0).abs() < f64::EPSILON);
775    }
776
777    #[test]
778    fn test_preset_configs() {
779        let brickwall = LimiterConfig::brickwall(-0.5);
780        assert_eq!(brickwall.mode, LimiterMode::Brickwall);
781        assert!((brickwall.ceiling_db - (-0.5)).abs() < f64::EPSILON);
782
783        let soft = LimiterConfig::soft(-1.0);
784        assert_eq!(soft.mode, LimiterMode::Soft);
785    }
786
787    #[test]
788    fn test_db_conversion() {
789        let linear = LimiterConfig::db_to_linear(0.0);
790        assert!((linear - 1.0).abs() < f64::EPSILON);
791
792        let db = LimiterConfig::linear_to_db(1.0);
793        assert!(db.abs() < f64::EPSILON);
794    }
795
796    #[test]
797    fn test_soft_limit() {
798        let ceiling = 0.9;
799
800        // Below ceiling - no change
801        let result = LimiterState::soft_limit(0.5, ceiling);
802        assert!((result - 0.5).abs() < f64::EPSILON);
803
804        // At ceiling - no change
805        let result = LimiterState::soft_limit(0.9, ceiling);
806        assert!((result - 0.9).abs() < f64::EPSILON);
807
808        // Above ceiling - limited
809        let result = LimiterState::soft_limit(1.5, ceiling);
810        assert!(result > ceiling);
811        assert!(result <= 1.0);
812
813        // Negative values
814        let result = LimiterState::soft_limit(-1.5, ceiling);
815        assert!(result < -ceiling);
816        assert!(result >= -1.0);
817    }
818
819    #[test]
820    fn test_true_peak_detector() {
821        let mut detector = TruePeakDetector::new(2);
822
823        let peak = detector.detect(0, 0.5);
824        assert!(peak >= 0.5);
825        assert!(peak.is_finite());
826
827        detector.reset();
828        assert!(detector.history[0].is_empty());
829    }
830
831    #[test]
832    fn test_lookahead_buffer() {
833        let mut buffer = LookaheadBuffer::new(1.0, 48000.0, 2);
834
835        // First samples should be delayed
836        let (delayed, _peak) = buffer.process(0, 1.0, 1.0);
837        assert!(delayed.abs() < f64::EPSILON); // Still filling buffer
838
839        // Reset
840        buffer.reset();
841        assert!(buffer.buffers[0].is_empty());
842    }
843
844    #[test]
845    fn test_gain_smoother() {
846        let mut smoother = GainSmoother::new(100.0, 48000.0);
847
848        // Attack should be instant
849        let gain = smoother.update(0.5);
850        assert!((gain - 0.5).abs() < f64::EPSILON);
851
852        // Release should be gradual
853        let gain1 = smoother.update(1.0);
854        let gain2 = smoother.update(1.0);
855        assert!(gain1 < gain2);
856        assert!(gain2 < 1.0);
857
858        smoother.reset();
859        assert!((smoother.current_gain - 1.0).abs() < f64::EPSILON);
860    }
861
862    #[test]
863    fn test_limiter_filter_creation() {
864        let config = LimiterConfig::brickwall(-0.3);
865        let filter = LimiterFilter::new(NodeId(1), "limiter", config);
866
867        assert_eq!(filter.id(), NodeId(1));
868        assert_eq!(filter.name(), "limiter");
869        assert_eq!(filter.node_type(), NodeType::Filter);
870    }
871
872    #[test]
873    fn test_limiter_filter_ports() {
874        let config = LimiterConfig::default();
875        let filter = LimiterFilter::new(NodeId(0), "test", config);
876
877        assert_eq!(filter.inputs().len(), 1);
878        assert_eq!(filter.outputs().len(), 1);
879        assert_eq!(filter.inputs()[0].port_type, PortType::Audio);
880    }
881
882    #[test]
883    fn test_process_none() {
884        let config = LimiterConfig::default();
885        let mut filter = LimiterFilter::new(NodeId(0), "test", config);
886
887        let result = filter.process(None).expect("process should succeed");
888        assert!(result.is_none());
889    }
890
891    #[test]
892    fn test_process_audio() {
893        let config = LimiterConfig::brickwall(-6.0);
894        let mut filter = LimiterFilter::new(NodeId(0), "test", config);
895
896        let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Mono);
897        let mut samples = BytesMut::new();
898        for _ in 0..100 {
899            samples.extend_from_slice(&1.0f32.to_le_bytes()); // Full scale
900        }
901        frame.samples = AudioBuffer::Interleaved(samples.freeze());
902
903        let result = filter
904            .process(Some(FilterFrame::Audio(frame)))
905            .expect("process should succeed");
906        assert!(result.is_some());
907
908        if let Some(FilterFrame::Audio(output)) = result {
909            if let AudioBuffer::Interleaved(data) = &output.samples {
910                // After lookahead delay, samples should be limited
911                let last_offset = data.len() - 4;
912                let sample = f32::from_le_bytes([
913                    data[last_offset],
914                    data[last_offset + 1],
915                    data[last_offset + 2],
916                    data[last_offset + 3],
917                ]);
918                let ceiling = LimiterConfig::db_to_linear(-6.0);
919                assert!(sample.abs() <= ceiling as f32 + 0.01);
920            }
921        }
922    }
923
924    #[test]
925    fn test_gain_reduction_metering() {
926        let config = LimiterConfig::default();
927        let filter = LimiterFilter::new(NodeId(0), "test", config);
928
929        // Before processing, GR should be 0
930        assert!(filter.gain_reduction_db().abs() < f64::EPSILON);
931    }
932
933    #[test]
934    fn test_state_transitions() {
935        let config = LimiterConfig::default();
936        let mut filter = LimiterFilter::new(NodeId(0), "test", config);
937
938        assert!(filter.set_state(NodeState::Processing).is_ok());
939        assert_eq!(filter.state(), NodeState::Processing);
940
941        assert!(filter.reset().is_ok());
942        assert_eq!(filter.state(), NodeState::Idle);
943    }
944}