web_audio_api/node/
stereo_panner.rs

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