Skip to main content

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/// Applies a single gain (volume) value to its incoming audio signal.
29///
30/// The value is exposed as an [`AudioParam`] so it can be automated over time.
31///
32/// - MDN documentation: <https://developer.mozilla.org/en-US/docs/Web/API/GainNode>
33/// - specification: <https://webaudio.github.io/web-audio-api/#GainNode>
34/// - see also: [`BaseAudioContext::create_gain`]
35///
36/// # Usage
37///
38/// ```no_run
39/// use web_audio_api::context::{BaseAudioContext, AudioContext};
40/// use web_audio_api::node::{AudioNode, AudioScheduledSourceNode};
41///
42/// let context = AudioContext::default();
43///
44/// // Build an oscillator that we want to fade in.
45/// let mut osc = context.create_oscillator();
46/// osc.frequency().set_value(440.);
47///
48/// // The gain node sits between the source and the destination.
49/// let gain = context.create_gain();
50/// gain.gain().set_value(0.);
51/// gain.gain()
52///     .linear_ramp_to_value_at_time(1., context.current_time() + 1.);
53///
54/// osc.connect(&gain);
55/// gain.connect(&context.destination());
56/// osc.start();
57/// ```
58///
59/// # Examples
60///
61/// - `cargo run --release --example amplitude_modulation`
62///
63#[derive(Debug)]
64pub struct GainNode {
65    /// Represents the node instance and its associated audio context
66    registration: AudioContextRegistration,
67    /// Infos about audio node channel configuration
68    channel_config: ChannelConfig,
69    /// Multiplier applied to every sample. Defaults to `1.0` (pass-through).
70    gain: AudioParam,
71}
72
73impl AudioNode for GainNode {
74    fn registration(&self) -> &AudioContextRegistration {
75        &self.registration
76    }
77
78    fn channel_config(&self) -> &ChannelConfig {
79        &self.channel_config
80    }
81
82    fn number_of_inputs(&self) -> usize {
83        1
84    }
85
86    fn number_of_outputs(&self) -> usize {
87        1
88    }
89}
90
91impl GainNode {
92    /// Constructs a new `GainNode` from explicit options.
93    ///
94    /// [`BaseAudioContext::create_gain`] is an alternative that applies the
95    /// spec defaults (`gain = 1.0`).
96    ///
97    /// # Arguments
98    ///
99    /// * `context` - audio context in which the audio node will live
100    /// * `options` - initial value of the gain parameter and channel config
101    pub fn new<C: BaseAudioContext>(context: &C, options: GainOptions) -> Self {
102        context.base().register(move |registration| {
103            let param_opts = AudioParamDescriptor {
104                name: String::new(),
105                min_value: f32::MIN,
106                max_value: f32::MAX,
107                default_value: 1.,
108                automation_rate: crate::param::AutomationRate::A,
109            };
110            let (param, proc) = context.create_audio_param(param_opts, &registration);
111
112            param.set_value(options.gain);
113
114            let render = GainRenderer { gain: proc };
115
116            let node = GainNode {
117                registration,
118                channel_config: options.audio_node_options.into(),
119                gain: param,
120            };
121
122            (node, Box::new(render))
123        })
124    }
125
126    /// Returns the gain `AudioParam`.
127    ///
128    /// The default value is `1.0` (pass-through). Setting `0.0` mutes the
129    /// signal; values greater than `1.0` boost it (and may clip downstream
130    /// nodes if uncompensated). Because the parameter is `a-rate`, it can be
131    /// scheduled with the full automation API such as
132    /// [`AudioParam::linear_ramp_to_value_at_time`] for fades.
133    #[must_use]
134    pub fn gain(&self) -> &AudioParam {
135        &self.gain
136    }
137}
138
139struct GainRenderer {
140    gain: AudioParamId,
141}
142
143impl AudioProcessor for GainRenderer {
144    fn process(
145        &mut self,
146        inputs: &[AudioRenderQuantum],
147        outputs: &mut [AudioRenderQuantum],
148        params: AudioParamValues<'_>,
149        _scope: &AudioWorkletGlobalScope,
150    ) -> bool {
151        // single input/output node
152        let input = &inputs[0];
153        let output = &mut outputs[0];
154
155        if input.is_silent() {
156            output.make_silent();
157            return false;
158        }
159
160        let gain = params.get(&self.gain);
161
162        // very fast track for mute or pass-through
163        if gain.len() == 1 {
164            // 1e-6 is -120 dB when close to 0 and ±8.283506e-6 dB when close to 1
165            // very probably small enough to not be audible
166            let threshold = 1e-6;
167
168            let diff_to_zero = gain[0].abs();
169            if diff_to_zero <= threshold {
170                output.make_silent();
171                return false;
172            }
173
174            let diff_to_one = (1. - gain[0]).abs();
175            if diff_to_one <= threshold {
176                *output = input.clone();
177                return false;
178            }
179        }
180
181        *output = input.clone();
182
183        if gain.len() == 1 {
184            let g = gain[0];
185
186            output.channels_mut().iter_mut().for_each(|channel| {
187                channel.iter_mut().for_each(|o| *o *= g);
188            });
189        } else {
190            output.channels_mut().iter_mut().for_each(|channel| {
191                channel
192                    .iter_mut()
193                    .zip(gain.iter().cycle())
194                    .for_each(|(o, g)| *o *= g);
195            });
196        }
197
198        false
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::context::OfflineAudioContext;
206    use float_eq::assert_float_eq;
207
208    #[test]
209    fn test_audioparam_value_applies_immediately() {
210        let context = OfflineAudioContext::new(1, 128, 48000.);
211        let options = GainOptions {
212            gain: 0.12,
213            ..Default::default()
214        };
215        let src = GainNode::new(&context, options);
216        assert_float_eq!(src.gain.value(), 0.12, abs_all <= 0.);
217    }
218}