firewheel_nodes/fast_filters/
bandpass.rs

1use firewheel_core::{
2    channel_config::{ChannelConfig, ChannelCount},
3    diff::{Diff, Patch},
4    dsp::{
5        coeff_update::{CoeffUpdateFactor, CoeffUpdateMask},
6        declick::{DeclickFadeCurve, Declicker},
7        filter::{
8            single_pole_iir::{
9                OnePoleIirHPFCoeff, OnePoleIirHPFCoeffSimd, OnePoleIirHPFSimd, OnePoleIirLPFCoeff,
10                OnePoleIirLPFCoeffSimd, OnePoleIirLPFSimd,
11            },
12            smoothing_filter::DEFAULT_SMOOTH_SECONDS,
13        },
14    },
15    event::ProcEvents,
16    node::{
17        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, EmptyConfig,
18        ProcBuffers, ProcExtra, ProcInfo, ProcStreamCtx, ProcessStatus,
19    },
20    param::smoother::{SmoothedParam, SmootherConfig},
21    StreamInfo,
22};
23
24use super::{MAX_HZ, MIN_HZ};
25
26pub type FastBandpassMonoNode = FastBandpassNode<1>;
27pub type FastBandpassStereoNode = FastBandpassNode<2>;
28
29/// A simple single-pole IIR bandpass filter node that is computationally
30/// efficient
31///
32/// It is computationally efficient, but it doesn't do that great of
33/// a job at attenuating low frequencies.
34#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
35#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
36#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub struct FastBandpassNode<const CHANNELS: usize> {
39    /// The cutoff frequency in hertz in the range `[20.0, 20480.0]`.
40    pub cutoff_hz: f32,
41    /// Whether or not this node is enabled.
42    pub enabled: bool,
43
44    /// The time in seconds of the internal smoothing filter.
45    ///
46    /// By default this is set to `0.015` (15ms).
47    pub smooth_seconds: f32,
48
49    /// An exponent representing the rate at which DSP coefficients are
50    /// updated when parameters are being smoothed.
51    ///
52    /// Smaller values will produce less "stair-stepping" artifacts,
53    /// but will also consume more CPU.
54    ///
55    /// The resulting number of frames (samples in a single channel of audio)
56    /// that will elapse between each update is calculated as
57    /// `2^coeff_update_factor`.
58    ///
59    /// By default this is set to `5`.
60    pub coeff_update_factor: CoeffUpdateFactor,
61}
62
63impl<const CHANNELS: usize> Default for FastBandpassNode<CHANNELS> {
64    fn default() -> Self {
65        Self {
66            cutoff_hz: 1_000.0,
67            enabled: true,
68            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
69            coeff_update_factor: CoeffUpdateFactor::default(),
70        }
71    }
72}
73
74impl<const CHANNELS: usize> FastBandpassNode<CHANNELS> {
75    /// Construct a new `FastBandpassNode` from the given parameters.
76    ///
77    /// * `cutoff_hz` - The cutoff frequency in hertz in the range `[20.0, 20480.0]`
78    /// * `enabled` - Whether or not this node is enabled
79    pub const fn from_cutoff_hz(cutoff_hz: f32, enabled: bool) -> Self {
80        Self {
81            cutoff_hz,
82            enabled,
83            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
84            coeff_update_factor: CoeffUpdateFactor::DEFAULT,
85        }
86    }
87}
88
89impl<const CHANNELS: usize> AudioNode for FastBandpassNode<CHANNELS> {
90    type Configuration = EmptyConfig;
91
92    fn info(&self, _config: &Self::Configuration) -> AudioNodeInfo {
93        AudioNodeInfo::new()
94            .debug_name("fast_bandpass")
95            .channel_config(ChannelConfig {
96                num_inputs: ChannelCount::new(CHANNELS as u32).unwrap(),
97                num_outputs: ChannelCount::new(CHANNELS as u32).unwrap(),
98            })
99    }
100
101    fn construct_processor(
102        &self,
103        _config: &Self::Configuration,
104        cx: ConstructProcessorContext,
105    ) -> impl AudioNodeProcessor {
106        let sample_rate_recip = cx.stream_info.sample_rate_recip as f32;
107
108        let cutoff_hz = self.cutoff_hz.clamp(MIN_HZ, MAX_HZ);
109
110        Processor {
111            lpf: OnePoleIirLPFSimd::default(),
112            lpf_coeff: OnePoleIirLPFCoeffSimd::<CHANNELS>::splat(OnePoleIirLPFCoeff::new(
113                cutoff_hz,
114                sample_rate_recip,
115            )),
116            hpf: OnePoleIirHPFSimd::default(),
117            hpf_coeff: OnePoleIirHPFCoeffSimd::<CHANNELS>::splat(OnePoleIirHPFCoeff::new(
118                cutoff_hz,
119                sample_rate_recip,
120            )),
121            cutoff_hz: SmoothedParam::new(
122                cutoff_hz,
123                SmootherConfig {
124                    smooth_seconds: self.smooth_seconds,
125                    ..Default::default()
126                },
127                cx.stream_info.sample_rate,
128            ),
129            enable_declicker: Declicker::from_enabled(self.enabled),
130            coeff_update_mask: self.coeff_update_factor.mask(),
131        }
132    }
133}
134
135struct Processor<const CHANNELS: usize> {
136    lpf: OnePoleIirLPFSimd<CHANNELS>,
137    hpf: OnePoleIirHPFSimd<CHANNELS>,
138    lpf_coeff: OnePoleIirLPFCoeffSimd<CHANNELS>,
139    hpf_coeff: OnePoleIirHPFCoeffSimd<CHANNELS>,
140
141    cutoff_hz: SmoothedParam,
142    enable_declicker: Declicker,
143    coeff_update_mask: CoeffUpdateMask,
144}
145
146impl<const CHANNELS: usize> AudioNodeProcessor for Processor<CHANNELS> {
147    fn process(
148        &mut self,
149        info: &ProcInfo,
150        buffers: ProcBuffers,
151        events: &mut ProcEvents,
152        extra: &mut ProcExtra,
153    ) -> ProcessStatus {
154        let mut cutoff_changed = false;
155
156        for patch in events.drain_patches::<FastBandpassNode<CHANNELS>>() {
157            match patch {
158                FastBandpassNodePatch::CutoffHz(cutoff) => {
159                    cutoff_changed = true;
160                    self.cutoff_hz.set_value(cutoff.clamp(MIN_HZ, MAX_HZ));
161                }
162                FastBandpassNodePatch::Enabled(enabled) => {
163                    // Tell the declicker to crossfade.
164                    self.enable_declicker
165                        .fade_to_enabled(enabled, &extra.declick_values);
166                }
167                FastBandpassNodePatch::SmoothSeconds(seconds) => {
168                    self.cutoff_hz.set_smooth_seconds(seconds, info.sample_rate);
169                }
170                FastBandpassNodePatch::CoeffUpdateFactor(f) => {
171                    self.coeff_update_mask = f.mask();
172                }
173            }
174        }
175
176        if self.enable_declicker.disabled() {
177            // Disabled. Bypass this node.
178            return ProcessStatus::Bypass;
179        }
180
181        if info.in_silence_mask.all_channels_silent(CHANNELS) && self.enable_declicker.has_settled()
182        {
183            // Outputs will be silent, so no need to process.
184
185            // Reset the smoothers and filters since they don't need to smooth any
186            // output.
187            self.cutoff_hz.reset_to_target();
188            self.lpf.reset();
189            self.hpf.reset();
190            self.enable_declicker.reset_to_target();
191
192            return ProcessStatus::ClearAllOutputs;
193        }
194
195        assert!(buffers.inputs.len() == CHANNELS);
196        assert!(buffers.outputs.len() == CHANNELS);
197        for ch in buffers.inputs.iter() {
198            assert!(ch.len() >= info.frames);
199        }
200        for ch in buffers.outputs.iter() {
201            assert!(ch.len() >= info.frames);
202        }
203
204        if self.cutoff_hz.is_smoothing() {
205            for i in 0..info.frames {
206                let cutoff_hz = self.cutoff_hz.next_smoothed();
207
208                // Because recalculating filter coefficients is expensive, a trick like
209                // this can be used to only recalculate them every few frames.
210                //
211                // TODO: use core::hint::cold_path() once that stabilizes
212                //
213                // TODO: Alternatively, this could be optimized using a lookup table
214                if self.coeff_update_mask.do_update(i) {
215                    self.lpf_coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
216                        cutoff_hz,
217                        info.sample_rate_recip as f32,
218                    ));
219                    self.hpf_coeff = OnePoleIirHPFCoeffSimd::splat(OnePoleIirHPFCoeff::new(
220                        cutoff_hz,
221                        info.sample_rate_recip as f32,
222                    ));
223                }
224
225                let s: [f32; CHANNELS] = core::array::from_fn(|ch_i| {
226                    // Safety: These bounds have been checked above.
227                    unsafe { *buffers.inputs.get_unchecked(ch_i).get_unchecked(i) }
228                });
229
230                let out = self.lpf.process(s, &self.lpf_coeff);
231                let out = self.hpf.process(out, &self.hpf_coeff);
232
233                for ch_i in 0..CHANNELS {
234                    // Safety: These bounds have been checked above.
235                    unsafe {
236                        *buffers.outputs.get_unchecked_mut(ch_i).get_unchecked_mut(i) = out[ch_i];
237                    }
238                }
239            }
240
241            if self.cutoff_hz.settle() {
242                self.lpf_coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
243                    self.cutoff_hz.target_value(),
244                    info.sample_rate_recip as f32,
245                ));
246                self.hpf_coeff = OnePoleIirHPFCoeffSimd::splat(OnePoleIirHPFCoeff::new(
247                    self.cutoff_hz.target_value(),
248                    info.sample_rate_recip as f32,
249                ));
250            }
251        } else {
252            // The cutoff parameter is not currently smoothing, so we can optimize by
253            // only updating the filter coefficients once.
254            if cutoff_changed {
255                self.lpf_coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
256                    self.cutoff_hz.target_value(),
257                    info.sample_rate_recip as f32,
258                ));
259                self.hpf_coeff = OnePoleIirHPFCoeffSimd::splat(OnePoleIirHPFCoeff::new(
260                    self.cutoff_hz.target_value(),
261                    info.sample_rate_recip as f32,
262                ));
263            }
264
265            for i in 0..info.frames {
266                let s: [f32; CHANNELS] = core::array::from_fn(|ch_i| {
267                    // Safety: These bounds have been checked above.
268                    unsafe { *buffers.inputs.get_unchecked(ch_i).get_unchecked(i) }
269                });
270
271                let out = self.lpf.process(s, &self.lpf_coeff);
272                let out = self.hpf.process(out, &self.hpf_coeff);
273
274                for ch_i in 0..CHANNELS {
275                    // Safety: These bounds have been checked above.
276                    unsafe {
277                        *buffers.outputs.get_unchecked_mut(ch_i).get_unchecked_mut(i) = out[ch_i];
278                    }
279                }
280            }
281        }
282
283        // Crossfade between the wet and dry signals to declick enabling/disabling.
284        self.enable_declicker.process_crossfade(
285            buffers.inputs,
286            buffers.outputs,
287            info.frames,
288            &extra.declick_values,
289            DeclickFadeCurve::Linear,
290        );
291
292        ProcessStatus::OutputsModified
293    }
294
295    fn new_stream(&mut self, stream_info: &StreamInfo, _context: &mut ProcStreamCtx) {
296        self.cutoff_hz.update_sample_rate(stream_info.sample_rate);
297        self.lpf_coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
298            self.cutoff_hz.target_value(),
299            stream_info.sample_rate_recip as f32,
300        ));
301        self.hpf_coeff = OnePoleIirHPFCoeffSimd::splat(OnePoleIirHPFCoeff::new(
302            self.cutoff_hz.target_value(),
303            stream_info.sample_rate_recip as f32,
304        ));
305    }
306}