bbx_dsp/blocks/effectors/
dc_blocker.rs

1//! DC offset removal filter using a simple one-pole high-pass design.
2
3use std::marker::PhantomData;
4
5use bbx_core::flush_denormal_f64;
6
7use crate::{
8    block::{Block, DEFAULT_EFFECTOR_INPUT_COUNT, DEFAULT_EFFECTOR_OUTPUT_COUNT},
9    context::DspContext,
10    graph::MAX_BLOCK_OUTPUTS,
11    parameter::ModulationOutput,
12    sample::Sample,
13};
14
15/// A DC blocking filter that removes DC offset from audio signals.
16///
17/// Uses a first-order high-pass filter with approximately 5Hz cutoff.
18pub struct DcBlockerBlock<S: Sample> {
19    /// Whether the DC blocker is enabled.
20    pub enabled: bool,
21
22    x_prev: [f64; MAX_BLOCK_OUTPUTS],
23    y_prev: [f64; MAX_BLOCK_OUTPUTS],
24
25    // Filter coefficient (~0.995 for 5Hz at 44.1kHz)
26    coeff: f64,
27
28    _phantom: PhantomData<S>,
29}
30
31impl<S: Sample> DcBlockerBlock<S> {
32    /// Create a new `DcBlockerBlock`.
33    pub fn new(enabled: bool) -> Self {
34        Self {
35            enabled,
36            x_prev: [0.0; MAX_BLOCK_OUTPUTS],
37            y_prev: [0.0; MAX_BLOCK_OUTPUTS],
38            coeff: 0.995, // Will be recalculated on prepare
39            _phantom: PhantomData,
40        }
41    }
42
43    /// Recalculate filter coefficient for the given sample rate.
44    /// Targets approximately 5Hz cutoff frequency.
45    pub fn set_sample_rate(&mut self, sample_rate: f64) {
46        // DC blocker coefficient: R = 1 - (2 * PI * fc / fs)
47        // For fc = 5Hz, this gives approximately 0.9993 at 44.1kHz
48        let cutoff_hz = 5.0;
49        self.coeff = 1.0 - (2.0 * S::PI.to_f64() * cutoff_hz / sample_rate);
50        self.coeff = self.coeff.clamp(0.9, 0.9999);
51    }
52
53    /// Reset the filter state.
54    pub fn reset(&mut self) {
55        self.x_prev = [0.0; MAX_BLOCK_OUTPUTS];
56        self.y_prev = [0.0; MAX_BLOCK_OUTPUTS];
57    }
58}
59
60impl<S: Sample> Block<S> for DcBlockerBlock<S> {
61    fn process(&mut self, inputs: &[&[S]], outputs: &mut [&mut [S]], _modulation_values: &[S], _context: &DspContext) {
62        if !self.enabled {
63            // Pass through unchanged
64            for (ch, input) in inputs.iter().enumerate() {
65                if ch < outputs.len() {
66                    outputs[ch].copy_from_slice(input);
67                }
68            }
69            return;
70        }
71
72        // Process each channel
73        for (ch, input) in inputs.iter().enumerate() {
74            if ch >= outputs.len() || ch >= MAX_BLOCK_OUTPUTS {
75                break;
76            }
77
78            for (i, &sample) in input.iter().enumerate() {
79                let x = sample.to_f64();
80
81                // y[n] = x[n] - x[n-1] + R * y[n-1]
82                let y = x - self.x_prev[ch] + self.coeff * self.y_prev[ch];
83
84                self.x_prev[ch] = x;
85                // Flush denormals to prevent CPU slowdown during quiet passages
86                self.y_prev[ch] = flush_denormal_f64(y);
87
88                outputs[ch][i] = S::from_f64(y);
89            }
90        }
91    }
92
93    #[inline]
94    fn input_count(&self) -> usize {
95        DEFAULT_EFFECTOR_INPUT_COUNT
96    }
97
98    #[inline]
99    fn output_count(&self) -> usize {
100        DEFAULT_EFFECTOR_OUTPUT_COUNT
101    }
102
103    #[inline]
104    fn modulation_outputs(&self) -> &[ModulationOutput] {
105        &[]
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::channel::ChannelLayout;
113
114    fn test_context(buffer_size: usize) -> DspContext {
115        DspContext {
116            sample_rate: 44100.0,
117            num_channels: 6,
118            buffer_size,
119            current_sample: 0,
120            channel_layout: ChannelLayout::Surround51,
121        }
122    }
123
124    #[test]
125    fn test_dc_blocker_6_channels() {
126        let mut blocker = DcBlockerBlock::<f32>::new(true);
127        blocker.set_sample_rate(44100.0);
128        let context = test_context(4);
129
130        let input: [[f32; 4]; 6] = [[0.5; 4]; 6];
131        let mut outputs: [[f32; 4]; 6] = [[0.0; 4]; 6];
132
133        let input_refs: Vec<&[f32]> = input.iter().map(|ch| ch.as_slice()).collect();
134        let mut output_refs: Vec<&mut [f32]> = outputs.iter_mut().map(|ch| ch.as_mut_slice()).collect();
135
136        blocker.process(&input_refs, &mut output_refs, &[], &context);
137
138        for ch in 0..6 {
139            assert!(outputs[ch][3].abs() > 0.0, "Channel {ch} should have output");
140        }
141    }
142
143    #[test]
144    fn test_dc_blocker_removes_dc_offset() {
145        let mut blocker = DcBlockerBlock::<f32>::new(true);
146        blocker.set_sample_rate(44100.0);
147        let context = test_context(1024);
148
149        let input: [f32; 1024] = [0.5; 1024];
150        let mut output: [f32; 1024] = [0.0; 1024];
151
152        let inputs: [&[f32]; 1] = [&input];
153        let mut outputs: [&mut [f32]; 1] = [&mut output];
154
155        for _ in 0..100 {
156            blocker.process(&inputs, &mut outputs, &[], &context);
157        }
158
159        let final_val = output[1023].abs();
160        assert!(final_val < 0.1, "DC should be mostly removed, got {final_val}");
161    }
162
163    #[test]
164    fn test_dc_blocker_input_output_counts_f32() {
165        let blocker = DcBlockerBlock::<f32>::new(true);
166        assert_eq!(blocker.input_count(), DEFAULT_EFFECTOR_INPUT_COUNT);
167        assert_eq!(blocker.output_count(), DEFAULT_EFFECTOR_OUTPUT_COUNT);
168    }
169
170    #[test]
171    fn test_dc_blocker_input_output_counts_f64() {
172        let blocker = DcBlockerBlock::<f64>::new(true);
173        assert_eq!(blocker.input_count(), DEFAULT_EFFECTOR_INPUT_COUNT);
174        assert_eq!(blocker.output_count(), DEFAULT_EFFECTOR_OUTPUT_COUNT);
175    }
176
177    #[test]
178    fn test_dc_blocker_basic_f64() {
179        let mut blocker = DcBlockerBlock::<f64>::new(true);
180        blocker.set_sample_rate(44100.0);
181        let context = test_context(64);
182
183        let input: [f64; 64] = [0.5; 64];
184        let mut output: [f64; 64] = [0.0; 64];
185
186        let inputs: [&[f64]; 1] = [&input];
187        let mut outputs: [&mut [f64]; 1] = [&mut output];
188
189        blocker.process(&inputs, &mut outputs, &[], &context);
190
191        assert!(output[63].abs() > 0.0, "DC blocker should produce output");
192    }
193
194    #[test]
195    fn test_dc_blocker_modulation_outputs_empty() {
196        let blocker = DcBlockerBlock::<f32>::new(true);
197        assert!(blocker.modulation_outputs().is_empty());
198    }
199
200    #[test]
201    fn test_dc_blocker_disabled_passthrough() {
202        let mut blocker = DcBlockerBlock::<f32>::new(false);
203        let context = test_context(4);
204
205        let input: [f32; 4] = [0.5, 0.6, 0.7, 0.8];
206        let mut output: [f32; 4] = [0.0; 4];
207
208        let inputs: [&[f32]; 1] = [&input];
209        let mut outputs: [&mut [f32]; 1] = [&mut output];
210
211        blocker.process(&inputs, &mut outputs, &[], &context);
212
213        assert_eq!(output, input, "Disabled DC blocker should pass through unchanged");
214    }
215
216    #[test]
217    fn test_dc_blocker_reset() {
218        let mut blocker = DcBlockerBlock::<f32>::new(true);
219        blocker.set_sample_rate(44100.0);
220        let context = test_context(64);
221
222        let input: [f32; 64] = [0.5; 64];
223        let mut output: [f32; 64] = [0.0; 64];
224
225        let inputs: [&[f32]; 1] = [&input];
226        let mut outputs: [&mut [f32]; 1] = [&mut output];
227
228        blocker.process(&inputs, &mut outputs, &[], &context);
229        blocker.reset();
230
231        let mut output2: [f32; 64] = [0.0; 64];
232        let mut outputs2: [&mut [f32]; 1] = [&mut output2];
233        blocker.process(&inputs, &mut outputs2, &[], &context);
234
235        assert!((output[0] - output2[0]).abs() < 1e-6, "Reset should clear state");
236    }
237}