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#[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 pub channels: NonZeroChannelCount,
35
36 pub max_impulse_length_seconds: f64,
41
42 pub partition_size: usize,
46}
47
48pub 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#[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 #[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 pub pause: bool,
82
83 pub wet_gain: Volume,
88
89 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 }
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 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 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 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 assert!(self.declick.trending_towards_zero());
290
291 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 if let Some(s) = &self.params.impulse_response {
304 if s.num_channels().get() < self.convolver.len() {
305 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 self.gain.reset_to_target();
344
345 if frames_processed == 0 {
346 return ProcessStatus::Bypass;
347 } else {
348 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}