Skip to main content

firewheel_nodes/
volume_pan.rs

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