web_audio_api/node/
gain.rs

1use crate::context::{AudioContextRegistration, AudioParamId, BaseAudioContext};
2use crate::param::{AudioParam, AudioParamDescriptor};
3use crate::render::{
4    AudioParamValues, AudioProcessor, AudioRenderQuantum, AudioWorkletGlobalScope,
5};
6
7use super::{AudioNode, AudioNodeOptions, ChannelConfig};
8
9/// Options for constructing a [`GainNode`]
10// dictionary GainOptions : AudioNodeOptions {
11//   float gain = 1.0;
12// };
13#[derive(Clone, Debug)]
14pub struct GainOptions {
15    pub gain: f32,
16    pub audio_node_options: AudioNodeOptions,
17}
18
19impl Default for GainOptions {
20    fn default() -> Self {
21        Self {
22            gain: 1.,
23            audio_node_options: AudioNodeOptions::default(),
24        }
25    }
26}
27
28/// AudioNode for volume control
29#[derive(Debug)]
30pub struct GainNode {
31    registration: AudioContextRegistration,
32    channel_config: ChannelConfig,
33    gain: AudioParam,
34}
35
36impl AudioNode for GainNode {
37    fn registration(&self) -> &AudioContextRegistration {
38        &self.registration
39    }
40
41    fn channel_config(&self) -> &ChannelConfig {
42        &self.channel_config
43    }
44
45    fn number_of_inputs(&self) -> usize {
46        1
47    }
48
49    fn number_of_outputs(&self) -> usize {
50        1
51    }
52}
53
54impl GainNode {
55    pub fn new<C: BaseAudioContext>(context: &C, options: GainOptions) -> Self {
56        context.base().register(move |registration| {
57            let param_opts = AudioParamDescriptor {
58                name: String::new(),
59                min_value: f32::MIN,
60                max_value: f32::MAX,
61                default_value: 1.,
62                automation_rate: crate::param::AutomationRate::A,
63            };
64            let (param, proc) = context.create_audio_param(param_opts, &registration);
65
66            param.set_value(options.gain);
67
68            let render = GainRenderer { gain: proc };
69
70            let node = GainNode {
71                registration,
72                channel_config: options.audio_node_options.into(),
73                gain: param,
74            };
75
76            (node, Box::new(render))
77        })
78    }
79
80    pub fn gain(&self) -> &AudioParam {
81        &self.gain
82    }
83}
84
85struct GainRenderer {
86    gain: AudioParamId,
87}
88
89impl AudioProcessor for GainRenderer {
90    fn process(
91        &mut self,
92        inputs: &[AudioRenderQuantum],
93        outputs: &mut [AudioRenderQuantum],
94        params: AudioParamValues<'_>,
95        _scope: &AudioWorkletGlobalScope,
96    ) -> bool {
97        // single input/output node
98        let input = &inputs[0];
99        let output = &mut outputs[0];
100
101        if input.is_silent() {
102            output.make_silent();
103            return false;
104        }
105
106        let gain = params.get(&self.gain);
107
108        // very fast track for mute or pass-through
109        if gain.len() == 1 {
110            // 1e-6 is -120 dB when close to 0 and ±8.283506e-6 dB when close to 1
111            // very probably small enough to not be audible
112            let threshold = 1e-6;
113
114            let diff_to_zero = gain[0].abs();
115            if diff_to_zero <= threshold {
116                output.make_silent();
117                return false;
118            }
119
120            let diff_to_one = (1. - gain[0]).abs();
121            if diff_to_one <= threshold {
122                *output = input.clone();
123                return false;
124            }
125        }
126
127        *output = input.clone();
128
129        if gain.len() == 1 {
130            let g = gain[0];
131
132            output.channels_mut().iter_mut().for_each(|channel| {
133                channel.iter_mut().for_each(|o| *o *= g);
134            });
135        } else {
136            output.channels_mut().iter_mut().for_each(|channel| {
137                channel
138                    .iter_mut()
139                    .zip(gain.iter().cycle())
140                    .for_each(|(o, g)| *o *= g);
141            });
142        }
143
144        false
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::context::OfflineAudioContext;
152    use float_eq::assert_float_eq;
153
154    #[test]
155    fn test_audioparam_value_applies_immediately() {
156        let context = OfflineAudioContext::new(1, 128, 48000.);
157        let options = GainOptions {
158            gain: 0.12,
159            ..Default::default()
160        };
161        let src = GainNode::new(&context, options);
162        assert_float_eq!(src.gain.value(), 0.12, abs_all <= 0.);
163    }
164}