Skip to main content

math_audio_dsp/
detector.rs

1// ============================================================================
2// Level Detector — Peak and RMS detection for dynamics plugins
3// ============================================================================
4
5/// Detection mode for level measurement.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum DetectionMode {
8    /// Instantaneous peak (absolute value).
9    Peak,
10    /// RMS with a sliding window.
11    Rms { window_ms: f32 },
12}
13
14/// A level detector that can operate in Peak or RMS mode.
15///
16/// Used by compressor, expander, gate, and multiband dynamics plugins
17/// to measure the input signal level for gain computation.
18#[derive(Debug, Clone)]
19pub struct LevelDetector {
20    mode: DetectionMode,
21    /// Running sum of squared samples (for RMS).
22    sum_sq: f64,
23    /// Circular buffer of squared samples (for RMS).
24    window_buf: Vec<f32>,
25    /// Write position in the circular buffer.
26    window_pos: usize,
27    /// Window length in samples.
28    window_len: usize,
29    sample_rate: u32,
30}
31
32impl LevelDetector {
33    pub fn new(mode: DetectionMode, sample_rate: u32) -> Self {
34        let window_len = match mode {
35            DetectionMode::Peak => 0,
36            DetectionMode::Rms { window_ms } => {
37                (window_ms * 0.001 * sample_rate as f32).round() as usize
38            }
39        }
40        .max(1);
41
42        Self {
43            mode,
44            sum_sq: 0.0,
45            window_buf: if matches!(mode, DetectionMode::Rms { .. }) {
46                vec![0.0; window_len]
47            } else {
48                Vec::new()
49            },
50            window_pos: 0,
51            window_len,
52            sample_rate,
53        }
54    }
55
56    /// Process one sample and return the detected level in dB.
57    #[inline]
58    pub fn process(&mut self, sample: f32) -> f32 {
59        let lin = self.process_linear(sample);
60        if lin < 1e-12 {
61            -120.0
62        } else {
63            20.0 * lin.log10()
64        }
65    }
66
67    /// Process one sample and return the detected level as linear amplitude (not dB).
68    #[inline]
69    pub fn process_linear(&mut self, sample: f32) -> f32 {
70        match self.mode {
71            DetectionMode::Peak => sample.abs(),
72            DetectionMode::Rms { .. } => {
73                let sq = (sample * sample) as f64;
74                let oldest = self.window_buf[self.window_pos] as f64;
75                self.sum_sq = (self.sum_sq + sq - oldest).max(0.0);
76                self.window_buf[self.window_pos] = sample * sample;
77                self.window_pos = (self.window_pos + 1) % self.window_len;
78
79                (self.sum_sq / self.window_len as f64).sqrt() as f32
80            }
81        }
82    }
83
84    pub fn reset(&mut self) {
85        self.sum_sq = 0.0;
86        self.window_buf.fill(0.0);
87        self.window_pos = 0;
88    }
89
90    /// Change detection mode. Resets internal state.
91    pub fn set_mode(&mut self, mode: DetectionMode) {
92        self.mode = mode;
93        let new_len = match mode {
94            DetectionMode::Peak => 1,
95            DetectionMode::Rms { window_ms } => {
96                (window_ms * 0.001 * self.sample_rate as f32).round() as usize
97            }
98        }
99        .max(1);
100
101        self.window_len = new_len;
102        if matches!(mode, DetectionMode::Rms { .. }) {
103            self.window_buf.resize(new_len, 0.0);
104        }
105        self.reset();
106    }
107
108    pub fn mode(&self) -> DetectionMode {
109        self.mode
110    }
111
112    pub fn sample_rate(&self) -> u32 {
113        self.sample_rate
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_peak_detection() {
123        let mut det = LevelDetector::new(DetectionMode::Peak, 48000);
124        // 1.0 amplitude → 0 dB
125        let db = det.process(1.0);
126        assert!((db - 0.0).abs() < 0.01);
127
128        // 0.1 amplitude → -20 dB
129        let db = det.process(0.1);
130        assert!((db - (-20.0)).abs() < 0.01);
131
132        // Negative sample → same as positive
133        let db = det.process(-0.5);
134        let expected = 20.0 * 0.5f32.log10();
135        assert!((db - expected).abs() < 0.01);
136    }
137
138    #[test]
139    fn test_rms_detection() {
140        let mut det = LevelDetector::new(DetectionMode::Rms { window_ms: 10.0 }, 48000);
141        // Feed constant 1.0 for window_len samples
142        let window_len = (10.0f32 * 0.001 * 48000.0).round() as usize;
143        for _ in 0..window_len {
144            det.process(1.0);
145        }
146        // RMS of constant 1.0 = 1.0 → 0 dB
147        let db = det.process(1.0);
148        assert!((db - 0.0).abs() < 0.1);
149    }
150
151    #[test]
152    fn test_silence_floor() {
153        let mut det = LevelDetector::new(DetectionMode::Peak, 48000);
154        let db = det.process(0.0);
155        assert!(db <= -120.0);
156    }
157
158    #[test]
159    fn test_rms_reset() {
160        let mut det = LevelDetector::new(DetectionMode::Rms { window_ms: 10.0 }, 48000);
161        for _ in 0..1000 {
162            det.process(1.0);
163        }
164        det.reset();
165        let db = det.process(0.0);
166        assert!(db <= -120.0);
167    }
168}