firewheel_core/dsp/
declick.rs

1#[cfg(not(feature = "std"))]
2use num_traits::Float;
3
4use core::f32::consts::FRAC_PI_2;
5use core::{num::NonZeroU32, ops::Range};
6
7#[cfg(not(feature = "std"))]
8use bevy_platform::prelude::Vec;
9
10use crate::dsp::filter::smoothing_filter::{SmoothingFilter, SmoothingFilterCoeff};
11
12/// A struct used to declick audio signals using crossfading.
13///
14/// This approach is more SIMD-friendly than using a smoothing filter
15/// or incrementing a gain value per-sample.
16///
17/// Used in conjunction with [`DeclickValues`].
18#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Declicker {
20    SettledAt0,
21    #[default]
22    SettledAt1,
23    FadingTo0 {
24        frames_left: usize,
25    },
26    FadingTo1 {
27        frames_left: usize,
28    },
29}
30
31impl Declicker {
32    pub fn from_enabled(enabled: bool) -> Self {
33        if enabled {
34            Self::SettledAt1
35        } else {
36            Self::SettledAt0
37        }
38    }
39
40    pub fn has_settled(&self) -> bool {
41        *self == Self::SettledAt0 || *self == Self::SettledAt1
42    }
43
44    pub fn disabled(&self) -> bool {
45        *self == Self::SettledAt0
46    }
47
48    pub fn fade_to_enabled(&mut self, enabled: bool, declick_values: &DeclickValues) {
49        if enabled {
50            self.fade_to_1(declick_values);
51        } else {
52            self.fade_to_0(declick_values);
53        }
54    }
55
56    pub fn fade_to_0(&mut self, declick_values: &DeclickValues) {
57        match self {
58            Self::SettledAt1 => {
59                *self = Self::FadingTo0 {
60                    frames_left: declick_values.frames(),
61                }
62            }
63            Self::FadingTo1 { frames_left } => {
64                let frames_left = if *frames_left <= declick_values.frames() {
65                    declick_values.frames() - *frames_left
66                } else {
67                    declick_values.frames()
68                };
69
70                *self = Self::FadingTo0 { frames_left }
71            }
72            _ => {}
73        }
74    }
75
76    pub fn fade_to_1(&mut self, declick_values: &DeclickValues) {
77        match self {
78            Self::SettledAt0 => {
79                *self = Self::FadingTo1 {
80                    frames_left: declick_values.frames(),
81                }
82            }
83            Self::FadingTo0 { frames_left } => {
84                let frames_left = if *frames_left <= declick_values.frames() {
85                    declick_values.frames() - *frames_left
86                } else {
87                    declick_values.frames()
88                };
89
90                *self = Self::FadingTo1 { frames_left }
91            }
92            _ => {}
93        }
94    }
95
96    /// Reset to the current target value.
97    pub fn reset_to_target(&mut self) {
98        *self = match &self {
99            Self::FadingTo0 { .. } => Self::SettledAt0,
100            Self::FadingTo1 { .. } => Self::SettledAt1,
101            s => **s,
102        }
103    }
104
105    pub fn reset_to_0(&mut self) {
106        *self = Self::SettledAt0;
107    }
108
109    pub fn reset_to_1(&mut self) {
110        *self = Self::SettledAt1;
111    }
112
113    /// Crossfade between the two buffers, where `DeclickValues::SettledAt0` is fully
114    /// `buffers_a`, and `DeclickValues::SettledAt1` is fully `buffers_b`.
115    pub fn process_crossfade<VA: AsRef<[f32]>, VB: AsMut<[f32]>>(
116        &mut self,
117        buffers_a: &[VA],
118        buffers_b: &mut [VB],
119        frames: usize,
120        declick_values: &DeclickValues,
121        fade_curve: DeclickFadeCurve,
122    ) {
123        let mut crossfade_buffers =
124            |declick_frames_left: &mut usize, values_a: &[f32], values_b: &[f32]| -> usize {
125                let process_frames = frames.min(*declick_frames_left);
126
127                let values_start = values_a.len() - *declick_frames_left;
128                let values_a = &values_a[values_start..values_start + process_frames];
129                let values_b = &values_b[values_start..values_start + process_frames];
130
131                for (ch_a, ch_b) in buffers_a.iter().zip(buffers_b.iter_mut()) {
132                    let slice_a = &ch_a.as_ref()[..process_frames];
133                    let slice_b = &mut ch_b.as_mut()[..process_frames];
134
135                    for i in 0..process_frames {
136                        slice_b[i] = (slice_a[i] * values_a[i]) + (slice_b[i] * values_b[i]);
137                    }
138                }
139
140                *declick_frames_left -= process_frames;
141
142                process_frames
143            };
144
145        match self {
146            Self::SettledAt0 => {
147                for (ch_a, ch_b) in buffers_a.iter().zip(buffers_b.iter_mut()) {
148                    let slice_a = &ch_a.as_ref()[..frames];
149                    let slice_b = &mut ch_b.as_mut()[..frames];
150
151                    slice_b.copy_from_slice(slice_a);
152                }
153            }
154            Self::FadingTo0 { frames_left } => {
155                let (values_a, values_b) = match fade_curve {
156                    DeclickFadeCurve::Linear => (
157                        &declick_values.linear_0_to_1_values,
158                        &declick_values.linear_1_to_0_values,
159                    ),
160                    DeclickFadeCurve::EqualPower3dB => (
161                        &declick_values.circular_0_to_1_values,
162                        &declick_values.circular_1_to_0_values,
163                    ),
164                };
165
166                let frames_processed = crossfade_buffers(frames_left, values_a, &values_b);
167
168                if frames_processed < frames {
169                    for (ch_a, ch_b) in buffers_a.iter().zip(buffers_b.iter_mut()) {
170                        let slice_a = &ch_a.as_ref()[frames_processed..frames];
171                        let slice_b = &mut ch_b.as_mut()[frames_processed..frames];
172
173                        slice_b.copy_from_slice(slice_a);
174                    }
175                }
176
177                if *frames_left == 0 {
178                    *self = Self::SettledAt0;
179                }
180            }
181            Self::FadingTo1 { frames_left } => {
182                let (values_a, values_b) = match fade_curve {
183                    DeclickFadeCurve::Linear => (
184                        &declick_values.linear_1_to_0_values,
185                        &declick_values.linear_0_to_1_values,
186                    ),
187                    DeclickFadeCurve::EqualPower3dB => (
188                        &declick_values.circular_1_to_0_values,
189                        &declick_values.circular_0_to_1_values,
190                    ),
191                };
192
193                crossfade_buffers(frames_left, values_a, values_b);
194
195                if *frames_left == 0 {
196                    *self = Self::SettledAt1;
197                }
198            }
199            _ => {}
200        }
201    }
202
203    pub fn process<V: AsMut<[f32]>>(
204        &mut self,
205        buffers: &mut [V],
206        range_in_buffer: Range<usize>,
207        declick_values: &DeclickValues,
208        gain: f32,
209        fade_curve: DeclickFadeCurve,
210    ) {
211        let mut fade_buffers = |declick_frames_left: &mut usize, values: &[f32]| -> usize {
212            let buffer_frames = range_in_buffer.end - range_in_buffer.start;
213            let process_frames = buffer_frames.min(*declick_frames_left);
214            let start_frame = values.len() - *declick_frames_left;
215
216            if gain == 1.0 {
217                for b in buffers.iter_mut() {
218                    let b = &mut b.as_mut()
219                        [range_in_buffer.start..range_in_buffer.start + process_frames];
220
221                    for (s, &g) in b
222                        .iter_mut()
223                        .zip(values[start_frame..start_frame + process_frames].iter())
224                    {
225                        *s *= g;
226                    }
227                }
228            } else {
229                for b in buffers.iter_mut() {
230                    let b = &mut b.as_mut()
231                        [range_in_buffer.start..range_in_buffer.start + process_frames];
232
233                    for (s, &g) in b
234                        .iter_mut()
235                        .zip(values[start_frame..start_frame + process_frames].iter())
236                    {
237                        *s *= g * gain;
238                    }
239                }
240            }
241
242            *declick_frames_left -= process_frames;
243
244            process_frames
245        };
246
247        match self {
248            Self::SettledAt0 => {
249                for b in buffers.iter_mut() {
250                    let b = &mut b.as_mut();
251                    b[range_in_buffer.clone()].fill(0.0);
252                }
253            }
254            Self::FadingTo0 { frames_left } => {
255                let values = match fade_curve {
256                    DeclickFadeCurve::Linear => &declick_values.linear_1_to_0_values,
257                    DeclickFadeCurve::EqualPower3dB => &declick_values.circular_1_to_0_values,
258                };
259
260                let frames_processed = fade_buffers(frames_left, values);
261
262                if frames_processed < range_in_buffer.end - range_in_buffer.start {
263                    for b in buffers.iter_mut() {
264                        let b = &mut b.as_mut()
265                            [range_in_buffer.start + frames_processed..range_in_buffer.end];
266                        b.fill(0.0);
267                    }
268                }
269
270                if *frames_left == 0 {
271                    *self = Self::SettledAt0;
272                }
273            }
274            Self::FadingTo1 { frames_left } => {
275                let values = match fade_curve {
276                    DeclickFadeCurve::Linear => &declick_values.linear_0_to_1_values,
277                    DeclickFadeCurve::EqualPower3dB => &declick_values.circular_0_to_1_values,
278                };
279
280                let frames_processed = fade_buffers(frames_left, values);
281
282                if frames_processed < range_in_buffer.end - range_in_buffer.start && gain != 1.0 {
283                    for b in buffers.iter_mut() {
284                        let b = &mut b.as_mut()
285                            [range_in_buffer.start + frames_processed..range_in_buffer.end];
286                        for s in b.iter_mut() {
287                            *s *= gain;
288                        }
289                    }
290                }
291
292                if *frames_left == 0 {
293                    *self = Self::SettledAt1;
294                }
295            }
296            _ => {}
297        }
298    }
299
300    pub fn trending_towards_zero(&self) -> bool {
301        match self {
302            Declicker::SettledAt0 | Declicker::FadingTo0 { .. } => true,
303            _ => false,
304        }
305    }
306
307    pub fn trending_towards_one(&self) -> bool {
308        match self {
309            Declicker::SettledAt1 | Declicker::FadingTo1 { .. } => true,
310            _ => false,
311        }
312    }
313
314    pub fn frames_left(&self) -> usize {
315        match *self {
316            Declicker::FadingTo0 { frames_left } => frames_left,
317            Declicker::FadingTo1 { frames_left } => frames_left,
318            _ => 0,
319        }
320    }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
325#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
326pub enum DeclickFadeCurve {
327    /// Linear fade.
328    Linear,
329    /// Equal power fade (circular).
330    EqualPower3dB,
331}
332
333/// A buffer of values that linearly ramp up/down between `0.0` and `1.0`.
334///
335/// This approach is more SIMD-friendly than using a smoothing filter or
336/// incrementing the gain per-sample.
337pub struct DeclickValues {
338    pub linear_0_to_1_values: Vec<f32>,
339    pub linear_1_to_0_values: Vec<f32>,
340    pub circular_0_to_1_values: Vec<f32>,
341    pub circular_1_to_0_values: Vec<f32>,
342}
343
344impl DeclickValues {
345    pub const DEFAULT_FADE_SECONDS: f32 = 10.0 / 1_000.0;
346
347    pub fn new(frames: NonZeroU32) -> Self {
348        let frames = frames.get() as usize;
349        let frames_recip = (frames as f32).recip();
350
351        let mut linear_0_to_1_values = Vec::new();
352        let mut linear_1_to_0_values = Vec::new();
353        let mut circular_0_to_1_values = Vec::new();
354        let mut circular_1_to_0_values = Vec::new();
355
356        linear_0_to_1_values.reserve_exact(frames);
357        linear_1_to_0_values.reserve_exact(frames);
358        circular_0_to_1_values.reserve_exact(frames);
359        circular_1_to_0_values.reserve_exact(frames);
360
361        linear_0_to_1_values = (0..frames).map(|i| i as f32 * frames_recip).collect();
362        linear_1_to_0_values = (0..frames).rev().map(|i| i as f32 * frames_recip).collect();
363
364        circular_0_to_1_values = linear_0_to_1_values
365            .iter()
366            .map(|x| (x * FRAC_PI_2).sin())
367            .collect();
368        circular_1_to_0_values = circular_0_to_1_values.iter().rev().copied().collect();
369
370        Self {
371            linear_0_to_1_values,
372            linear_1_to_0_values,
373            circular_0_to_1_values,
374            circular_1_to_0_values,
375        }
376    }
377
378    pub fn frames(&self) -> usize {
379        self.linear_0_to_1_values.len()
380    }
381}
382
383/// A struct used to declick audio signals using a lowpass filter.
384///
385/// Note, this method of declicking does not have as good quality or
386/// performance as the crossfading method used by [`Declicker`]. But
387/// this can be used in situations where crossfading two signals is
388/// infeasible or too expensive.
389#[derive(Debug)]
390pub struct LowpassDeclicker<const MAX_CHANNELS: usize> {
391    filters: [SmoothingFilter; MAX_CHANNELS],
392    coeff: SmoothingFilterCoeff,
393    smooth_secs: f32,
394    smooth_frames: usize,
395    smooth_frames_recip: f32,
396    frames_left: usize,
397}
398
399impl<const MAX_CHANNELS: usize> LowpassDeclicker<MAX_CHANNELS> {
400    pub fn new(sample_rate: NonZeroU32, smooth_secs: f32) -> Self {
401        let smooth_frames = ((smooth_secs * sample_rate.get() as f32).round() as usize).max(1);
402        let smooth_frames_recip = (smooth_frames as f32).recip();
403
404        Self {
405            filters: [SmoothingFilter::new(0.0); MAX_CHANNELS],
406            coeff: SmoothingFilterCoeff::new(sample_rate, smooth_secs),
407            smooth_secs,
408            smooth_frames,
409            smooth_frames_recip,
410            frames_left: 0,
411        }
412    }
413
414    pub fn is_declicking(&self) -> bool {
415        self.frames_left > 0
416    }
417
418    pub fn update_sample_rate(&mut self, sample_rate: NonZeroU32) {
419        self.coeff = SmoothingFilterCoeff::new(sample_rate, self.smooth_secs);
420        self.smooth_frames =
421            ((self.smooth_secs * sample_rate.get() as f32).round() as usize).max(1);
422        self.smooth_frames_recip = (self.smooth_frames as f32).recip();
423    }
424
425    pub fn begin(&mut self) {
426        self.frames_left = self.smooth_frames;
427    }
428
429    pub fn reset(&mut self) {
430        self.frames_left = 0;
431    }
432
433    pub fn process<V: AsMut<[f32]>>(&mut self, buffers: &mut [V], frames: usize) {
434        if frames == 0 {
435            return;
436        }
437
438        if self.frames_left == 0 {
439            for (buf, f) in buffers.iter_mut().zip(self.filters.iter_mut()) {
440                f.z1 = buf.as_mut()[frames - 1];
441            }
442
443            return;
444        }
445
446        let proc_frames = self.frames_left.min(frames);
447
448        if buffers.len().min(MAX_CHANNELS) == 2 {
449            // Provide an optimized loop for stereo.
450
451            let (buf_l, buf_r) = buffers.split_first_mut().unwrap();
452            let buf_l = &mut buf_l.as_mut()[..proc_frames];
453            let buf_r = &mut buf_r[0].as_mut()[..proc_frames];
454
455            let (f_l, f_r) = self.filters.split_first_mut().unwrap();
456            let f_r = &mut f_r[0];
457
458            for i in 0..proc_frames {
459                let filtered_l = f_l.process(buf_l[i], self.coeff);
460                let filtered_r = f_r.process(buf_r[i], self.coeff);
461
462                let filtered_mix = (self.frames_left - i) as f32 * self.smooth_frames_recip;
463
464                buf_l[i] = (filtered_l * filtered_mix) + (buf_l[i] * (1.0 - filtered_mix));
465                buf_r[i] = (filtered_r * filtered_mix) + (buf_r[i] * (1.0 - filtered_mix));
466            }
467        } else {
468            for (buf, f) in buffers.iter_mut().zip(self.filters.iter_mut()) {
469                for (i, s) in buf.as_mut()[..proc_frames].iter_mut().enumerate() {
470                    let filtered_s = f.process(*s, self.coeff);
471
472                    let filtered_mix = (self.frames_left - i) as f32 * self.smooth_frames_recip;
473
474                    *s = (filtered_s * filtered_mix) + (*s * (1.0 - filtered_mix));
475                }
476            }
477        }
478
479        self.frames_left -= proc_frames;
480    }
481}