Skip to main content

firewheel_nodes/
mix.rs

1use firewheel_core::node::NodeError;
2use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN;
3use firewheel_core::{
4    channel_config::{ChannelConfig, ChannelCount, NonZeroChannelCount},
5    diff::{Diff, Patch},
6    dsp::{
7        fade::FadeCurve,
8        filter::smoothing_filter::DEFAULT_SMOOTH_SECONDS,
9        mix::Mix,
10        volume::{DEFAULT_MIN_AMP, Volume},
11    },
12    event::ProcEvents,
13    mask::{MaskType, SilenceMask},
14    node::{
15        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, ProcBuffers,
16        ProcExtra, ProcInfo, ProcStreamCtx, ProcessStatus,
17    },
18    param::smoother::{SmoothedParam, SmootherConfig},
19};
20
21/// The configuration for a [`MixNode`]
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
24#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct MixNodeConfig {
27    /// The number of input channels for a single input. This will also be
28    /// the total number of output channels.
29    ///
30    /// ## Panics
31    ///
32    /// This will cause a panic if this value is greater than `32`.
33    pub channels: NonZeroChannelCount,
34}
35
36impl Default for MixNodeConfig {
37    fn default() -> Self {
38        Self {
39            channels: NonZeroChannelCount::STEREO,
40        }
41    }
42}
43
44/// A node which mixes two signals together
45///
46/// The first half of the inputs are the first signal, and the second half are the
47/// second signal.
48#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
49#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
50#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct MixNode {
53    /// The overall volume
54    ///
55    /// By default this is set to [`Volume::UNITY_GAIN`].
56    pub volume: Volume,
57
58    /// The value representing the mix between the two audio signals
59    ///
60    /// This is a normalized value in the range `[0.0, 1.0]`, where `0.0` is fully
61    /// the first signal, `1.0` is fully the second signal, and `0.5` is an equal
62    /// mix of both.
63    ///
64    /// By default this is set to [`Mix::FULLY_FIRST`].
65    pub mix: Mix,
66
67    /// The algorithm used to map the normalized mix value in the range
68    /// `[0.0, 1.0]` to the corresponding gain values for the two signals.
69    ///
70    /// By default this is set to [`FadeCurve::EqualPower3dB`].
71    pub fade_curve: FadeCurve,
72
73    /// The time in seconds of the internal smoothing filter.
74    ///
75    /// By default this is set to `0.062` (62ms). This value is chosen such that
76    /// the stair-stepping effect isn't noticeable for a typical block size of 1024
77    /// samples.
78    pub smooth_seconds: f32,
79    /// If the resulting gain (in raw amplitude, not decibels) is less
80    /// than or equal to this value, then the gain will be clamped to
81    /// `0.0` (silence).
82    ///
83    /// By default this is set to `0.00001` (-100 decibels).
84    pub min_gain: f32,
85}
86
87impl MixNode {
88    pub const fn from_volume_mix(volume: Volume, mix: Mix) -> Self {
89        Self {
90            volume,
91            mix,
92            fade_curve: FadeCurve::EqualPower3dB,
93            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
94            min_gain: DEFAULT_MIN_AMP,
95        }
96    }
97
98    pub const fn from_mix(mix: Mix) -> Self {
99        Self {
100            volume: Volume::UNITY_GAIN,
101            mix,
102            fade_curve: FadeCurve::EqualPower3dB,
103            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
104            min_gain: DEFAULT_MIN_AMP,
105        }
106    }
107
108    /// Set the given volume in a linear scale, where `0.0` is silence and
109    /// `1.0` is unity gain.
110    ///
111    /// These units are suitable for volume sliders (simply convert percent
112    /// volume to linear volume by diving the percent volume by 100).
113    pub const fn set_volume_linear(&mut self, linear: f32) {
114        self.volume = Volume::Linear(linear);
115    }
116
117    /// Set the given volume in percentage, where `0.0` is silence and
118    /// `100.0` is unity gain.
119    ///
120    /// These units are suitable for volume sliders.
121    pub const fn set_volume_percent(&mut self, percent: f32) {
122        self.volume = Volume::from_percent(percent);
123    }
124
125    /// Set the given volume in decibels, where `0.0` is unity gain and
126    /// `f32::NEG_INFINITY` is silence.
127    pub const fn set_volume_decibels(&mut self, decibels: f32) {
128        self.volume = Volume::Decibels(decibels);
129    }
130
131    pub fn compute_gains(&self, min_amp: f32) -> (f32, f32) {
132        let global_gain = self.volume.amp_clamped(min_amp);
133
134        let (mut gain_0, mut gain_1) = self.mix.compute_gains(self.fade_curve);
135
136        gain_0 *= global_gain;
137        gain_1 *= global_gain;
138
139        if gain_0 > 0.99999 && gain_0 < 1.00001 {
140            gain_0 = 1.0;
141        }
142        if gain_1 > 0.99999 && gain_1 < 1.00001 {
143            gain_1 = 1.0;
144        }
145
146        (gain_0, gain_1)
147    }
148}
149
150impl Default for MixNode {
151    fn default() -> Self {
152        Self {
153            volume: Volume::default(),
154            mix: Mix::FULLY_FIRST,
155            fade_curve: FadeCurve::default(),
156            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
157            min_gain: DEFAULT_MIN_AMP,
158        }
159    }
160}
161
162impl AudioNode for MixNode {
163    type Configuration = MixNodeConfig;
164
165    fn info(&self, config: &Self::Configuration) -> Result<AudioNodeInfo, NodeError> {
166        let num_channels = config.channels.get().get();
167
168        Ok(AudioNodeInfo::new()
169            .debug_name("mix")
170            .channel_config(ChannelConfig {
171                num_inputs: ChannelCount::new(num_channels * 2).unwrap_or_else(|| {
172                    panic!(
173                        "MixNodeConfig::channels cannot be greater than 32, got {}",
174                        num_channels
175                    )
176                }),
177                num_outputs: config.channels.get(),
178            }))
179    }
180
181    fn construct_processor(
182        &self,
183        _config: &Self::Configuration,
184        cx: ConstructProcessorContext,
185    ) -> Result<impl AudioNodeProcessor, NodeError> {
186        let min_gain = self.min_gain.max(0.0);
187
188        let (gain_0, gain_1) = self.compute_gains(self.min_gain);
189
190        Ok(Processor {
191            gain_0: SmoothedParam::new(
192                gain_0,
193                DEFAULT_GAIN_SPAN,
194                SmootherConfig {
195                    smooth_seconds: self.smooth_seconds,
196                    ..Default::default()
197                },
198                cx.stream_info.sample_rate,
199            ),
200            gain_1: SmoothedParam::new(
201                gain_1,
202                DEFAULT_GAIN_SPAN,
203                SmootherConfig {
204                    smooth_seconds: self.smooth_seconds,
205                    ..Default::default()
206                },
207                cx.stream_info.sample_rate,
208            ),
209            params: *self,
210            min_gain,
211            prev_input_settled: true,
212        })
213    }
214}
215
216struct Processor {
217    gain_0: SmoothedParam,
218    gain_1: SmoothedParam,
219
220    params: MixNode,
221
222    min_gain: f32,
223    prev_input_settled: bool,
224}
225
226impl AudioNodeProcessor for Processor {
227    fn events(&mut self, info: &ProcInfo, events: &mut ProcEvents, _extra: &mut ProcExtra) {
228        let mut updated = false;
229        for mut patch in events.drain_patches::<MixNode>() {
230            match &mut patch {
231                MixNodePatch::Mix(m) => {
232                    if m.get() <= 0.00001 {
233                        *m = Mix::new(0.0);
234                    } else if m.get() >= 0.99999 {
235                        *m = Mix::new(1.0);
236                    }
237                }
238                MixNodePatch::SmoothSeconds(seconds) => {
239                    self.gain_0.set_smooth_seconds(*seconds, info.sample_rate);
240                    self.gain_1.set_smooth_seconds(*seconds, info.sample_rate);
241                }
242                MixNodePatch::MinGain(min_gain) => {
243                    self.min_gain = (*min_gain).max(0.0);
244                }
245                _ => {}
246            }
247
248            self.params.apply(patch);
249            updated = true;
250        }
251
252        if updated {
253            let (gain_0, gain_1) = self.params.compute_gains(self.min_gain);
254            self.gain_0.set_value(gain_0);
255            self.gain_1.set_value(gain_1);
256
257            if self.prev_input_settled {
258                // The previous block's input settled at zero, so no need to smooth.
259                self.gain_0.reset_to_target();
260                self.gain_1.reset_to_target();
261            }
262        }
263    }
264
265    fn bypassed(&mut self, _bypassed: bool) {
266        self.gain_0.reset_to_target();
267        self.gain_1.reset_to_target();
268    }
269
270    fn process(
271        &mut self,
272        info: &ProcInfo,
273        buffers: ProcBuffers,
274        extra: &mut ProcExtra,
275    ) -> ProcessStatus {
276        let channels = buffers.outputs.len();
277
278        let gain_0_silent = self.gain_0.has_settled_at_or_below(self.min_gain);
279        let gain_1_silent = self.gain_1.has_settled_at_or_below(self.min_gain);
280        let has_settled = self.gain_0.has_settled() && self.gain_1.has_settled();
281
282        if (gain_0_silent && gain_1_silent)
283            || info
284                .in_silence_mask
285                .all_channels_silent(buffers.inputs.len())
286        {
287            self.gain_0.reset_to_target();
288            self.gain_1.reset_to_target();
289            self.prev_input_settled = true;
290
291            return ProcessStatus::ClearAllOutputs;
292        }
293
294        self.prev_input_settled = buffers.inputs_settled_at_zero();
295
296        let mut out_silence_mask = SilenceMask::NONE_SILENT;
297
298        if has_settled {
299            if self.params.mix.get() == 0.0 && self.gain_0.target_value() == 1.0 {
300                // Simply copy input 0 to output
301                for (ch_i, (in_ch, out_ch)) in buffers.inputs[..channels]
302                    .iter()
303                    .zip(buffers.outputs.iter_mut())
304                    .enumerate()
305                {
306                    if info.in_silence_mask.is_channel_silent(ch_i) {
307                        out_silence_mask.set_channel(ch_i, true);
308
309                        if !info.out_silence_mask.is_channel_silent(ch_i) {
310                            out_ch.fill(0.0);
311                        }
312                    } else {
313                        out_ch.copy_from_slice(in_ch);
314                    }
315                }
316
317                return ProcessStatus::OutputsModifiedWithMask(MaskType::Silence(out_silence_mask));
318            } else if self.params.mix.get() == 1.0 && self.gain_1.target_value() == 1.0 {
319                // Simply copy input 1 to output
320                for (ch_i, (in_ch, out_ch)) in buffers.inputs[channels..]
321                    .iter()
322                    .zip(buffers.outputs.iter_mut())
323                    .enumerate()
324                {
325                    if info.in_silence_mask.is_channel_silent(channels + ch_i) {
326                        out_silence_mask.set_channel(ch_i, true);
327
328                        if !info.out_silence_mask.is_channel_silent(ch_i) {
329                            out_ch.fill(0.0);
330                        }
331                    } else {
332                        out_ch.copy_from_slice(in_ch);
333                    }
334                }
335
336                return ProcessStatus::OutputsModifiedWithMask(MaskType::Silence(out_silence_mask));
337            }
338        }
339
340        match channels {
341            1 => {
342                // Provide an optimized loop for mono
343
344                if has_settled {
345                    for ((&in0_s, &in1_s), out_s) in buffers.inputs[0]
346                        .iter()
347                        .zip(buffers.inputs[1].iter())
348                        .zip(buffers.outputs[0].iter_mut())
349                    {
350                        *out_s = (in0_s * self.gain_0.target_value())
351                            + (in1_s * self.gain_1.target_value());
352                    }
353                } else {
354                    for ((&in0_s, &in1_s), out_s) in buffers.inputs[0]
355                        .iter()
356                        .zip(buffers.inputs[1].iter())
357                        .zip(buffers.outputs[0].iter_mut())
358                    {
359                        let gain_0 = self.gain_0.next_smoothed();
360                        let gain_1 = self.gain_1.next_smoothed();
361
362                        *out_s = (in0_s * gain_0) + (in1_s * gain_1);
363                    }
364
365                    self.gain_0.settle();
366                    self.gain_1.settle();
367                }
368            }
369            2 => {
370                // Provide an optimized loop for stereo
371
372                let in0_l = &buffers.inputs[0][..info.frames];
373                let in0_r = &buffers.inputs[1][..info.frames];
374                let in1_l = &buffers.inputs[2][..info.frames];
375                let in1_r = &buffers.inputs[3][..info.frames];
376
377                let (out_l, out_r) = buffers.outputs.split_first_mut().unwrap();
378                let out_l = &mut out_l[..info.frames];
379                let out_r = &mut out_r[0][..info.frames];
380
381                if has_settled {
382                    for i in 0..info.frames {
383                        out_l[i] = (in0_l[i] * self.gain_0.target_value())
384                            + (in1_l[i] * self.gain_1.target_value());
385                        out_r[i] = (in0_r[i] * self.gain_0.target_value())
386                            + (in1_r[i] * self.gain_1.target_value());
387                    }
388                } else {
389                    for i in 0..info.frames {
390                        let gain_0 = self.gain_0.next_smoothed();
391                        let gain_1 = self.gain_1.next_smoothed();
392
393                        out_l[i] = (in0_l[i] * gain_0) + (in1_l[i] * gain_1);
394                        out_r[i] = (in0_r[i] * gain_0) + (in1_r[i] * gain_1);
395                    }
396
397                    self.gain_0.settle();
398                    self.gain_1.settle();
399                }
400            }
401            _ => {
402                if has_settled {
403                    for (ch_i, ((in0_ch, in1_ch), out_ch)) in buffers.inputs[0..channels]
404                        .iter()
405                        .zip(buffers.inputs[channels..].iter())
406                        .zip(buffers.outputs.iter_mut())
407                        .enumerate()
408                    {
409                        let in0_ch_silent = info.in_silence_mask.is_channel_silent(ch_i);
410                        let in1_ch_silent = info.in_silence_mask.is_channel_silent(channels + ch_i);
411
412                        // For some reason clippy doesn't see the third "or" expression and thinks
413                        // this can be simplified.
414                        #[allow(clippy::nonminimal_bool)]
415                        let channel_silent = (in0_ch_silent && in1_ch_silent)
416                            || (gain_0_silent && in1_ch_silent)
417                            || (gain_1_silent && in0_ch_silent);
418
419                        if channel_silent {
420                            out_silence_mask.set_channel(ch_i, true);
421
422                            if !info.out_silence_mask.is_channel_silent(ch_i) {
423                                out_ch.fill(0.0);
424                            }
425                        } else {
426                            for ((&in0_s, &in1_s), out_s) in
427                                in0_ch.iter().zip(in1_ch.iter()).zip(out_ch.iter_mut())
428                            {
429                                *out_s = (in0_s * self.gain_0.target_value())
430                                    + (in1_s * self.gain_1.target_value());
431                            }
432                        }
433                    }
434                } else {
435                    let [gain_0_buf, gain_1_buf] = extra.scratch_buffers.channels_mut::<2>();
436                    self.gain_0
437                        .process_into_buffer(&mut gain_0_buf[..info.frames]);
438                    self.gain_1
439                        .process_into_buffer(&mut gain_1_buf[..info.frames]);
440
441                    for (ch_i, ((in0_ch, in1_ch), out_ch)) in buffers.inputs[0..channels]
442                        .iter()
443                        .zip(buffers.inputs[channels..].iter())
444                        .zip(buffers.outputs.iter_mut())
445                        .enumerate()
446                    {
447                        let in0_ch_silent = info.in_silence_mask.is_channel_silent(ch_i);
448                        let in1_ch_silent = info.in_silence_mask.is_channel_silent(channels + ch_i);
449
450                        // For some reason clippy doesn't see the third "or" expression and thinks
451                        // this can be simplified.
452                        #[allow(clippy::nonminimal_bool)]
453                        let channel_silent = (in0_ch_silent && in1_ch_silent)
454                            || (gain_0_silent && in1_ch_silent)
455                            || (gain_1_silent && in0_ch_silent);
456
457                        if channel_silent {
458                            out_silence_mask.set_channel(ch_i, true);
459
460                            if !info.out_silence_mask.is_channel_silent(ch_i) {
461                                out_ch.fill(0.0);
462                            }
463                        } else {
464                            for ((((&in0_s, &in1_s), &gain0_s), &gain1_s), out_s) in in0_ch
465                                .iter()
466                                .zip(in1_ch.iter())
467                                .zip(gain_0_buf.iter())
468                                .zip(gain_1_buf.iter())
469                                .zip(out_ch.iter_mut())
470                            {
471                                *out_s = (in0_s * gain0_s) + (in1_s * gain1_s);
472                            }
473                        }
474                    }
475
476                    self.gain_0.settle();
477                    self.gain_1.settle();
478                }
479            }
480        }
481
482        ProcessStatus::OutputsModifiedWithMask(MaskType::Silence(out_silence_mask))
483    }
484
485    fn new_stream(
486        &mut self,
487        stream_info: &firewheel_core::StreamInfo,
488        _context: &mut ProcStreamCtx,
489    ) {
490        self.gain_0.update_sample_rate(stream_info.sample_rate);
491        self.gain_1.update_sample_rate(stream_info.sample_rate);
492    }
493}