Skip to main content

firewheel_nodes/
convolution.rs

1use core::f32;
2use core::ops::Range;
3
4use fft_convolver::FFTConvolver;
5use firewheel_core::channel_config::NonZeroChannelCount;
6use firewheel_core::collector::ArcGc;
7use firewheel_core::event::ProcEvents;
8use firewheel_core::node::{NodeError, ProcBuffers, ProcExtra, ProcInfo};
9use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN;
10use firewheel_core::{
11    channel_config::ChannelConfig,
12    diff::{Diff, Patch},
13    dsp::{
14        declick::{DeclickFadeCurve, Declicker},
15        filter::smoothing_filter::DEFAULT_SMOOTH_SECONDS,
16        volume::{DEFAULT_MIN_AMP, Volume},
17    },
18    node::{
19        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, ProcessStatus,
20    },
21    param::smoother::{SmoothedParam, SmootherConfig},
22    sample_resource::SampleResourceF32,
23};
24
25/// Node configuration for [`ConvolutionNode`].
26#[derive(Debug, Clone, Copy, PartialEq)]
27#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
28#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct ConvolutionNodeConfig {
31    /// The number of channels in this node.
32    ///
33    /// By default this is set to [`NonZeroChannelCount::STEREO`].
34    pub channels: NonZeroChannelCount,
35
36    /// The maximum length of an impulse response in seconds this node can
37    /// hold.
38    ///
39    /// By default this is set to `4.0`.
40    pub max_impulse_length_seconds: f64,
41
42    /// Smaller blocks may reduce latency at the cost of increased CPU usage.
43    ///
44    /// By default this is set to `1024`.
45    pub partition_size: usize,
46}
47
48/// The default partition size to use with a [`ConvolutionNode`].
49///
50/// Smaller blocks may reduce latency at the cost of increased CPU usage.
51pub const DEFAULT_PARTITION_SIZE: usize = 1024;
52
53impl Default for ConvolutionNodeConfig {
54    fn default() -> Self {
55        Self {
56            channels: NonZeroChannelCount::STEREO,
57            max_impulse_length_seconds: 4.0,
58            partition_size: DEFAULT_PARTITION_SIZE,
59        }
60    }
61}
62
63/// Imparts characteristics of an impulse response to the input signal.
64///
65/// Convolution is often used to achieve reverb effects, but is more
66/// computationally expensive than algorithmic reverb.
67#[derive(Patch, Diff, Clone, PartialEq)]
68#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
69#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71pub struct ConvolutionNode {
72    /// The impulse response to use.
73    #[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
74    #[cfg_attr(feature = "serde", serde(skip))]
75    pub impulse_response: Option<ArcGc<dyn SampleResourceF32 + Send + Sync + 'static>>,
76
77    /// Pause the convolution processing.
78    ///
79    /// This prevents a tail from ringing out when you want all sound to
80    /// momentarily pause.
81    pub pause: bool,
82
83    /// The output gain.
84    ///
85    /// Defaults to -20dB to balance the volume increase likely to occur when
86    /// convolving audio. Values closer to 1.0 may be very loud.
87    pub wet_gain: Volume,
88
89    /// Adjusts the time in seconds over which parameters are smoothed for `mix`
90    /// and `wet_gain`.
91    ///
92    /// By default this is set to `0.062` (62ms). This value is chosen such that
93    /// the stair-stepping effect isn't noticeable for a typical block size of 1024
94    /// samples.
95    pub smooth_seconds: f32,
96}
97
98impl Default for ConvolutionNode {
99    fn default() -> Self {
100        Self {
101            impulse_response: None,
102            wet_gain: Volume::Decibels(-20.0),
103            pause: false,
104            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
105        }
106    }
107}
108
109impl core::fmt::Debug for ConvolutionNode {
110    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
111        let mut f = f.debug_struct("SamplerNode");
112        f.field(
113            "impulse_len_frames",
114            &self.impulse_response.as_ref().map(|i| i.len_frames()),
115        );
116        f.field("pause", &self.pause);
117        f.field("wet_gain", &self.wet_gain);
118        f.field("smooth_seconds", &self.smooth_seconds);
119        f.finish()
120    }
121}
122
123impl AudioNode for ConvolutionNode {
124    type Configuration = ConvolutionNodeConfig;
125
126    fn info(&self, config: &Self::Configuration) -> Result<AudioNodeInfo, NodeError> {
127        Ok(AudioNodeInfo::new()
128            .debug_name("convolution")
129            .channel_config(ChannelConfig::new(
130                config.channels.get(),
131                config.channels.get(),
132            )))
133        // TODO: If and when the scheduler gets proper in-place processing support, use
134        // in-place processing for this node.
135    }
136
137    fn construct_processor(
138        &self,
139        config: &Self::Configuration,
140        cx: ConstructProcessorContext,
141    ) -> Result<impl AudioNodeProcessor, NodeError> {
142        let sample_rate = cx.stream_info.sample_rate;
143        let smooth_config = SmootherConfig {
144            smooth_seconds: self.smooth_seconds,
145            ..Default::default()
146        };
147
148        let max_frames: usize =
149            (config.max_impulse_length_seconds * (sample_rate.get() as f64)).ceil() as usize;
150
151        // TODO: Ask the creator of `fft-convolver` to add a `with_capacity` method so we don't
152        // need to use this workaround.
153        let mut tmp_impulse = vec![0.0; max_frames];
154        tmp_impulse[0] = 1.0;
155
156        let mut convolver: Vec<FFTConvolver<f32>> = (0..config.channels.get().get())
157            .map(|_| {
158                let mut c = FFTConvolver::default();
159                c.init(config.partition_size, &tmp_impulse).unwrap();
160                c
161            })
162            .collect();
163
164        let did_init_first_impulse = if let Some(s) = &self.impulse_response {
165            if s.len_frames() > max_frames as u64 {
166                return Err(ImpulseTooLongError {
167                    got_len_seconds: s.len_frames() as f64 / cx.stream_info.sample_rate_recip,
168                    max_len_seconds: config.max_impulse_length_seconds,
169                }
170                .into());
171            }
172
173            if s.num_channels().get() < config.channels.get().get() as usize {
174                // Assume a mono impulse response and set it to all channels.
175                let impulse_slice = s.channel(0).unwrap();
176
177                for c in convolver.iter_mut() {
178                    c.set_response(impulse_slice).unwrap();
179                    c.reset();
180                }
181            } else {
182                for (ch_i, c) in convolver.iter_mut().enumerate() {
183                    c.set_response(s.channel(ch_i).unwrap()).unwrap();
184                    c.reset();
185                }
186            }
187
188            true
189        } else {
190            false
191        };
192
193        Ok(ConvolutionProcessor {
194            params: self.clone(),
195            gain: SmoothedParam::new(
196                self.wet_gain.amp(),
197                DEFAULT_GAIN_SPAN,
198                smooth_config,
199                sample_rate,
200            ),
201            declick: Declicker::SettledAt0,
202            convolver,
203            max_frames,
204            did_init_first_impulse,
205            has_impulse: did_init_first_impulse,
206            new_impulse_queued: false,
207        })
208    }
209}
210
211struct ConvolutionProcessor {
212    params: ConvolutionNode,
213    gain: SmoothedParam,
214    declick: Declicker,
215    convolver: Vec<FFTConvolver<f32>>,
216    max_frames: usize,
217    did_init_first_impulse: bool,
218    has_impulse: bool,
219    new_impulse_queued: bool,
220}
221
222impl AudioNodeProcessor for ConvolutionProcessor {
223    fn events(&mut self, info: &ProcInfo, events: &mut ProcEvents, extra: &mut ProcExtra) {
224        let mut got_new_impulse = false;
225
226        for patch in events.drain_patches::<ConvolutionNode>() {
227            match patch {
228                ConvolutionNodePatch::ImpulseResponse(_) => {
229                    got_new_impulse = true;
230                }
231                ConvolutionNodePatch::WetGain(gain) => {
232                    self.gain.set_value(gain.amp());
233                }
234                ConvolutionNodePatch::Pause(pause) => {
235                    if self.has_impulse {
236                        self.declick.fade_to_enabled(!pause, &extra.declick_values);
237                    }
238                }
239                ConvolutionNodePatch::SmoothSeconds(smooth_seconds) => {
240                    self.gain
241                        .set_smooth_seconds(smooth_seconds, info.sample_rate);
242                }
243            }
244
245            self.params.apply(patch);
246        }
247
248        if got_new_impulse {
249            if let Some(s) = &self.params.impulse_response {
250                let sample_len = s.len_frames();
251                if sample_len > self.max_frames as u64 {
252                    let _ = extra.logger.try_error("Impulse is too long, please increase ConvolutionNodeConfig::max_impulse_len_seconds");
253                } else {
254                    self.new_impulse_queued = true;
255                    // Fade out the previous impulse
256                    self.declick.fade_to_0(&extra.declick_values);
257                }
258            } else {
259                self.declick.fade_to_0(&extra.declick_values);
260                self.has_impulse = false;
261                self.new_impulse_queued = false;
262            }
263        }
264    }
265
266    fn bypassed(&mut self, bypassed: bool) {
267        if !bypassed {
268            self.gain.reset_to_target();
269            self.declick.reset_to_target();
270
271            for c in self.convolver.iter_mut() {
272                c.reset();
273            }
274        }
275    }
276
277    fn process(
278        &mut self,
279        info: &ProcInfo,
280        mut buffers: ProcBuffers,
281        extra: &mut ProcExtra,
282    ) -> ProcessStatus {
283        let mut frames_processed = 0;
284        let mut output_silent = true;
285
286        if self.new_impulse_queued {
287            if self.declick != Declicker::SettledAt0 {
288                // Sanity check
289                assert!(self.declick.trending_towards_zero());
290
291                // Fade out the previous impulse
292                let proc_frames = self.declick.frames_left().min(info.frames);
293
294                self.convolve_block(&mut buffers, 0..proc_frames, extra);
295
296                frames_processed = proc_frames;
297                output_silent = false;
298            }
299
300            if self.declick == Declicker::SettledAt0 {
301                // Finished fading out old impulse, replace with new one
302
303                if let Some(s) = &self.params.impulse_response {
304                    if s.num_channels().get() < self.convolver.len() {
305                        // Assume a mono impulse response and set it to all channels.
306                        let impulse_slice = s.channel(0).unwrap();
307
308                        for c in self.convolver.iter_mut() {
309                            c.set_response(impulse_slice).unwrap();
310
311                            if !self.did_init_first_impulse {
312                                c.reset();
313                            }
314                        }
315                    } else {
316                        for (ch_i, c) in self.convolver.iter_mut().enumerate() {
317                            c.set_response(s.channel(ch_i).unwrap()).unwrap();
318
319                            if !self.did_init_first_impulse {
320                                c.reset();
321                            }
322                        }
323                    }
324
325                    self.did_init_first_impulse = true;
326                    self.has_impulse = true;
327
328                    if !self.params.pause {
329                        self.declick.fade_to_1(&extra.declick_values);
330                    }
331                }
332
333                self.new_impulse_queued = false;
334            }
335        }
336
337        if self.declick != Declicker::SettledAt0 {
338            self.convolve_block(&mut buffers, frames_processed..info.frames, extra);
339            output_silent = false;
340        } else {
341            // output is silent
342
343            self.gain.reset_to_target();
344
345            if frames_processed == 0 {
346                return ProcessStatus::Bypass;
347            } else {
348                // Clear the rest to zeros.
349                for (ch_i, ch) in buffers.outputs.iter_mut().enumerate() {
350                    if !info.out_silence_mask.is_channel_silent(ch_i) {
351                        ch[frames_processed..].fill(0.0);
352                    }
353                }
354            }
355        }
356
357        if output_silent {
358            ProcessStatus::outputs_modified_with_silence_mask(info.in_silence_mask)
359        } else {
360            buffers.check_for_silence_on_outputs(DEFAULT_MIN_AMP)
361        }
362    }
363}
364
365impl ConvolutionProcessor {
366    fn convolve_block(
367        &mut self,
368        buffers: &mut ProcBuffers,
369        range: Range<usize>,
370        extra: &mut ProcExtra,
371    ) {
372        let frames = range.end - range.start;
373
374        let mut scratch_buffers = extra.scratch_buffers.all_mut();
375        let (wet_gain_buffer, wet_declick_buffer) = scratch_buffers.split_first_mut().unwrap();
376        let wet_declick_buffer = &mut wet_declick_buffer[0];
377
378        self.gain
379            .process_into_buffer(&mut wet_gain_buffer[0..frames]);
380        self.declick.process_into_gain_buffer(
381            &mut wet_declick_buffer[0..frames],
382            false,
383            &extra.declick_values,
384            DeclickFadeCurve::EqualPower3dB,
385        );
386
387        for ((conv, input), output) in self
388            .convolver
389            .iter_mut()
390            .zip(buffers.inputs.iter())
391            .zip(buffers.outputs.iter_mut())
392        {
393            conv.process(&input[range.clone()], &mut output[range.clone()])
394                .unwrap();
395
396            for ((out_s, &g1), &g2) in output[range.clone()]
397                .iter_mut()
398                .zip(wet_gain_buffer.iter())
399                .zip(wet_declick_buffer.iter())
400            {
401                *out_s *= g1 * g2;
402            }
403        }
404
405        self.gain.settle();
406    }
407}
408
409#[derive(Debug, Clone, Copy, PartialEq)]
410pub struct ImpulseTooLongError {
411    pub got_len_seconds: f64,
412    pub max_len_seconds: f64,
413}
414
415impl core::error::Error for ImpulseTooLongError {}
416
417impl core::fmt::Display for ImpulseTooLongError {
418    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
419        write!(
420            f,
421            "Impulse of length {} seconds is longer than Convolver with max length {} seconds. Please increase ConvolutionNodeConfig::max_impulse_len_seconds",
422            self.got_len_seconds, self.max_len_seconds
423        )
424    }
425}