Skip to main content

firewheel_nodes/
volume.rs

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