Skip to content

Commit 56e50df

Browse files
author
doomy
committed
smoothed filter values
1 parent 672278b commit 56e50df

File tree

1 file changed

+113
-61
lines changed

1 file changed

+113
-61
lines changed

crates/firewheel-nodes/src/echo.rs

Lines changed: 113 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ use firewheel_core::{
77
buffer::ChannelBuffer,
88
declick::{DeclickFadeCurve, Declicker},
99
fade::FadeCurve,
10-
filter::single_pole_iir::{
11-
OnePoleIirHPF, OnePoleIirHPFCoeff, OnePoleIirLPF, OnePoleIirLPFCoeff,
10+
filter::{
11+
single_pole_iir::{
12+
OnePoleIirHPF, OnePoleIirHPFCoeff, OnePoleIirLPF, OnePoleIirLPFCoeff,
13+
},
14+
smoothing_filter::DEFAULT_SMOOTH_SECONDS,
1215
},
1316
mix::{Mix, MixDSP},
1417
volume::Volume,
@@ -65,7 +68,7 @@ impl Default for EchoNodeConfig {
6568
#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
6669
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
6770
pub struct EchoNode<const CHANNELS: usize> {
68-
/// TODO: must be smooothed! The lowpass frequency in hertz in the range
71+
/// The lowpass frequency in hertz in the range
6972
/// `[20.0, 20480.0]`.
7073
pub feedback_lpf: f32,
7174
/// The highpass frequency in hertz in the range `[20.0, 20480.0]`.
@@ -96,6 +99,11 @@ pub struct EchoNode<const CHANNELS: usize> {
9699
/// By default this is set to [`FadeCurve::EqualPower3dB`].
97100
pub fade_curve: FadeCurve,
98101

102+
/// Adjusts the time in seconds over which parameters are smoothed.
103+
///
104+
/// Defaults to `0.015` (15ms).
105+
pub smooth_seconds: f32,
106+
99107
pub stop: Notify<()>,
100108
pub paused: bool,
101109
}
@@ -112,6 +120,16 @@ impl<const CHANNELS: usize> Default for EchoNode<CHANNELS> {
112120
delay_seconds: [0.5; CHANNELS],
113121
feedback: [Volume::from_percent(30.0); CHANNELS],
114122
crossfeed: [Volume::from_percent(0.0); CHANNELS],
123+
smooth_seconds: DEFAULT_SMOOTH_SECONDS,
124+
}
125+
}
126+
}
127+
128+
impl<const CHANNELS: usize> EchoNode<CHANNELS> {
129+
fn smoother_config(&self) -> SmootherConfig {
130+
SmootherConfig {
131+
smooth_seconds: self.smooth_seconds,
132+
..Default::default()
115133
}
116134
}
117135
}
@@ -130,62 +148,55 @@ impl<const CHANNELS: usize> AudioNode for EchoNode<CHANNELS> {
130148
config: &Self::Configuration,
131149
cx: ConstructProcessorContext,
132150
) -> impl AudioNodeProcessor {
133-
const DELAY_SMOOTH_SECONDS: f32 = 0.15;
134151
let max_frames = cx.stream_info.max_block_frames.get() as usize;
135152
let sample_rate = cx.stream_info.sample_rate;
153+
let smoother_config = self.smoother_config();
136154
Processor::<CHANNELS> {
137155
params: *self,
138156
declicker: Declicker::default(),
139-
delay_seconds_smoothed: self.delay_seconds.map(|channel| {
140-
SmoothedParam::new(
141-
channel,
142-
SmootherConfig {
143-
smooth_seconds: DELAY_SMOOTH_SECONDS,
144-
..Default::default()
145-
},
146-
sample_rate,
147-
)
148-
}),
149-
feedback_smoothed: self.feedback.map(|channel| {
150-
SmoothedParam::new(channel.linear(), Default::default(), sample_rate)
151-
}),
152-
crossfeed_smoothed: self.crossfeed.map(|channel| {
153-
SmoothedParam::new(channel.linear(), Default::default(), sample_rate)
154-
}),
157+
delay_seconds_smoothed: self
158+
.delay_seconds
159+
.map(|channel| SmoothedParam::new(channel, smoother_config, sample_rate)),
160+
feedback_smoothed: self
161+
.feedback
162+
.map(|channel| SmoothedParam::new(channel.linear(), smoother_config, sample_rate)),
163+
crossfeed_smoothed: self
164+
.crossfeed
165+
.map(|channel| SmoothedParam::new(channel.linear(), smoother_config, sample_rate)),
155166
delay_seconds_smoothed_buffer: ChannelBuffer::<f32, CHANNELS>::new(max_frames),
156167
feedback_smoothed_buffer: ChannelBuffer::<f32, CHANNELS>::new(max_frames),
157168
crossfeed_smoothed_buffer: ChannelBuffer::<f32, CHANNELS>::new(max_frames),
158169
delay_buffers: from_fn(|_| DelayLine::<f32>::initialized(config.buffer_capacity)),
159170
mix_dsp: MixDSP::new(
160171
self.mix,
161172
self.fade_curve,
162-
SmootherConfig::default(),
173+
smoother_config,
163174
cx.stream_info.sample_rate,
164175
),
165-
tap_lpf_coeff: OnePoleIirLPFCoeff::new(
166-
// Ensure cutoff is above 0Hz
167-
self.feedback_lpf.max(0.0),
168-
cx.stream_info.sample_rate_recip as f32,
169-
),
170-
tap_hpf_coeff: OnePoleIirHPFCoeff::new(
171-
// Ensure cutoff is above 0Hz
172-
self.feedback_hpf.max(0.0),
173-
cx.stream_info.sample_rate_recip as f32,
174-
),
175-
tap_lpf: [OnePoleIirLPF::default(); CHANNELS],
176-
tap_hpf: [OnePoleIirHPF::default(); CHANNELS],
176+
feedback_lpf: [OnePoleIirLPF::default(); CHANNELS],
177+
feedback_hpf: [OnePoleIirHPF::default(); CHANNELS],
177178
prev_delay_seconds: [None; CHANNELS],
178179
next_delay_seconds: [None; CHANNELS],
180+
feedback_lpf_smoothed: SmoothedParam::new(
181+
self.feedback_lpf,
182+
smoother_config,
183+
sample_rate,
184+
),
185+
feedback_hpf_smoothed: SmoothedParam::new(
186+
self.feedback_hpf,
187+
smoother_config,
188+
sample_rate,
189+
),
179190
}
180191
}
181192
}
182193

183194
struct Processor<const CHANNELS: usize> {
184195
params: EchoNode<CHANNELS>,
185-
tap_lpf: [OnePoleIirLPF; CHANNELS],
186-
tap_hpf: [OnePoleIirHPF; CHANNELS],
187-
tap_lpf_coeff: OnePoleIirLPFCoeff,
188-
tap_hpf_coeff: OnePoleIirHPFCoeff,
196+
feedback_lpf_smoothed: SmoothedParam,
197+
feedback_hpf_smoothed: SmoothedParam,
198+
feedback_lpf: [OnePoleIirLPF; CHANNELS],
199+
feedback_hpf: [OnePoleIirHPF; CHANNELS],
189200
mix_dsp: MixDSP,
190201
declicker: Declicker,
191202
// Set when transitioning delay seconds. When settled on the new value,
@@ -227,49 +238,62 @@ impl<const CHANNELS: usize> AudioNodeProcessor for Processor<CHANNELS> {
227238
events: &mut ProcEvents,
228239
extra: &mut ProcExtra,
229240
) -> ProcessStatus {
241+
const SCRATCH_CHANNELS: usize = 2;
242+
const LPF_SCRATCH_INDEX: usize = 0;
243+
const HPF_SCRATCH_INDEX: usize = 1;
244+
230245
let mut clear_buffers = false;
231246
for mut patch in events.drain_patches::<EchoNode<CHANNELS>>() {
232247
match &mut patch {
233-
// TODO: This must be a smoothed value!
234-
EchoNodePatch::FeedbackLpf(value) => {
235-
self.tap_lpf_coeff =
236-
OnePoleIirLPFCoeff::new(*value, info.sample_rate_recip as f32);
248+
EchoNodePatch::SmoothSeconds(seconds) => {
249+
// Change all smoothed parameters to new smoothing
250+
let update_smoothing = |param: &mut SmoothedParam| {
251+
param.set_smooth_seconds(*seconds, info.sample_rate);
252+
};
253+
self.crossfeed_smoothed
254+
.iter_mut()
255+
.chain(self.feedback_smoothed.iter_mut())
256+
.chain([self.feedback_hpf_smoothed, self.feedback_lpf_smoothed].iter_mut())
257+
.chain(self.delay_seconds_smoothed.iter_mut())
258+
.for_each(update_smoothing);
259+
}
260+
EchoNodePatch::FeedbackLpf(cutoff_hz) => {
261+
self.feedback_lpf_smoothed.set_value(*cutoff_hz);
237262
}
238-
EchoNodePatch::FeedbackHpf(value) => {
239-
self.tap_hpf_coeff =
240-
OnePoleIirHPFCoeff::new(*value, info.sample_rate_recip as f32);
263+
EchoNodePatch::FeedbackHpf(cutoff_hz) => {
264+
self.feedback_hpf_smoothed.set_value(*cutoff_hz);
241265
}
242-
EchoNodePatch::Mix(value) => {
243-
self.mix_dsp.set_mix(*value, self.params.fade_curve);
266+
EchoNodePatch::Mix(mix) => {
267+
self.mix_dsp.set_mix(*mix, self.params.fade_curve);
244268
}
245-
EchoNodePatch::DelaySeconds((index, value)) => {
269+
EchoNodePatch::DelaySeconds((index, delay_seconds)) => {
246270
// TODO: make more robust
247271
// Check to see if settled.
248272
if self.prev_delay_seconds[*index].is_none() {
249273
self.prev_delay_seconds[*index] =
250274
Some(self.delay_seconds_smoothed[*index].target_value());
251-
self.delay_seconds_smoothed[*index].set_value(*value)
275+
self.delay_seconds_smoothed[*index].set_value(*delay_seconds)
252276
} else {
253277
// If we're still transitioning, queue up the desired change.
254-
self.next_delay_seconds[*index] = Some(*value);
278+
self.next_delay_seconds[*index] = Some(*delay_seconds);
255279
}
256280
}
257-
EchoNodePatch::Feedback((index, value)) => {
258-
self.feedback_smoothed[*index].set_value(value.linear())
281+
EchoNodePatch::Feedback((index, feedback)) => {
282+
self.feedback_smoothed[*index].set_value(feedback.linear())
259283
}
260-
EchoNodePatch::Crossfeed((index, value)) => {
261-
self.crossfeed_smoothed[*index].set_value(value.linear());
284+
EchoNodePatch::Crossfeed((index, crossfeed)) => {
285+
self.crossfeed_smoothed[*index].set_value(crossfeed.linear());
262286
}
263287
EchoNodePatch::Stop(_) => {
264288
clear_buffers = true;
265289
self.declicker.fade_to_enabled(false, &extra.declick_values);
266290
}
267-
EchoNodePatch::Paused(value) => {
291+
EchoNodePatch::Paused(is_paused) => {
268292
self.declicker
269-
.fade_to_enabled(!*value, &extra.declick_values);
293+
.fade_to_enabled(!*is_paused, &extra.declick_values);
270294
}
271-
EchoNodePatch::FadeCurve(value) => {
272-
self.mix_dsp.set_mix(self.params.mix, *value);
295+
EchoNodePatch::FadeCurve(fade_curve) => {
296+
self.mix_dsp.set_mix(self.params.mix, *fade_curve);
273297
}
274298
}
275299
self.params.apply(patch);
@@ -287,6 +311,17 @@ impl<const CHANNELS: usize> AudioNodeProcessor for Processor<CHANNELS> {
287311
}
288312

289313
// Process smoothed values all at the same time
314+
315+
// Smoothed cutoff values do not have to be calculated per channel.
316+
// Calculate smoothed filter values
317+
let mut scratch = extra.scratch_buffers.channels_mut::<SCRATCH_CHANNELS>();
318+
let scratch: [&mut [&mut [f32]]; 2] = scratch.split_at_mut(1).into();
319+
let lpf_smoothed = &mut scratch[LPF_SCRATCH_INDEX][0];
320+
self.feedback_lpf_smoothed.process_into_buffer(lpf_smoothed);
321+
322+
let hpf_smoothed = &mut scratch[HPF_SCRATCH_INDEX][0];
323+
self.feedback_hpf_smoothed.process_into_buffer(hpf_smoothed);
324+
290325
for channel_index in (0..CHANNELS).into_iter() {
291326
// Queue up delays if applicable
292327
if self.next_delay_seconds[channel_index].is_some() {
@@ -380,8 +415,8 @@ impl<const CHANNELS: usize> AudioNodeProcessor for Processor<CHANNELS> {
380415
// Process signal to find next samples to feed into the buffer (wet
381416
// signal)
382417
let next_buffer_samples: [f32; CHANNELS] = from_fn(|channel_index| {
383-
let lpf = &mut self.tap_lpf[channel_index];
384-
let hpf = &mut self.tap_hpf[channel_index];
418+
let lpf = &mut self.feedback_lpf[channel_index];
419+
let hpf = &mut self.feedback_hpf[channel_index];
385420

386421
let input_sample = buffers.inputs[channel_index][sample_index];
387422
let feedback_sample = delayed_samples[channel_index]
@@ -394,8 +429,25 @@ impl<const CHANNELS: usize> AudioNodeProcessor for Processor<CHANNELS> {
394429
.sum::<f32>();
395430

396431
let mut next = input_sample + feedback_sample + crossfed_sample;
397-
next = lpf.process(next, self.tap_lpf_coeff);
398-
next = hpf.process(next, self.tap_hpf_coeff);
432+
433+
// Change filter coeffs based on smoothed values
434+
let scratch = extra.scratch_buffers.channels::<SCRATCH_CHANNELS>();
435+
436+
// Filter samples through high and lowpass filter
437+
next = lpf.process(
438+
next,
439+
OnePoleIirLPFCoeff::new(
440+
scratch[LPF_SCRATCH_INDEX][sample_index],
441+
info.sample_rate_recip as f32,
442+
),
443+
);
444+
next = hpf.process(
445+
next,
446+
OnePoleIirHPFCoeff::new(
447+
scratch[HPF_SCRATCH_INDEX][sample_index],
448+
info.sample_rate_recip as f32,
449+
),
450+
);
399451
next
400452
});
401453

0 commit comments

Comments
 (0)