firewheel_nodes/
volume.rs

1use firewheel_core::{
2    channel_config::{ChannelConfig, NonZeroChannelCount},
3    diff::{Diff, Patch},
4    dsp::{
5        filter::smoothing_filter::DEFAULT_SMOOTH_SECONDS,
6        volume::{Volume, DEFAULT_AMP_EPSILON},
7    },
8    event::ProcEvents,
9    mask::MaskType,
10    node::{
11        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, ProcBuffers,
12        ProcExtra, ProcInfo, ProcStreamCtx, ProcessStatus,
13    },
14    param::smoother::{SmoothedParam, SmootherConfig},
15};
16
17/// The configuration of a [`VolumeNode`]
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
20#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct VolumeNodeConfig {
23    /// The number of input and output channels.
24    pub channels: NonZeroChannelCount,
25}
26
27impl Default for VolumeNodeConfig {
28    fn default() -> Self {
29        Self {
30            channels: NonZeroChannelCount::STEREO,
31        }
32    }
33}
34
35/// A node that changes the volume of a signal
36#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
37#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
38#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct VolumeNode {
41    /// The volume to apply to the signal
42    pub volume: Volume,
43
44    /// The time in seconds of the internal smoothing filter.
45    ///
46    /// By default this is set to `0.015` (15ms).
47    pub smooth_seconds: f32,
48    /// If the resutling gain (in raw amplitude, not decibels) is less
49    /// than or equal to this value, then the gain will be clamped to
50    /// `0.0` (silence).
51    ///
52    /// By default this is set to `0.00001` (-100 decibels).
53    pub min_gain: f32,
54}
55
56impl Default for VolumeNode {
57    fn default() -> Self {
58        Self {
59            volume: Volume::default(),
60            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
61            min_gain: DEFAULT_AMP_EPSILON,
62        }
63    }
64}
65
66impl VolumeNode {
67    /// Construct a volume node from the given volume in a linear scale,
68    /// where `0.0` is silence and `1.0` is unity gain.
69    ///
70    /// These units are suitable for volume sliders (simply convert percent
71    /// volume to linear volume by diving the percent volume by 100).
72    pub const fn from_linear(linear: f32) -> Self {
73        Self {
74            volume: Volume::Linear(linear),
75            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
76            min_gain: DEFAULT_AMP_EPSILON,
77        }
78    }
79
80    /// Construct a volume node from the given volume in percentage,
81    /// where `0.0` is silence and `100.0` is unity gain.
82    ///
83    /// These units are suitable for volume sliders.
84    pub const fn from_percent(percent: f32) -> Self {
85        Self {
86            volume: Volume::from_percent(percent),
87            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
88            min_gain: DEFAULT_AMP_EPSILON,
89        }
90    }
91
92    /// Construct a volume node from the given volume in decibels, where `0.0`
93    /// is unity gain and `f32::NEG_INFINITY` is silence.
94    pub const fn from_decibels(decibels: f32) -> Self {
95        Self {
96            volume: Volume::Decibels(decibels),
97            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
98            min_gain: DEFAULT_AMP_EPSILON,
99        }
100    }
101
102    /// Set the given volume in a linear scale, where `0.0` is silence and
103    /// `1.0` is unity gain.
104    ///
105    /// These units are suitable for volume sliders (simply convert percent
106    /// volume to linear volume by diving the percent volume by 100).
107    pub const fn set_linear(&mut self, linear: f32) {
108        self.volume = Volume::Linear(linear);
109    }
110
111    /// Set the given volume in percentage, where `0.0` is silence and
112    /// `100.0` is unity gain.
113    ///
114    /// These units are suitable for volume sliders.
115    pub const fn set_percent(&mut self, percent: f32) {
116        self.volume = Volume::from_percent(percent);
117    }
118
119    /// Set the given volume in decibels, where `0.0` is unity gain and
120    /// `f32::NEG_INFINITY` is silence.
121    pub const fn set_decibels(&mut self, decibels: f32) {
122        self.volume = Volume::Decibels(decibels);
123    }
124}
125
126impl AudioNode for VolumeNode {
127    type Configuration = VolumeNodeConfig;
128
129    fn info(&self, config: &Self::Configuration) -> AudioNodeInfo {
130        AudioNodeInfo::new()
131            .debug_name("volume")
132            .channel_config(ChannelConfig {
133                num_inputs: config.channels.get(),
134                num_outputs: config.channels.get(),
135            })
136    }
137
138    fn construct_processor(
139        &self,
140        _config: &Self::Configuration,
141        cx: ConstructProcessorContext,
142    ) -> impl AudioNodeProcessor {
143        let min_gain = self.min_gain.max(0.0);
144        let gain = self.volume.amp_clamped(min_gain);
145
146        VolumeProcessor {
147            gain: SmoothedParam::new(
148                gain,
149                SmootherConfig {
150                    smooth_seconds: self.smooth_seconds,
151                    ..Default::default()
152                },
153                cx.stream_info.sample_rate,
154            ),
155            min_gain,
156        }
157    }
158}
159
160struct VolumeProcessor {
161    gain: SmoothedParam,
162
163    min_gain: f32,
164}
165
166impl AudioNodeProcessor for VolumeProcessor {
167    fn process(
168        &mut self,
169        info: &ProcInfo,
170        buffers: ProcBuffers,
171        events: &mut ProcEvents,
172        extra: &mut ProcExtra,
173    ) -> ProcessStatus {
174        for patch in events.drain_patches::<VolumeNode>() {
175            match patch {
176                VolumeNodePatch::Volume(v) => {
177                    let mut gain = v.amp_clamped(self.min_gain);
178                    if gain > 0.99999 && gain < 1.00001 {
179                        gain = 1.0;
180                    }
181                    self.gain.set_value(gain);
182
183                    if info.prev_output_was_silent {
184                        // Previous block was silent, so no need to smooth.
185                        self.gain.reset_to_target();
186                    }
187                }
188                VolumeNodePatch::SmoothSeconds(seconds) => {
189                    self.gain.set_smooth_seconds(seconds, info.sample_rate);
190                }
191                VolumeNodePatch::MinGain(min_gain) => {
192                    self.min_gain = min_gain.max(0.0);
193                }
194            }
195        }
196
197        if info
198            .in_silence_mask
199            .all_channels_silent(buffers.inputs.len())
200        {
201            // All channels are silent, so there is no need to process. Also reset
202            // the filter since it doesn't need to smooth anything.
203            self.gain.reset_to_target();
204
205            return ProcessStatus::ClearAllOutputs;
206        }
207
208        if self.gain.has_settled() {
209            if self.gain.target_value() <= self.min_gain {
210                // Muted, so there is no need to process.
211                return ProcessStatus::ClearAllOutputs;
212            } else if self.gain.target_value() == 1.0 {
213                // Unity gain, there is no need to process.
214                return ProcessStatus::Bypass;
215            } else {
216                for (ch_i, (out_ch, in_ch)) in buffers
217                    .outputs
218                    .iter_mut()
219                    .zip(buffers.inputs.iter())
220                    .enumerate()
221                {
222                    if info.in_silence_mask.is_channel_silent(ch_i) {
223                        if !info.out_silence_mask.is_channel_silent(ch_i) {
224                            out_ch.fill(0.0);
225                        }
226                    } else {
227                        for (os, &is) in out_ch.iter_mut().zip(in_ch.iter()) {
228                            *os = is * self.gain.target_value();
229                        }
230                    }
231                }
232
233                return ProcessStatus::OutputsModifiedWithMask(MaskType::Silence(
234                    info.in_silence_mask,
235                ));
236            }
237        }
238
239        if buffers.inputs.len() == 1 {
240            // Provide an optimized loop for mono.
241            for (os, &is) in buffers.outputs[0].iter_mut().zip(buffers.inputs[0].iter()) {
242                *os = is * self.gain.next_smoothed();
243            }
244        } else if buffers.inputs.len() == 2 {
245            // Provide an optimized loop for stereo.
246
247            let in0 = &buffers.inputs[0][..info.frames];
248            let in1 = &buffers.inputs[1][..info.frames];
249            let (out0, out1) = buffers.outputs.split_first_mut().unwrap();
250            let out0 = &mut out0[..info.frames];
251            let out1 = &mut out1[0][..info.frames];
252
253            for i in 0..info.frames {
254                let gain = self.gain.next_smoothed();
255
256                out0[i] = in0[i] * gain;
257                out1[i] = in1[i] * gain;
258            }
259        } else {
260            let scratch_buffer = extra.scratch_buffers.first_mut();
261
262            self.gain
263                .process_into_buffer(&mut scratch_buffer[..info.frames]);
264
265            for (ch_i, (out_ch, in_ch)) in buffers
266                .outputs
267                .iter_mut()
268                .zip(buffers.inputs.iter())
269                .enumerate()
270            {
271                if info.in_silence_mask.is_channel_silent(ch_i) {
272                    if !info.out_silence_mask.is_channel_silent(ch_i) {
273                        out_ch.fill(0.0);
274                    }
275                    continue;
276                }
277
278                for ((os, &is), &g) in out_ch
279                    .iter_mut()
280                    .zip(in_ch.iter())
281                    .zip(scratch_buffer[..info.frames].iter())
282                {
283                    *os = is * g;
284                }
285            }
286        }
287
288        self.gain.settle();
289
290        ProcessStatus::OutputsModified
291    }
292
293    fn new_stream(
294        &mut self,
295        stream_info: &firewheel_core::StreamInfo,
296        _context: &mut ProcStreamCtx,
297    ) {
298        self.gain.update_sample_rate(stream_info.sample_rate);
299    }
300}