Skip to main content

firewheel_nodes/
volume_pan.rs

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