firewheel_nodes/fast_filters/
bandpass.rs1use 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#[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 pub cutoff_hz: f32,
41 pub enabled: bool,
43
44 pub smooth_seconds: f32,
48
49 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 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 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 return ProcessStatus::Bypass;
179 }
180
181 if info.in_silence_mask.all_channels_silent(CHANNELS) && self.enable_declicker.has_settled()
182 {
183 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 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 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 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 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 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 unsafe {
277 *buffers.outputs.get_unchecked_mut(ch_i).get_unchecked_mut(i) = out[ch_i];
278 }
279 }
280 }
281 }
282
283 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}