firewheel_nodes/fast_filters/
lowpass.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::{OnePoleIirLPFCoeff, OnePoleIirLPFCoeffSimd, OnePoleIirLPFSimd},
9 smoothing_filter::DEFAULT_SMOOTH_SECONDS,
10 },
11 },
12 event::ProcEvents,
13 node::{
14 AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, EmptyConfig,
15 ProcBuffers, ProcExtra, ProcInfo, ProcStreamCtx, ProcessStatus,
16 },
17 param::smoother::{SmoothedParam, SmootherConfig},
18 StreamInfo,
19};
20
21use super::{MAX_HZ, MIN_HZ};
22
23pub type FastLowpassMonoNode = FastLowpassNode<1>;
24pub type FastLowpassStereoNode = FastLowpassNode<2>;
25
26#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
28#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
29#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub struct FastLowpassNode<const CHANNELS: usize> {
32 pub cutoff_hz: f32,
34 pub enabled: bool,
36
37 pub smooth_seconds: f32,
41
42 pub coeff_update_factor: CoeffUpdateFactor,
54}
55
56impl<const CHANNELS: usize> Default for FastLowpassNode<CHANNELS> {
57 fn default() -> Self {
58 Self {
59 cutoff_hz: 1_000.0,
60 enabled: true,
61 smooth_seconds: DEFAULT_SMOOTH_SECONDS,
62 coeff_update_factor: CoeffUpdateFactor::default(),
63 }
64 }
65}
66
67impl<const CHANNELS: usize> FastLowpassNode<CHANNELS> {
68 pub const fn from_cutoff_hz(cutoff_hz: f32, enabled: bool) -> Self {
73 Self {
74 cutoff_hz,
75 enabled,
76 smooth_seconds: DEFAULT_SMOOTH_SECONDS,
77 coeff_update_factor: CoeffUpdateFactor::DEFAULT,
78 }
79 }
80}
81
82impl<const CHANNELS: usize> AudioNode for FastLowpassNode<CHANNELS> {
83 type Configuration = EmptyConfig;
84
85 fn info(&self, _config: &Self::Configuration) -> AudioNodeInfo {
86 AudioNodeInfo::new()
87 .debug_name("fast_lowpass")
88 .channel_config(ChannelConfig {
89 num_inputs: ChannelCount::new(CHANNELS as u32).unwrap(),
90 num_outputs: ChannelCount::new(CHANNELS as u32).unwrap(),
91 })
92 }
93
94 fn construct_processor(
95 &self,
96 _config: &Self::Configuration,
97 cx: ConstructProcessorContext,
98 ) -> impl AudioNodeProcessor {
99 let sample_rate_recip = cx.stream_info.sample_rate_recip as f32;
100
101 let cutoff_hz = self.cutoff_hz.clamp(MIN_HZ, MAX_HZ);
102
103 Processor {
104 filter: OnePoleIirLPFSimd::default(),
105 coeff: OnePoleIirLPFCoeffSimd::<CHANNELS>::splat(OnePoleIirLPFCoeff::new(
106 cutoff_hz,
107 sample_rate_recip,
108 )),
109 cutoff_hz: SmoothedParam::new(
110 cutoff_hz,
111 SmootherConfig {
112 smooth_seconds: self.smooth_seconds,
113 ..Default::default()
114 },
115 cx.stream_info.sample_rate,
116 ),
117 enable_declicker: Declicker::from_enabled(self.enabled),
118 coeff_update_mask: self.coeff_update_factor.mask(),
119 }
120 }
121}
122
123struct Processor<const CHANNELS: usize> {
124 filter: OnePoleIirLPFSimd<CHANNELS>,
125 coeff: OnePoleIirLPFCoeffSimd<CHANNELS>,
126
127 cutoff_hz: SmoothedParam,
128 enable_declicker: Declicker,
129 coeff_update_mask: CoeffUpdateMask,
130}
131
132impl<const CHANNELS: usize> AudioNodeProcessor for Processor<CHANNELS> {
133 fn process(
134 &mut self,
135 info: &ProcInfo,
136 buffers: ProcBuffers,
137 events: &mut ProcEvents,
138 extra: &mut ProcExtra,
139 ) -> ProcessStatus {
140 let mut cutoff_changed = false;
141
142 for patch in events.drain_patches::<FastLowpassNode<CHANNELS>>() {
143 match patch {
144 FastLowpassNodePatch::CutoffHz(cutoff) => {
145 cutoff_changed = true;
146 self.cutoff_hz.set_value(cutoff.clamp(MIN_HZ, MAX_HZ));
147 }
148 FastLowpassNodePatch::Enabled(enabled) => {
149 self.enable_declicker
151 .fade_to_enabled(enabled, &extra.declick_values);
152 }
153 FastLowpassNodePatch::SmoothSeconds(seconds) => {
154 self.cutoff_hz.set_smooth_seconds(seconds, info.sample_rate);
155 }
156 FastLowpassNodePatch::CoeffUpdateFactor(f) => {
157 self.coeff_update_mask = f.mask();
158 }
159 }
160 }
161
162 if self.enable_declicker.disabled() {
163 return ProcessStatus::Bypass;
165 }
166
167 if info.in_silence_mask.all_channels_silent(CHANNELS) && self.enable_declicker.has_settled()
168 {
169 self.cutoff_hz.reset_to_target();
174 self.filter.reset();
175 self.enable_declicker.reset_to_target();
176
177 return ProcessStatus::ClearAllOutputs;
178 }
179
180 assert!(buffers.inputs.len() == CHANNELS);
181 assert!(buffers.outputs.len() == CHANNELS);
182 for ch in buffers.inputs.iter() {
183 assert!(ch.len() >= info.frames);
184 }
185 for ch in buffers.outputs.iter() {
186 assert!(ch.len() >= info.frames);
187 }
188
189 if self.cutoff_hz.is_smoothing() {
190 for i in 0..info.frames {
191 let cutoff_hz = self.cutoff_hz.next_smoothed();
192
193 if self.coeff_update_mask.do_update(i) {
200 self.coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
201 cutoff_hz,
202 info.sample_rate_recip as f32,
203 ));
204 }
205
206 let s: [f32; CHANNELS] = core::array::from_fn(|ch_i| {
207 unsafe { *buffers.inputs.get_unchecked(ch_i).get_unchecked(i) }
209 });
210
211 let out = self.filter.process(s, &self.coeff);
212
213 for ch_i in 0..CHANNELS {
214 unsafe {
216 *buffers.outputs.get_unchecked_mut(ch_i).get_unchecked_mut(i) = out[ch_i];
217 }
218 }
219 }
220
221 if self.cutoff_hz.settle() {
222 self.coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
223 self.cutoff_hz.target_value(),
224 info.sample_rate_recip as f32,
225 ));
226 }
227 } else {
228 if cutoff_changed {
231 self.coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
232 self.cutoff_hz.target_value(),
233 info.sample_rate_recip as f32,
234 ));
235 }
236
237 for i in 0..info.frames {
238 let s: [f32; CHANNELS] = core::array::from_fn(|ch_i| {
239 unsafe { *buffers.inputs.get_unchecked(ch_i).get_unchecked(i) }
241 });
242
243 let out = self.filter.process(s, &self.coeff);
244
245 for ch_i in 0..CHANNELS {
246 unsafe {
248 *buffers.outputs.get_unchecked_mut(ch_i).get_unchecked_mut(i) = out[ch_i];
249 }
250 }
251 }
252 }
253
254 self.enable_declicker.process_crossfade(
256 buffers.inputs,
257 buffers.outputs,
258 info.frames,
259 &extra.declick_values,
260 DeclickFadeCurve::Linear,
261 );
262
263 ProcessStatus::OutputsModified
264 }
265
266 fn new_stream(&mut self, stream_info: &StreamInfo, _context: &mut ProcStreamCtx) {
267 self.cutoff_hz.update_sample_rate(stream_info.sample_rate);
268 self.coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
269 self.cutoff_hz.target_value(),
270 stream_info.sample_rate_recip as f32,
271 ));
272 }
273}