Skip to main content

web_audio_api/node/
stereo_panner.rs

1//! The stereo panner control and renderer parts
2use std::f32::consts::PI;
3
4use crate::context::{AudioContextRegistration, AudioParamId, BaseAudioContext};
5use crate::param::{AudioParam, AudioParamDescriptor};
6use crate::render::{
7    AudioParamValues, AudioProcessor, AudioRenderQuantum, AudioWorkletGlobalScope,
8};
9
10use super::{AudioNode, AudioNodeOptions, ChannelConfig, ChannelCountMode, ChannelInterpretation};
11
12/// Options for constructing a [`StereoPannerOptions`]
13// dictionary StereoPannerOptions : AudioNodeOptions {
14//   float pan = 0;
15// };
16#[derive(Clone, Debug)]
17pub struct StereoPannerOptions {
18    /// initial value for the pan parameter
19    pub pan: f32,
20    /// audio node options
21    pub audio_node_options: AudioNodeOptions,
22}
23
24impl Default for StereoPannerOptions {
25    fn default() -> Self {
26        Self {
27            pan: 0.,
28            audio_node_options: AudioNodeOptions {
29                channel_count: 2,
30                channel_count_mode: ChannelCountMode::ClampedMax,
31                channel_interpretation: ChannelInterpretation::Speakers,
32            },
33        }
34    }
35}
36
37/// Assert that the channel count is valid for the StereoPannerNode
38/// see <https://webaudio.github.io/web-audio-api/#audionode-channelcount-constraints>
39///
40/// # Panics
41///
42/// This function panics if given count is greater than 2
43///
44#[track_caller]
45#[inline(always)]
46fn assert_valid_channel_count(count: usize) {
47    assert!(
48        count <= 2,
49        "NotSupportedError - StereoPannerNode channel count cannot be greater than two"
50    );
51}
52
53/// Assert that the channel count is valid for the StereoPannerNode
54/// see <https://webaudio.github.io/web-audio-api/#audionode-channelcountmode-constraints>
55///
56/// # Panics
57///
58/// This function panics if given count mode is [`ChannelCountMode::Max`]
59///
60#[track_caller]
61#[inline(always)]
62fn assert_valid_channel_count_mode(mode: ChannelCountMode) {
63    assert_ne!(
64        mode,
65        ChannelCountMode::Max,
66        "NotSupportedError - StereoPannerNode channel count mode cannot be set to max",
67    );
68}
69
70/// Generates the stereo gains for a specific x ∈ [0, 1] derived from pan:
71/// - `gain_left = (x * PI / 2.).cos()`
72/// - `gain_right = (x * PI / 2.).sin()`
73#[inline(always)]
74fn get_stereo_gains(x: f32) -> [f32; 2] {
75    let gain_left = ((1. - x) * PI / 2.).sin(); // more accurate than cos()
76    let gain_right = (x * PI / 2.).sin();
77
78    [gain_left, gain_right]
79}
80
81/// `StereoPannerNode` positions an incoming audio stream in a stereo image
82///
83/// It is an audio-processing module that positions an incoming audio stream
84/// in a stereo image using a low-cost panning algorithm.
85///
86/// - MDN documentation: <https://developer.mozilla.org/en-US/docs/Web/API/StereoPannerNode>
87/// - specification: <https://webaudio.github.io/web-audio-api/#stereopannernode>
88/// - see also: [`BaseAudioContext::create_stereo_panner`]
89///
90/// # Usage
91///
92/// ```no_run
93/// use web_audio_api::context::{BaseAudioContext, AudioContext};
94/// use web_audio_api::node::{AudioNode, AudioScheduledSourceNode};
95///
96/// // create an `AudioContext`
97/// let context = AudioContext::default();
98/// // load and decode a soundfile
99/// let panner = context.create_stereo_panner();
100/// panner.connect(&context.destination());
101/// // position source on the left
102/// panner.pan().set_value(-1.);
103///
104/// // pipe an oscillator into the stereo panner
105/// let mut osc = context.create_oscillator();
106/// osc.frequency().set_value(200.);
107/// osc.connect(&panner);
108/// osc.start();
109/// ```
110///
111/// # Examples
112///
113/// - `cargo run --release --example stereo_panner`
114///
115#[derive(Debug)]
116pub struct StereoPannerNode {
117    /// Represents the node instance and its associated audio context
118    registration: AudioContextRegistration,
119    /// Infos about audio node channel configuration
120    channel_config: ChannelConfig,
121    /// The position of the input in the output’s stereo image. -1 represents
122    /// full left, +1 represents full right.
123    pan: AudioParam,
124}
125
126impl AudioNode for StereoPannerNode {
127    fn registration(&self) -> &AudioContextRegistration {
128        &self.registration
129    }
130
131    fn channel_config(&self) -> &ChannelConfig {
132        &self.channel_config
133    }
134
135    fn number_of_inputs(&self) -> usize {
136        1
137    }
138
139    fn number_of_outputs(&self) -> usize {
140        1
141    }
142
143    fn set_channel_count_mode(&self, mode: ChannelCountMode) {
144        assert_valid_channel_count_mode(mode);
145        self.channel_config
146            .set_count_mode(mode, self.registration());
147    }
148
149    fn set_channel_count(&self, count: usize) {
150        assert_valid_channel_count(count);
151        self.channel_config.set_count(count, self.registration());
152    }
153}
154
155impl StereoPannerNode {
156    /// returns a `StereoPannerNode` instance
157    ///
158    /// # Arguments
159    ///
160    /// * `context` - audio context in which the audio node will live.
161    /// * `options` - stereo panner options
162    ///
163    /// # Panics
164    ///
165    /// Will panic if:
166    ///
167    /// * `options.channel_config.count` is greater than 2
168    /// * `options.channel_config.mode` is `ChannelCountMode::Max`
169    ///
170    pub fn new<C: BaseAudioContext>(context: &C, options: StereoPannerOptions) -> Self {
171        context.base().register(move |registration| {
172            assert_valid_channel_count_mode(options.audio_node_options.channel_count_mode);
173            assert_valid_channel_count(options.audio_node_options.channel_count);
174
175            let pan_options = AudioParamDescriptor {
176                name: String::new(),
177                min_value: -1.,
178                max_value: 1.,
179                default_value: 0.,
180                automation_rate: crate::param::AutomationRate::A,
181            };
182            let (pan_param, pan_proc) = context.create_audio_param(pan_options, &registration);
183
184            pan_param.set_value(options.pan);
185
186            let renderer = StereoPannerRenderer::new(pan_proc);
187
188            let node = Self {
189                registration,
190                channel_config: options.audio_node_options.into(),
191                pan: pan_param,
192            };
193
194            (node, Box::new(renderer))
195        })
196    }
197
198    /// Returns the pan audio parameter
199    #[must_use]
200    pub fn pan(&self) -> &AudioParam {
201        &self.pan
202    }
203}
204
205/// `StereoPannerRenderer` represents the rendering part of `StereoPannerNode`
206struct StereoPannerRenderer {
207    /// Position of the input in the output’s stereo image.
208    /// -1 represents full left, +1 represents full right.
209    pan: AudioParamId,
210}
211
212impl StereoPannerRenderer {
213    fn new(pan: AudioParamId) -> Self {
214        Self { pan }
215    }
216}
217
218impl AudioProcessor for StereoPannerRenderer {
219    fn process(
220        &mut self,
221        inputs: &[AudioRenderQuantum],
222        outputs: &mut [AudioRenderQuantum],
223        params: AudioParamValues<'_>,
224        _scope: &AudioWorkletGlobalScope,
225    ) -> bool {
226        // single input/output node
227        let input = &inputs[0];
228        let output = &mut outputs[0];
229
230        if input.is_silent() {
231            output.make_silent();
232            return false;
233        }
234
235        output.set_number_of_channels(2);
236
237        // a-rate param
238        let pan_values = params.get(&self.pan);
239
240        let [left, right] = output.stereo_mut();
241
242        match input.number_of_channels() {
243            0 => (),
244            1 => {
245                if pan_values.len() == 1 {
246                    let pan = pan_values[0];
247                    let x = (pan + 1.) * 0.5;
248                    let [gain_left, gain_right] = get_stereo_gains(x);
249
250                    left.iter_mut()
251                        .zip(right.iter_mut())
252                        .zip(input.channel_data(0).iter())
253                        .for_each(|((l, r), input)| {
254                            *l = input * gain_left;
255                            *r = input * gain_right;
256                        });
257                } else {
258                    left.iter_mut()
259                        .zip(right.iter_mut())
260                        .zip(pan_values.iter())
261                        .zip(input.channel_data(0).iter())
262                        .for_each(|(((l, r), pan), input)| {
263                            let x = (pan + 1.) * 0.5;
264                            let [gain_left, gain_right] = get_stereo_gains(x);
265
266                            *l = input * gain_left;
267                            *r = input * gain_right;
268                        });
269                }
270            }
271            2 => {
272                if pan_values.len() == 1 {
273                    let pan = pan_values[0];
274                    let x = if pan <= 0. { pan + 1. } else { pan };
275                    let [gain_left, gain_right] = get_stereo_gains(x);
276
277                    left.iter_mut()
278                        .zip(right.iter_mut())
279                        .zip(input.channel_data(0).iter())
280                        .zip(input.channel_data(1).iter())
281                        .for_each(|(((l, r), &input_left), &input_right)| {
282                            if pan <= 0. {
283                                *l = input_right.mul_add(gain_left, input_left);
284                                *r = input_right * gain_right;
285                            } else {
286                                *l = input_left * gain_left;
287                                *r = input_left.mul_add(gain_right, input_right);
288                            }
289                        });
290                } else {
291                    left.iter_mut()
292                        .zip(right.iter_mut())
293                        .zip(pan_values.iter())
294                        .zip(input.channel_data(0).iter())
295                        .zip(input.channel_data(1).iter())
296                        .for_each(|((((l, r), &pan), &input_left), &input_right)| {
297                            if pan <= 0. {
298                                let x = pan + 1.;
299                                let [gain_left, gain_right] = get_stereo_gains(x);
300
301                                *l = input_right.mul_add(gain_left, input_left);
302                                *r = input_right * gain_right;
303                            } else {
304                                let x = pan;
305                                let [gain_left, gain_right] = get_stereo_gains(x);
306
307                                *l = input_left * gain_left;
308                                *r = input_left.mul_add(gain_right, input_right);
309                            }
310                        });
311                }
312            }
313            _ => panic!("StereoPannerNode should not have more than 2 channels to process"),
314        }
315
316        false
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use float_eq::assert_float_eq;
323
324    use crate::context::{BaseAudioContext, OfflineAudioContext};
325    use crate::node::AudioScheduledSourceNode;
326
327    use super::*;
328
329    #[test]
330    fn test_constructor() {
331        {
332            let context = OfflineAudioContext::new(2, 1, 44_100.);
333            let _panner = StereoPannerNode::new(&context, StereoPannerOptions::default());
334        }
335
336        {
337            let context = OfflineAudioContext::new(2, 1, 44_100.);
338            let _panner = context.create_stereo_panner();
339        }
340
341        {
342            let context = OfflineAudioContext::new(2, 1, 44_100.);
343            let panner = StereoPannerNode::new(&context, StereoPannerOptions::default());
344
345            let default_pan = 0.;
346            let pan = panner.pan.value();
347            assert_float_eq!(pan, default_pan, abs_all <= 0.);
348        }
349    }
350
351    #[test]
352    fn test_init_with_channel_count_mode() {
353        let context = OfflineAudioContext::new(2, 1, 44_100.);
354        let options = StereoPannerOptions {
355            audio_node_options: AudioNodeOptions {
356                channel_count_mode: ChannelCountMode::Explicit,
357                ..AudioNodeOptions::default()
358            },
359            ..StereoPannerOptions::default()
360        };
361        let panner = StereoPannerNode::new(&context, options);
362        assert_eq!(
363            panner.channel_config().count_mode(),
364            ChannelCountMode::Explicit
365        );
366        assert_eq!(panner.channel_count_mode(), ChannelCountMode::Explicit);
367    }
368
369    #[test]
370    fn test_mono_panning() {
371        let sample_rate = 44_100.;
372
373        let context = OfflineAudioContext::new(2, 128, 44_100.);
374
375        let mut buffer = context.create_buffer(1, 128, sample_rate);
376        buffer.copy_to_channel(&[1.; 128], 0);
377
378        // left
379        {
380            let mut context = OfflineAudioContext::new(2, 128, 44_100.);
381            // force channel count to mono
382            let panner = StereoPannerNode::new(
383                &context,
384                StereoPannerOptions {
385                    audio_node_options: AudioNodeOptions {
386                        channel_count: 1,
387                        channel_count_mode: ChannelCountMode::ClampedMax,
388                        ..AudioNodeOptions::default()
389                    },
390                    pan: -1.,
391                },
392            );
393            panner.connect(&context.destination());
394
395            let mut src = context.create_buffer_source();
396            src.connect(&panner);
397            src.set_buffer(buffer.clone());
398            src.start();
399
400            let res = context.start_rendering_sync();
401
402            assert_float_eq!(res.get_channel_data(0)[..], [1.; 128], abs_all <= 0.);
403            assert_float_eq!(res.get_channel_data(1)[..], [0.; 128], abs_all <= 0.);
404        }
405
406        // right
407        {
408            let mut context = OfflineAudioContext::new(2, 128, 44_100.);
409            // force channel count to mono
410            let panner = StereoPannerNode::new(
411                &context,
412                StereoPannerOptions {
413                    audio_node_options: AudioNodeOptions {
414                        channel_count: 1,
415                        channel_count_mode: ChannelCountMode::ClampedMax,
416                        ..AudioNodeOptions::default()
417                    },
418                    pan: 1.,
419                },
420            );
421            panner.connect(&context.destination());
422
423            let mut src = context.create_buffer_source();
424            src.connect(&panner);
425            src.set_buffer(buffer.clone());
426            src.start();
427
428            let res = context.start_rendering_sync();
429
430            assert_float_eq!(res.get_channel_data(0)[..], [0.; 128], abs_all <= 1e-7);
431            assert_float_eq!(res.get_channel_data(1)[..], [1.; 128], abs_all <= 0.);
432        }
433
434        // equal power
435        {
436            let mut context = OfflineAudioContext::new(2, 128, 44_100.);
437            // force channel count to mono
438            let panner = StereoPannerNode::new(
439                &context,
440                StereoPannerOptions {
441                    audio_node_options: AudioNodeOptions {
442                        channel_count: 1,
443                        channel_count_mode: ChannelCountMode::ClampedMax,
444                        ..AudioNodeOptions::default()
445                    },
446                    pan: 0.,
447                },
448            );
449            panner.connect(&context.destination());
450
451            let mut src = context.create_buffer_source();
452            src.connect(&panner);
453            src.set_buffer(buffer.clone());
454            src.start();
455
456            let res = context.start_rendering_sync();
457
458            let mut power = [0.; 128];
459            power
460                .iter_mut()
461                .zip(res.get_channel_data(0).iter())
462                .zip(res.get_channel_data(1).iter())
463                .for_each(|((p, l), r)| {
464                    *p = l * l + r * r;
465                });
466
467            assert_float_eq!(power, [1.; 128], abs_all <= 1.2e-7);
468        }
469    }
470
471    #[test]
472    fn test_stereo_panning() {
473        let sample_rate = 44_100.;
474
475        let context = OfflineAudioContext::new(2, 128, 44_100.);
476
477        let mut buffer = context.create_buffer(2, 128, sample_rate);
478        buffer.copy_to_channel(&[1.; 128], 0);
479        buffer.copy_to_channel(&[1.; 128], 1);
480
481        // left
482        {
483            let mut context = OfflineAudioContext::new(2, 128, 44_100.);
484            // force channel count to mono
485            let panner = StereoPannerNode::new(
486                &context,
487                StereoPannerOptions {
488                    pan: -1.,
489                    ..StereoPannerOptions::default()
490                },
491            );
492            panner.connect(&context.destination());
493
494            let mut src = context.create_buffer_source();
495            src.connect(&panner);
496            src.set_buffer(buffer.clone());
497            src.start();
498
499            let res = context.start_rendering_sync();
500
501            assert_float_eq!(res.get_channel_data(0)[..], [2.; 128], abs_all <= 0.);
502            assert_float_eq!(res.get_channel_data(1)[..], [0.; 128], abs_all <= 0.);
503        }
504
505        // right
506        {
507            let mut context = OfflineAudioContext::new(2, 128, 44_100.);
508            // force channel count to mono
509            let panner = StereoPannerNode::new(
510                &context,
511                StereoPannerOptions {
512                    pan: 1.,
513                    ..StereoPannerOptions::default()
514                },
515            );
516            panner.connect(&context.destination());
517
518            let mut src = context.create_buffer_source();
519            src.connect(&panner);
520            src.set_buffer(buffer.clone());
521            src.start();
522
523            let res = context.start_rendering_sync();
524
525            assert_float_eq!(res.get_channel_data(0)[..], [0.; 128], abs_all <= 1e-7);
526            assert_float_eq!(res.get_channel_data(1)[..], [2.; 128], abs_all <= 0.);
527        }
528
529        // middle
530        {
531            let mut context = OfflineAudioContext::new(2, 128, 44_100.);
532            // force channel count to mono
533            let panner = StereoPannerNode::new(
534                &context,
535                StereoPannerOptions {
536                    pan: 0.,
537                    ..StereoPannerOptions::default()
538                },
539            );
540            panner.connect(&context.destination());
541
542            let mut src = context.create_buffer_source();
543            src.connect(&panner);
544            src.set_buffer(buffer.clone());
545            src.start();
546
547            let res = context.start_rendering_sync();
548
549            assert_float_eq!(res.get_channel_data(0)[..], [1.; 128], abs_all <= 1e-7);
550            assert_float_eq!(res.get_channel_data(1)[..], [1.; 128], abs_all <= 0.);
551        }
552    }
553}