Skip to main content

firewheel_nodes/
mix.rs

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