Skip to main content

math_audio_dsp/
delta_monitor.rs

1// ============================================================================
2// Delta Monitor — "Solo what's being removed/added"
3// ============================================================================
4//
5// Computes the difference between the dry (input) and wet (processed) signal.
6// Essential for spectral compressors and denoisers where you want to hear
7// exactly what the processor is removing.
8//
9// HARD RULES:
10// - No allocations
11// - Zero-cost when disabled
12
13/// Monitors the delta (difference) between input and output of a processor.
14///
15/// When enabled, replaces the output with `output - input`, letting the user
16/// hear only what the processor changed.
17#[derive(Debug, Clone, Copy)]
18pub struct DeltaMonitor {
19    enabled: bool,
20}
21
22impl Default for DeltaMonitor {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl DeltaMonitor {
29    pub fn new() -> Self {
30        Self { enabled: false }
31    }
32
33    pub fn set_enabled(&mut self, enabled: bool) {
34        self.enabled = enabled;
35    }
36
37    #[inline]
38    pub fn enabled(&self) -> bool {
39        self.enabled
40    }
41
42    /// If enabled, replaces `output[i]` with `output[i] - input[i]` (the delta).
43    /// If disabled, does nothing (zero cost — branch predicted away).
44    ///
45    /// Both slices must have the same length (total samples = frames * channels).
46    #[inline]
47    pub fn apply_if_enabled(&self, input: &[f32], output: &mut [f32]) {
48        if !self.enabled {
49            return;
50        }
51        debug_assert_eq!(input.len(), output.len());
52        for (out, &inp) in output.iter_mut().zip(input.iter()) {
53            *out -= inp;
54        }
55    }
56
57    /// Convenience: capture the dry signal before processing, then call
58    /// `apply_after` on the wet signal. This is for in-place processors
59    /// that overwrite the input buffer.
60    ///
61    /// `dry_copy` must be filled by the caller before processing.
62    #[inline]
63    pub fn apply_from_copy(&self, dry_copy: &[f32], wet_buffer: &mut [f32]) {
64        self.apply_if_enabled(dry_copy, wet_buffer);
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_disabled_is_noop() {
74        let monitor = DeltaMonitor::new();
75        let input = [1.0, 2.0, 3.0];
76        let mut output = [1.5, 2.5, 3.5];
77        let output_copy = output;
78        monitor.apply_if_enabled(&input, &mut output);
79        assert_eq!(output, output_copy);
80    }
81
82    #[test]
83    fn test_enabled_computes_delta() {
84        let mut monitor = DeltaMonitor::new();
85        monitor.set_enabled(true);
86        let input = [1.0, 2.0, 3.0];
87        let mut output = [1.5, 2.5, 3.5];
88        monitor.apply_if_enabled(&input, &mut output);
89        assert!((output[0] - 0.5).abs() < 1e-6);
90        assert!((output[1] - 0.5).abs() < 1e-6);
91        assert!((output[2] - 0.5).abs() < 1e-6);
92    }
93
94    #[test]
95    fn test_delta_of_identical_signals_is_zero() {
96        let mut monitor = DeltaMonitor::new();
97        monitor.set_enabled(true);
98        let input = [1.0, -0.5, 0.3];
99        let mut output = [1.0, -0.5, 0.3];
100        monitor.apply_if_enabled(&input, &mut output);
101        for &s in &output {
102            assert!(s.abs() < 1e-6);
103        }
104    }
105
106    #[test]
107    fn test_default() {
108        let monitor = DeltaMonitor::default();
109        assert!(!monitor.enabled());
110    }
111}