1use std::any::Any;
2use std::collections::HashMap;
3use std::f32::consts::PI;
4use std::sync::{Mutex, OnceLock};
5
6use float_eq::float_eq;
7use hrtf::{HrirSphere, HrtfContext, HrtfProcessor, Vec3};
8
9use crate::context::{AudioContextRegistration, AudioParamId, BaseAudioContext};
10use crate::param::{AudioParam, AudioParamDescriptor};
11use crate::render::{
12 AudioParamValues, AudioProcessor, AudioRenderQuantum, AudioWorkletGlobalScope,
13};
14use crate::RENDER_QUANTUM_SIZE;
15
16use super::{AudioNode, AudioNodeOptions, ChannelConfig, ChannelCountMode, ChannelInterpretation};
17
18#[track_caller]
25#[inline(always)]
26#[allow(clippy::manual_range_contains)]
27pub(crate) fn assert_valid_cone_outer_gain(value: f64) {
28 assert!(
29 value >= 0. && value <= 1.,
30 "InvalidStateError - coneOuterGain must be in the range [0, 1]"
31 );
32}
33
34pub(crate) fn load_hrtf_processor(sample_rate: u32) -> (HrtfProcessor, usize) {
40 static INSTANCE: OnceLock<Mutex<HashMap<u32, (HrtfProcessor, usize)>>> = OnceLock::new();
41 let cache = INSTANCE.get_or_init(|| Mutex::new(HashMap::new()));
42
43 {
45 if let Some(value) = cache.lock().unwrap().get(&sample_rate) {
46 return value.clone();
47 }
48 }
49
50 let resource = include_bytes!("../../resources/IRC_1003_C.bin");
52 let hrir_sphere = HrirSphere::new(&resource[..], sample_rate).unwrap();
53 let len = hrir_sphere.len();
54
55 let interpolation_steps = 1; let samples_per_step = RENDER_QUANTUM_SIZE / interpolation_steps;
57 let processor = HrtfProcessor::new(hrir_sphere, interpolation_steps, samples_per_step);
58
59 let value = (processor, len);
60 cache.lock().unwrap().insert(sample_rate, value.clone());
61
62 value
63}
64
65#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
67pub enum PanningModelType {
68 #[default]
69 EqualPower,
70 HRTF,
71}
72
73impl From<u8> for PanningModelType {
74 fn from(i: u8) -> Self {
75 match i {
76 0 => PanningModelType::EqualPower,
77 1 => PanningModelType::HRTF,
78 _ => unreachable!(),
79 }
80 }
81}
82
83#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
85pub enum DistanceModelType {
86 Linear,
87 #[default]
88 Inverse,
89 Exponential,
90}
91
92impl From<u8> for DistanceModelType {
93 fn from(i: u8) -> Self {
94 match i {
95 0 => DistanceModelType::Linear,
96 1 => DistanceModelType::Inverse,
97 2 => DistanceModelType::Exponential,
98 _ => unreachable!(),
99 }
100 }
101}
102
103#[derive(Clone, Debug)]
121pub struct PannerOptions {
122 pub panning_model: PanningModelType,
123 pub distance_model: DistanceModelType,
124 pub position_x: f32,
125 pub position_y: f32,
126 pub position_z: f32,
127 pub orientation_x: f32,
128 pub orientation_y: f32,
129 pub orientation_z: f32,
130 pub ref_distance: f64,
131 pub max_distance: f64,
132 pub rolloff_factor: f64,
133 pub cone_inner_angle: f64,
134 pub cone_outer_angle: f64,
135 pub cone_outer_gain: f64,
136 pub audio_node_options: AudioNodeOptions,
137}
138
139impl Default for PannerOptions {
140 fn default() -> Self {
141 PannerOptions {
142 panning_model: PanningModelType::default(),
143 distance_model: DistanceModelType::default(),
144 position_x: 0.,
145 position_y: 0.,
146 position_z: 0.,
147 orientation_x: 1.,
148 orientation_y: 0.,
149 orientation_z: 0.,
150 ref_distance: 1.,
151 max_distance: 10000.,
152 rolloff_factor: 1.,
153 cone_inner_angle: 360.,
154 cone_outer_angle: 360.,
155 cone_outer_gain: 0.,
156 audio_node_options: AudioNodeOptions {
157 channel_count: 2,
158 channel_count_mode: ChannelCountMode::ClampedMax,
159 channel_interpretation: ChannelInterpretation::Speakers,
160 },
161 }
162 }
163}
164
165enum ControlMessage {
166 DistanceModel(DistanceModelType),
167 PanningModel(Box<Option<HrtfState>>),
169 RefDistance(f64),
170 MaxDistance(f64),
171 RollOffFactor(f64),
172 ConeInnerAngle(f64),
173 ConeOuterAngle(f64),
174 ConeOuterGain(f64),
175}
176
177#[track_caller]
185#[inline(always)]
186fn assert_valid_channel_count(count: usize) {
187 assert!(
188 count <= 2,
189 "NotSupportedError - PannerNode channel count cannot be greater than two"
190 );
191}
192
193#[track_caller]
201#[inline(always)]
202fn assert_valid_channel_count_mode(mode: ChannelCountMode) {
203 assert_ne!(
204 mode,
205 ChannelCountMode::Max,
206 "NotSupportedError - PannerNode channel count mode cannot be set to max"
207 );
208}
209
210struct HrtfState {
212 len: usize,
213 processor: HrtfProcessor,
214 output_interleaved: Vec<(f32, f32)>,
215 prev_sample_vector: Vec3,
216 prev_left_samples: Vec<f32>,
217 prev_right_samples: Vec<f32>,
218 prev_distance_gain: f32,
219}
220
221impl HrtfState {
222 fn new(processor: HrtfProcessor, len: usize) -> Self {
223 Self {
224 len,
225 processor,
226 output_interleaved: vec![(0., 0.); RENDER_QUANTUM_SIZE],
227 prev_sample_vector: Vec3::new(0., 0., 1.),
228 prev_left_samples: vec![], prev_right_samples: vec![], prev_distance_gain: 0.,
231 }
232 }
233
234 fn process(
235 &mut self,
236 source: &[f32],
237 new_distance_gain: f32,
238 projected_source: [f32; 3],
239 ) -> &[(f32, f32)] {
240 self.output_interleaved.fill((0., 0.));
242
243 let new_sample_vector = Vec3 {
244 x: projected_source[0],
245 z: projected_source[1],
246 y: projected_source[2],
247 };
248
249 let context = HrtfContext {
250 source,
251 output: &mut self.output_interleaved,
252 new_sample_vector,
253 prev_sample_vector: self.prev_sample_vector,
254 prev_left_samples: &mut self.prev_left_samples,
255 prev_right_samples: &mut self.prev_right_samples,
256 new_distance_gain,
257 prev_distance_gain: self.prev_distance_gain,
258 };
259
260 self.processor.process_samples(context);
261
262 self.prev_sample_vector = new_sample_vector;
263 self.prev_distance_gain = new_distance_gain;
264
265 &self.output_interleaved
266 }
267
268 fn tail_time_samples(&self) -> usize {
269 self.len
270 }
271}
272
273#[derive(Debug)]
320pub struct PannerNode {
321 registration: AudioContextRegistration,
322 channel_config: ChannelConfig,
323 position_x: AudioParam,
324 position_y: AudioParam,
325 position_z: AudioParam,
326 orientation_x: AudioParam,
327 orientation_y: AudioParam,
328 orientation_z: AudioParam,
329 cone_inner_angle: f64,
330 cone_outer_angle: f64,
331 cone_outer_gain: f64,
332 distance_model: DistanceModelType,
333 ref_distance: f64,
334 max_distance: f64,
335 rolloff_factor: f64,
336 panning_model: PanningModelType,
337}
338
339impl AudioNode for PannerNode {
340 fn registration(&self) -> &AudioContextRegistration {
341 &self.registration
342 }
343
344 fn channel_config(&self) -> &ChannelConfig {
345 &self.channel_config
346 }
347
348 fn number_of_inputs(&self) -> usize {
349 1
350 }
351
352 fn number_of_outputs(&self) -> usize {
353 1
354 }
355
356 fn set_channel_count(&self, count: usize) {
359 assert_valid_channel_count(count);
360 self.channel_config.set_count(count, self.registration());
361 }
362
363 fn set_channel_count_mode(&self, mode: ChannelCountMode) {
364 assert_valid_channel_count_mode(mode);
365 self.channel_config
366 .set_count_mode(mode, self.registration());
367 }
368}
369
370impl PannerNode {
371 #[allow(clippy::missing_panics_doc)]
387 pub fn new<C: BaseAudioContext>(context: &C, options: PannerOptions) -> Self {
388 let mut node = context.base().register(|registration| {
389 use crate::spatial::PARAM_OPTS;
390
391 let PannerOptions {
392 position_x,
393 position_y,
394 position_z,
395 orientation_x,
396 orientation_y,
397 orientation_z,
398 distance_model,
399 ref_distance,
400 max_distance,
401 rolloff_factor,
402 cone_inner_angle,
403 cone_outer_angle,
404 cone_outer_gain,
405 audio_node_options: channel_config,
406 panning_model,
407 } = options;
408
409 assert!(
410 ref_distance >= 0.,
411 "RangeError - refDistance cannot be negative"
412 );
413 assert!(
414 max_distance > 0.,
415 "RangeError - maxDistance must be strictly positive"
416 );
417 assert!(
418 rolloff_factor >= 0.,
419 "RangeError - rolloffFactor cannot be negative"
420 );
421 assert_valid_cone_outer_gain(cone_outer_gain);
422 assert_valid_channel_count(channel_config.channel_count);
423 assert_valid_channel_count_mode(channel_config.channel_count_mode);
424
425 let (param_px, render_px) = context.create_audio_param(PARAM_OPTS, ®istration);
427 let (param_py, render_py) = context.create_audio_param(PARAM_OPTS, ®istration);
428 let (param_pz, render_pz) = context.create_audio_param(PARAM_OPTS, ®istration);
429 param_px.set_value(position_x);
430 param_py.set_value(position_y);
431 param_pz.set_value(position_z);
432
433 let orientation_x_opts = AudioParamDescriptor {
435 default_value: 1.0,
436 ..PARAM_OPTS
437 };
438 let (param_ox, render_ox) =
439 context.create_audio_param(orientation_x_opts, ®istration);
440 let (param_oy, render_oy) = context.create_audio_param(PARAM_OPTS, ®istration);
441 let (param_oz, render_oz) = context.create_audio_param(PARAM_OPTS, ®istration);
442 param_ox.set_value(orientation_x);
443 param_oy.set_value(orientation_y);
444 param_oz.set_value(orientation_z);
445
446 let render = PannerRenderer {
447 position_x: render_px,
448 position_y: render_py,
449 position_z: render_pz,
450 orientation_x: render_ox,
451 orientation_y: render_oy,
452 orientation_z: render_oz,
453 distance_model,
454 ref_distance,
455 max_distance,
456 rolloff_factor,
457 cone_inner_angle,
458 cone_outer_angle,
459 cone_outer_gain,
460 hrtf_state: None,
461 tail_time_counter: 0,
462 };
463
464 let node = PannerNode {
465 registration,
466 channel_config: channel_config.into(),
467 position_x: param_px,
468 position_y: param_py,
469 position_z: param_pz,
470 orientation_x: param_ox,
471 orientation_y: param_oy,
472 orientation_z: param_oz,
473 distance_model,
474 ref_distance,
475 max_distance,
476 rolloff_factor,
477 cone_inner_angle,
478 cone_outer_angle,
479 cone_outer_gain,
480 panning_model,
481 };
482
483 context.base().ensure_audio_listener_present();
485
486 (node, Box::new(render))
487 });
488
489 context
491 .base()
492 .connect_listener_to_panner(node.registration().id());
493
494 node.set_panning_model(options.panning_model);
496
497 node
498 }
499
500 pub fn position_x(&self) -> &AudioParam {
501 &self.position_x
502 }
503
504 pub fn position_y(&self) -> &AudioParam {
505 &self.position_y
506 }
507
508 pub fn position_z(&self) -> &AudioParam {
509 &self.position_z
510 }
511
512 pub fn set_position(&self, x: f32, y: f32, z: f32) {
513 self.position_x.set_value(x);
514 self.position_y.set_value(y);
515 self.position_z.set_value(z);
516 }
517
518 pub fn orientation_x(&self) -> &AudioParam {
519 &self.orientation_x
520 }
521
522 pub fn orientation_y(&self) -> &AudioParam {
523 &self.orientation_y
524 }
525
526 pub fn orientation_z(&self) -> &AudioParam {
527 &self.orientation_z
528 }
529
530 pub fn set_orientation(&self, x: f32, y: f32, z: f32) {
531 self.orientation_x.set_value(x);
532 self.orientation_y.set_value(y);
533 self.orientation_z.set_value(z);
534 }
535
536 pub fn distance_model(&self) -> DistanceModelType {
537 self.distance_model
538 }
539
540 pub fn set_distance_model(&mut self, value: DistanceModelType) {
541 self.distance_model = value;
542 self.registration
543 .post_message(ControlMessage::DistanceModel(value));
544 }
545
546 pub fn ref_distance(&self) -> f64 {
547 self.ref_distance
548 }
549
550 pub fn set_ref_distance(&mut self, value: f64) {
556 assert!(value >= 0., "RangeError - refDistance cannot be negative");
557 self.ref_distance = value;
558 self.registration
559 .post_message(ControlMessage::RefDistance(value));
560 }
561
562 pub fn max_distance(&self) -> f64 {
563 self.max_distance
564 }
565
566 pub fn set_max_distance(&mut self, value: f64) {
572 assert!(
573 value > 0.,
574 "RangeError - maxDistance must be strictly positive"
575 );
576 self.max_distance = value;
577 self.registration
578 .post_message(ControlMessage::MaxDistance(value));
579 }
580
581 pub fn rolloff_factor(&self) -> f64 {
582 self.rolloff_factor
583 }
584
585 pub fn set_rolloff_factor(&mut self, value: f64) {
591 assert!(value >= 0., "RangeError - rolloffFactor cannot be negative");
592 self.rolloff_factor = value;
593 self.registration
594 .post_message(ControlMessage::RollOffFactor(value));
595 }
596
597 pub fn cone_inner_angle(&self) -> f64 {
598 self.cone_inner_angle
599 }
600
601 pub fn set_cone_inner_angle(&mut self, value: f64) {
602 self.cone_inner_angle = value;
603 self.registration
604 .post_message(ControlMessage::ConeInnerAngle(value));
605 }
606
607 pub fn cone_outer_angle(&self) -> f64 {
608 self.cone_outer_angle
609 }
610
611 pub fn set_cone_outer_angle(&mut self, value: f64) {
612 self.cone_outer_angle = value;
613 self.registration
614 .post_message(ControlMessage::ConeOuterAngle(value));
615 }
616
617 pub fn cone_outer_gain(&self) -> f64 {
618 self.cone_outer_gain
619 }
620
621 pub fn set_cone_outer_gain(&mut self, value: f64) {
627 assert_valid_cone_outer_gain(value);
628 self.cone_outer_gain = value;
629 self.registration
630 .post_message(ControlMessage::ConeOuterGain(value));
631 }
632
633 pub fn panning_model(&self) -> PanningModelType {
634 self.panning_model
635 }
636
637 #[allow(clippy::missing_panics_doc)] pub fn set_panning_model(&mut self, value: PanningModelType) {
639 let hrtf_option = match value {
640 PanningModelType::EqualPower => None,
641 PanningModelType::HRTF => {
642 let sample_rate = self.context().sample_rate() as u32;
643 let (processor, len) = load_hrtf_processor(sample_rate);
644 Some(HrtfState::new(processor, len))
645 }
646 };
647
648 self.panning_model = value;
649 self.registration
650 .post_message(ControlMessage::PanningModel(Box::new(hrtf_option)));
651 }
652}
653
654#[derive(Copy, Clone)]
655struct SpatialParams {
656 dist_gain: f32,
657 cone_gain: f32,
658 azimuth: f32,
659 elevation: f32,
660}
661
662struct PannerRenderer {
663 position_x: AudioParamId,
664 position_y: AudioParamId,
665 position_z: AudioParamId,
666 orientation_x: AudioParamId,
667 orientation_y: AudioParamId,
668 orientation_z: AudioParamId,
669 distance_model: DistanceModelType,
670 ref_distance: f64,
671 max_distance: f64,
672 rolloff_factor: f64,
673 cone_inner_angle: f64,
674 cone_outer_angle: f64,
675 cone_outer_gain: f64,
676 hrtf_state: Option<HrtfState>, tail_time_counter: usize,
678}
679
680impl AudioProcessor for PannerRenderer {
681 fn process(
682 &mut self,
683 inputs: &[AudioRenderQuantum],
684 outputs: &mut [AudioRenderQuantum],
685 params: AudioParamValues<'_>,
686 _scope: &AudioWorkletGlobalScope,
687 ) -> bool {
688 let input = &inputs[0];
690 let output = &mut outputs[0];
691
692 if input.is_silent() {
694 let tail_time = match &self.hrtf_state {
697 None => false,
698 Some(hrtf_state) => hrtf_state.tail_time_samples() > self.tail_time_counter,
699 };
700 if !tail_time {
701 output.make_silent();
702 return false;
703 }
704
705 self.tail_time_counter += RENDER_QUANTUM_SIZE;
706 }
707
708 let mut hrtf_state = self.hrtf_state.take();
710
711 let source_position_x = params.get(&self.position_x);
713 let source_position_y = params.get(&self.position_y);
714 let source_position_z = params.get(&self.position_z);
715 let source_orientation_x = params.get(&self.orientation_x);
716 let source_orientation_y = params.get(&self.orientation_y);
717 let source_orientation_z = params.get(&self.orientation_z);
718
719 let [listener_position_x, listener_position_y, listener_position_z, listener_forward_x, listener_forward_y, listener_forward_z, listener_up_x, listener_up_y, listener_up_z] =
721 params.listener_params();
722
723 let mut a_rate_params = source_position_x
725 .iter()
726 .cycle()
727 .zip(source_position_y.iter().cycle())
728 .zip(source_position_z.iter().cycle())
729 .zip(source_orientation_x.iter().cycle())
730 .zip(source_orientation_y.iter().cycle())
731 .zip(source_orientation_z.iter().cycle())
732 .zip(listener_position_x.iter().cycle())
733 .zip(listener_position_y.iter().cycle())
734 .zip(listener_position_z.iter().cycle())
735 .zip(listener_forward_x.iter().cycle())
736 .zip(listener_forward_y.iter().cycle())
737 .zip(listener_forward_z.iter().cycle())
738 .zip(listener_up_x.iter().cycle())
739 .zip(listener_up_y.iter().cycle())
740 .zip(listener_up_z.iter().cycle())
741 .map(|tuple| {
742 let ((((((sp_so_lp, lfx), lfy), lfz), lux), luy), luz) = tuple;
744 let (((sp_so, lpx), lpy), lpz) = sp_so_lp;
745 let (((sp, sox), soy), soz) = sp_so;
746 let ((spx, spy), spz) = sp;
747
748 let source_position = [*spx, *spy, *spz];
750 let source_orientation = [*sox, *soy, *soz];
751 let listener_position = [*lpx, *lpy, *lpz];
752 let listener_forward = [*lfx, *lfy, *lfz];
753 let listener_up = [*lux, *luy, *luz];
754
755 let dist_gain = self.dist_gain(source_position, listener_position);
757 let cone_gain =
758 self.cone_gain(source_position, source_orientation, listener_position);
759
760 let (azimuth, elevation) = crate::spatial::azimuth_and_elevation(
762 source_position,
763 listener_position,
764 listener_forward,
765 listener_up,
766 );
767
768 SpatialParams {
769 dist_gain,
770 cone_gain,
771 azimuth,
772 elevation,
773 }
774 });
775
776 if let Some(hrtf_state) = &mut hrtf_state {
777 let SpatialParams {
779 dist_gain,
780 cone_gain,
781 azimuth,
782 elevation,
783 } = a_rate_params.next().unwrap();
784
785 let new_distance_gain = cone_gain * dist_gain;
786
787 let az_rad = azimuth * PI / 180.;
789 let el_rad = elevation * PI / 180.;
790 let x = az_rad.sin() * el_rad.cos();
791 let z = az_rad.cos() * el_rad.cos();
792 let y = el_rad.sin();
793 let mut projected_source = [x, y, z];
794
795 if float_eq!(&projected_source[..], &[0.; 3][..], abs_all <= 1E-6) {
796 projected_source = [0., 0., 1.];
797 }
798
799 *output = input.clone();
805 let mut overall_gain_correction = 1.;
806 if output.number_of_channels() == 2 {
807 overall_gain_correction *= 2.; output.mix(1, ChannelInterpretation::Speakers);
809 }
810
811 let output_interleaved =
812 hrtf_state.process(output.channel_data(0), new_distance_gain, projected_source);
813
814 output.set_number_of_channels(2);
815 let [left, right] = output.stereo_mut();
816
817 output_interleaved
818 .iter()
819 .zip(&mut left[..])
820 .zip(&mut right[..])
821 .for_each(|((p, l), r)| {
822 *l = overall_gain_correction * p.0;
823 *r = overall_gain_correction * p.1;
824 });
825 } else {
826 let single_valued = source_position_x.len() == 1
830 && source_position_y.len() == 1
831 && source_position_z.len() == 1
832 && source_orientation_x.len() == 1
833 && source_orientation_y.len() == 1
834 && source_orientation_z.len() == 1
835 && listener_position_x.len() == 1
836 && listener_position_y.len() == 1
837 && listener_position_z.len() == 1
838 && listener_forward_x.len() == 1
839 && listener_forward_y.len() == 1
840 && listener_forward_z.len() == 1
841 && listener_up_x.len() == 1
842 && listener_up_y.len() == 1
843 && listener_up_z.len() == 1;
844
845 if single_valued {
846 let param_value = a_rate_params.next().unwrap();
847 match input.number_of_channels() {
848 1 => {
849 *output = input.clone();
850 output.mix(2, ChannelInterpretation::Speakers);
851 let [left, right] = output.stereo_mut();
852 left.iter_mut()
853 .zip(&mut right[..])
854 .for_each(|(l, r)| apply_mono_to_stereo_gain(param_value, l, r));
855 }
856 2 => {
857 output.set_number_of_channels(2);
858 let [left, right] = output.stereo_mut();
859 input
860 .channel_data(0)
861 .iter()
862 .copied()
863 .zip(input.channel_data(1).iter().copied())
864 .zip(&mut left[..])
865 .zip(&mut right[..])
866 .for_each(|(((il, ir), ol), or)| {
867 apply_stereo_to_stereo_gain(param_value, il, ir, ol, or)
868 });
869 }
870 _ => unreachable!(),
871 }
872 } else {
873 match input.number_of_channels() {
874 1 => {
875 *output = input.clone();
876 output.mix(2, ChannelInterpretation::Speakers);
877 let [left, right] = output.stereo_mut();
878 a_rate_params
879 .zip(&mut left[..])
880 .zip(&mut right[..])
881 .for_each(|((p, l), r)| apply_mono_to_stereo_gain(p, l, r));
882 }
883 2 => {
884 output.set_number_of_channels(2);
885 let [left, right] = output.stereo_mut();
886 a_rate_params
887 .zip(input.channel_data(0).iter().copied())
888 .zip(input.channel_data(1).iter().copied())
889 .zip(&mut left[..])
890 .zip(&mut right[..])
891 .for_each(|((((p, il), ir), ol), or)| {
892 apply_stereo_to_stereo_gain(p, il, ir, ol, or)
893 });
894 }
895 _ => unreachable!(),
896 }
897 }
898 }
899
900 self.hrtf_state = hrtf_state;
902
903 self.hrtf_state.is_some()
905 }
906
907 fn onmessage(&mut self, msg: &mut dyn Any) {
908 if let Some(control) = msg.downcast_mut::<ControlMessage>() {
909 match control {
910 ControlMessage::DistanceModel(value) => self.distance_model = *value,
911 ControlMessage::RefDistance(value) => self.ref_distance = *value,
912 ControlMessage::MaxDistance(value) => self.max_distance = *value,
913 ControlMessage::RollOffFactor(value) => self.rolloff_factor = *value,
914 ControlMessage::ConeInnerAngle(value) => self.cone_inner_angle = *value,
915 ControlMessage::ConeOuterAngle(value) => self.cone_outer_angle = *value,
916 ControlMessage::ConeOuterGain(value) => self.cone_outer_gain = *value,
917 ControlMessage::PanningModel(value) => self.hrtf_state = value.take(),
918 }
919
920 return;
921 }
922
923 log::warn!("PannerRenderer: Dropping incoming message {msg:?}");
924 }
925}
926
927impl PannerRenderer {
928 fn cone_gain(
929 &self,
930 source_position: [f32; 3],
931 source_orientation: [f32; 3],
932 listener_position: [f32; 3],
933 ) -> f32 {
934 let abs_inner_angle = self.cone_inner_angle.abs() as f32 / 2.;
935 let abs_outer_angle = self.cone_outer_angle.abs() as f32 / 2.;
936 if abs_inner_angle >= 180. && abs_outer_angle >= 180. {
937 1. } else {
939 let cone_outer_gain = self.cone_outer_gain as f32;
940
941 let abs_angle =
942 crate::spatial::angle(source_position, source_orientation, listener_position);
943
944 if abs_angle < abs_inner_angle {
945 1. } else if abs_angle >= abs_outer_angle {
947 cone_outer_gain } else {
949 let x = (abs_angle - abs_inner_angle) / (abs_outer_angle - abs_inner_angle);
951 (1. - x) + cone_outer_gain * x
952 }
953 }
954 }
955
956 fn dist_gain(&self, source_position: [f32; 3], listener_position: [f32; 3]) -> f32 {
957 let distance_model = self.distance_model;
958 let ref_distance = self.ref_distance;
959 let distance = crate::spatial::distance(source_position, listener_position) as f64;
960
961 let dist_gain = match distance_model {
962 DistanceModelType::Linear => {
963 let rolloff_factor = self.rolloff_factor.clamp(0., 1.);
964 let max_distance = self.max_distance;
965 let d2ref = ref_distance.min(max_distance);
966 let d2max = ref_distance.max(max_distance);
967 let d_clamped = distance.clamp(d2ref, d2max);
968 1. - rolloff_factor * (d_clamped - d2ref) / (d2max - d2ref)
969 }
970 DistanceModelType::Inverse => {
971 let rolloff_factor = self.rolloff_factor.max(0.);
972 if distance > 0. {
973 ref_distance
974 / (ref_distance
975 + rolloff_factor * (ref_distance.max(distance) - ref_distance))
976 } else {
977 1.
978 }
979 }
980 DistanceModelType::Exponential => {
981 let rolloff_factor = self.rolloff_factor.max(0.);
982 (distance.max(ref_distance) / ref_distance).powf(-rolloff_factor)
983 }
984 };
985 dist_gain as f32
986 }
987}
988
989fn apply_mono_to_stereo_gain(spatial_params: SpatialParams, l: &mut f32, r: &mut f32) {
990 let SpatialParams {
991 dist_gain,
992 cone_gain,
993 azimuth,
994 ..
995 } = spatial_params;
996
997 let mut azimuth = azimuth.clamp(-180., 180.);
999
1000 if azimuth < -90. {
1002 azimuth = -180. - azimuth;
1003 } else if azimuth > 90. {
1004 azimuth = 180. - azimuth;
1005 }
1006
1007 let x = (azimuth + 90.) / 180.;
1009 let gain_l = (x * PI / 2.).cos();
1010 let gain_r = (x * PI / 2.).sin();
1011
1012 *l *= gain_l * dist_gain * cone_gain;
1014 *r *= gain_r * dist_gain * cone_gain;
1015}
1016
1017fn apply_stereo_to_stereo_gain(
1018 spatial_params: SpatialParams,
1019 il: f32,
1020 ir: f32,
1021 ol: &mut f32,
1022 or: &mut f32,
1023) {
1024 let SpatialParams {
1025 dist_gain,
1026 cone_gain,
1027 azimuth,
1028 ..
1029 } = spatial_params;
1030
1031 let mut azimuth = azimuth.clamp(-180., 180.);
1033
1034 if azimuth < -90. {
1036 azimuth = -180. - azimuth;
1037 } else if azimuth > 90. {
1038 azimuth = 180. - azimuth;
1039 }
1040
1041 let x = if azimuth <= 0. {
1043 (azimuth + 90.) / 90.
1044 } else {
1045 azimuth / 90.
1046 };
1047 let gain_l = (x * PI / 2.).cos();
1048 let gain_r = (x * PI / 2.).sin();
1049
1050 if azimuth <= 0. {
1052 *ol = (il + ir * gain_l) * dist_gain * cone_gain;
1053 *or = ir * gain_r * dist_gain * cone_gain;
1054 } else {
1055 *ol = il * gain_l * dist_gain * cone_gain;
1056 *or = (ir + il * gain_r) * dist_gain * cone_gain;
1057 }
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use float_eq::{assert_float_eq, assert_float_ne};
1063
1064 use crate::context::{BaseAudioContext, OfflineAudioContext};
1065 use crate::node::{AudioBufferSourceNode, AudioBufferSourceOptions, AudioScheduledSourceNode};
1066 use crate::AudioBuffer;
1067
1068 use super::*;
1069
1070 #[test]
1071 fn test_audioparam_value_applies_immediately() {
1072 let context = OfflineAudioContext::new(1, 128, 48000.);
1073 let options = PannerOptions {
1074 position_x: 12.,
1075 ..Default::default()
1076 };
1077 let src = PannerNode::new(&context, options);
1078 assert_float_eq!(src.position_x.value(), 12., abs_all <= 0.);
1079 }
1080
1081 #[test]
1082 fn test_equal_power_mono_to_stereo() {
1083 let sample_rate = 44100.;
1084 let length = RENDER_QUANTUM_SIZE * 4;
1085 let mut context = OfflineAudioContext::new(2, length, sample_rate);
1086
1087 let input = AudioBuffer::from(vec![vec![1.; RENDER_QUANTUM_SIZE]], sample_rate);
1089 let mut src = AudioBufferSourceNode::new(&context, AudioBufferSourceOptions::default());
1090 src.set_buffer(input);
1091 src.start();
1092
1093 let options = PannerOptions {
1094 panning_model: PanningModelType::EqualPower,
1095 ..PannerOptions::default()
1096 };
1097 let panner = PannerNode::new(&context, options);
1098 assert_eq!(panner.panning_model(), PanningModelType::EqualPower);
1099 panner.set_channel_count(1);
1100 panner.position_x().set_value(1.); src.connect(&panner);
1103 panner.connect(&context.destination());
1104
1105 let output = context.start_rendering_sync();
1106 let original = vec![1.; RENDER_QUANTUM_SIZE];
1107 let zero = vec![0.; RENDER_QUANTUM_SIZE];
1108
1109 assert_float_eq!(
1111 output.get_channel_data(0)[..128],
1112 &zero[..],
1113 abs_all <= 1E-6
1114 );
1115 assert_float_eq!(
1116 output.get_channel_data(1)[..128],
1117 &original[..],
1118 abs_all <= 1E-6
1119 );
1120
1121 assert_float_eq!(
1123 output.get_channel_data(0)[128..256],
1124 &zero[..],
1125 abs_all <= 1E-6
1126 );
1127 assert_float_eq!(
1128 output.get_channel_data(1)[128..256],
1129 &zero[..],
1130 abs_all <= 1E-6
1131 );
1132 }
1133
1134 #[test]
1135 fn test_equal_power_azimuth_mono_to_stereo() {
1136 let sample_rate = 44100.;
1137 let length = RENDER_QUANTUM_SIZE;
1138 let mut context = OfflineAudioContext::new(2, length, sample_rate);
1139
1140 let input = AudioBuffer::from(vec![vec![1.; RENDER_QUANTUM_SIZE]], sample_rate);
1142 let mut src = AudioBufferSourceNode::new(&context, AudioBufferSourceOptions::default());
1143 src.set_buffer(input);
1144 src.start();
1145
1146 let options = PannerOptions {
1147 panning_model: PanningModelType::EqualPower,
1148 ..PannerOptions::default()
1149 };
1150 let panner = PannerNode::new(&context, options);
1151 assert_eq!(panner.panning_model(), PanningModelType::EqualPower);
1152 panner.position_y().set_value(1.); src.connect(&panner);
1155 panner.connect(&context.destination());
1156
1157 let output = context.start_rendering_sync();
1158 let sqrt2 = vec![(1.0f32 / 2.).sqrt(); RENDER_QUANTUM_SIZE];
1159
1160 assert_float_eq!(
1162 output.get_channel_data(0)[..128],
1163 &sqrt2[..],
1164 abs_all <= 1E-6
1165 );
1166 assert_float_eq!(
1167 output.get_channel_data(1)[..128],
1168 &sqrt2[..],
1169 abs_all <= 1E-6
1170 );
1171 }
1172
1173 #[test]
1174 fn test_equal_power_stereo_to_stereo() {
1175 let sample_rate = 44100.;
1176 let length = RENDER_QUANTUM_SIZE;
1177 let mut context = OfflineAudioContext::new(2, length, sample_rate);
1178
1179 let listener = context.listener();
1181 listener.position_x().set_value(10.);
1182 listener.position_y().set_value(0.);
1183 listener.position_z().set_value(0.);
1184 listener.forward_x().set_value(1.);
1185 listener.forward_y().set_value(0.);
1186 listener.forward_z().set_value(0.);
1187 listener.up_x().set_value(0.);
1188 listener.up_y().set_value(0.);
1189 listener.up_z().set_value(1.);
1190
1191 let input = AudioBuffer::from(
1193 vec![vec![1.; RENDER_QUANTUM_SIZE], vec![1.; RENDER_QUANTUM_SIZE]],
1194 sample_rate,
1195 );
1196 let mut src = AudioBufferSourceNode::new(&context, AudioBufferSourceOptions::default());
1197 src.set_buffer(input);
1198 src.start();
1199
1200 let panner = context.create_panner();
1202 panner.position_x().set_value(10.);
1203 panner.position_y().set_value(10.);
1204 panner.position_z().set_value(0.);
1205
1206 src.connect(&panner);
1207 panner.connect(&context.destination());
1208
1209 let output = context.start_rendering_sync();
1210
1211 assert_float_eq!(
1214 output.get_channel_data(0)[..RENDER_QUANTUM_SIZE],
1215 &[0.2; RENDER_QUANTUM_SIZE][..],
1216 abs_all <= 0.001
1217 );
1218 assert_float_eq!(
1220 output.get_channel_data(1)[..RENDER_QUANTUM_SIZE],
1221 &[0.; RENDER_QUANTUM_SIZE][..],
1222 abs_all <= 0.001
1223 );
1224 }
1225
1226 #[test]
1227 fn test_hrtf() {
1228 let sample_rate = 44100.;
1229 let length = RENDER_QUANTUM_SIZE * 4;
1230 let mut context = OfflineAudioContext::new(2, length, sample_rate);
1231
1232 let input = AudioBuffer::from(vec![vec![1.; RENDER_QUANTUM_SIZE]], sample_rate);
1234 let mut src = AudioBufferSourceNode::new(&context, AudioBufferSourceOptions::default());
1235 src.set_buffer(input);
1236 src.start();
1237
1238 let options = PannerOptions {
1239 panning_model: PanningModelType::HRTF,
1240 ..PannerOptions::default()
1241 };
1242 let panner = PannerNode::new(&context, options);
1243 assert_eq!(panner.panning_model(), PanningModelType::HRTF);
1244 panner.position_x().set_value(1.); src.connect(&panner);
1247 panner.connect(&context.destination());
1248
1249 let output = context.start_rendering_sync();
1250 let original = vec![1.; RENDER_QUANTUM_SIZE];
1251
1252 assert_float_ne!(
1254 output.get_channel_data(0)[..128],
1255 &original[..],
1256 abs_all <= 1E-6
1257 );
1258 assert_float_ne!(
1259 output.get_channel_data(1)[..128],
1260 &original[..],
1261 abs_all <= 1E-6
1262 );
1263
1264 let left = output.channel_data(0).as_slice();
1266 assert!(left[128..256].iter().any(|v| *v >= 1E-6));
1267
1268 let right = output.channel_data(1).as_slice();
1269 assert!(right[128..256].iter().any(|v| *v >= 1E-6));
1270 }
1271
1272 #[test]
1273 fn test_hrtf_loads_at_minimum_sample_rate() {
1274 let (_processor, len) = load_hrtf_processor(crate::MIN_SAMPLE_RATE as u32);
1275
1276 assert!(len > 0);
1277 assert!(
1278 len < 512,
1279 "minimum-rate HRTF should use the resampled HRIR length, got {len}"
1280 );
1281 }
1282
1283 #[test]
1284 fn test_hrtf_renders_at_minimum_sample_rate() {
1285 let sample_rate = crate::MIN_SAMPLE_RATE;
1286 let length = RENDER_QUANTUM_SIZE * 4;
1287 let mut context = OfflineAudioContext::new(2, length, sample_rate);
1288
1289 let input = AudioBuffer::from(vec![vec![1.; RENDER_QUANTUM_SIZE]], sample_rate);
1290 let mut src = AudioBufferSourceNode::new(&context, AudioBufferSourceOptions::default());
1291 src.set_buffer(input);
1292 src.start();
1293
1294 let options = PannerOptions {
1295 panning_model: PanningModelType::HRTF,
1296 ..PannerOptions::default()
1297 };
1298 let panner = PannerNode::new(&context, options);
1299 panner.position_x().set_value(1.);
1300
1301 src.connect(&panner);
1302 panner.connect(&context.destination());
1303
1304 let output = context.start_rendering_sync();
1305 let left = output.channel_data(0).as_slice();
1306 let right = output.channel_data(1).as_slice();
1307
1308 assert!(left.iter().all(|v| v.is_finite()));
1309 assert!(right.iter().all(|v| v.is_finite()));
1310 assert!(left.iter().any(|v| *v != 0.));
1311 assert!(right.iter().any(|v| *v != 0.));
1312 }
1313
1314 #[test]
1315 fn test_arate_position_automation_varies_within_quantum() {
1316 let mut context = OfflineAudioContext::new(2, 256, 48000.);
1324
1325 let mut src = context.create_constant_source();
1326 src.offset().set_value(1.);
1327 src.start();
1328
1329 let panner = context.create_panner();
1330 panner.position_x().set_value_at_time(-10., 0.);
1332 panner
1333 .position_x()
1334 .linear_ramp_to_value_at_time(10., 128. / 48000.);
1335
1336 src.connect(&panner);
1337 panner.connect(&context.destination());
1338
1339 let result = context.start_rendering_sync();
1340 let left = result.get_channel_data(0);
1341
1342 let varies = left.windows(2).take(127).any(|w| w[0] != w[1]);
1346 assert!(
1347 varies,
1348 "position automation was evaluated once per quantum (k-rate degradation)"
1349 );
1350 }
1351}