1use std::f32::consts::PI as PI32;
6use std::f64::consts::PI;
7
8use euclid::default::Vector3D;
9use malloc_size_of_derive::MallocSizeOf;
10use num_traits::{Float, Zero};
11
12use crate::audio_node::{AudioNodeEngine, AudioNodeMessage, AudioNodeType, BlockInfo, ChannelInfo};
13use crate::block::{Block, Chunk, FRAMES_PER_BLOCK, Tick};
14use crate::param::{Param, ParamDir, ParamType};
15
16pub fn normalize_zero<T: Float>(v: Vector3D<T>) -> Vector3D<T> {
18 let len = v.length();
19 if len.is_zero() { v } else { v / len }
20}
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq, MallocSizeOf)]
23pub enum PanningModel {
24 EqualPower,
25 HRTF,
26}
27
28#[derive(Copy, Clone, Debug, PartialEq, Eq, MallocSizeOf)]
29pub enum DistanceModel {
30 Linear,
31 Inverse,
32 Exponential,
33}
34
35#[derive(Copy, Clone, Debug, MallocSizeOf)]
36pub struct PannerNodeOptions {
37 pub panning_model: PanningModel,
38 pub distance_model: DistanceModel,
39 pub position_x: f32,
40 pub position_y: f32,
41 pub position_z: f32,
42 pub orientation_x: f32,
43 pub orientation_y: f32,
44 pub orientation_z: f32,
45 pub ref_distance: f64,
46 pub max_distance: f64,
47 pub rolloff_factor: f64,
48 pub cone_inner_angle: f64,
49 pub cone_outer_angle: f64,
50 pub cone_outer_gain: f64,
51}
52
53#[derive(MallocSizeOf)]
54pub enum PannerNodeMessage {
55 SetPanningModel(PanningModel),
56 SetDistanceModel(DistanceModel),
57 SetRefDistance(f64),
58 SetMaxDistance(f64),
59 SetRolloff(f64),
60 SetConeInner(f64),
61 SetConeOuter(f64),
62 SetConeGain(f64),
63}
64
65impl Default for PannerNodeOptions {
66 fn default() -> Self {
67 PannerNodeOptions {
68 panning_model: PanningModel::EqualPower,
69 distance_model: DistanceModel::Inverse,
70 position_x: 0.,
71 position_y: 0.,
72 position_z: 0.,
73 orientation_x: 1.,
74 orientation_y: 0.,
75 orientation_z: 0.,
76 ref_distance: 1.,
77 max_distance: 10000.,
78 rolloff_factor: 1.,
79 cone_inner_angle: 360.,
80 cone_outer_angle: 360.,
81 cone_outer_gain: 0.,
82 }
83 }
84}
85
86#[derive(AudioNodeCommon)]
87pub(crate) struct PannerNode {
88 channel_info: ChannelInfo,
89 panning_model: PanningModel,
90 distance_model: DistanceModel,
91 position_x: Param,
92 position_y: Param,
93 position_z: Param,
94 orientation_x: Param,
95 orientation_y: Param,
96 orientation_z: Param,
97 ref_distance: f64,
98 max_distance: f64,
99 rolloff_factor: f64,
100 cone_inner_angle: f64,
101 cone_outer_angle: f64,
102 cone_outer_gain: f64,
103 listener_data: Option<Block>,
104}
105
106impl PannerNode {
107 pub fn new(options: PannerNodeOptions, channel_info: ChannelInfo) -> Self {
108 if options.panning_model == PanningModel::HRTF {
109 log::warn!("HRTF requested but not supported")
110 }
111 Self {
112 channel_info,
113 panning_model: options.panning_model,
114 distance_model: options.distance_model,
115 position_x: Param::new(options.position_x),
116 position_y: Param::new(options.position_y),
117 position_z: Param::new(options.position_z),
118 orientation_x: Param::new(options.orientation_x),
119 orientation_y: Param::new(options.orientation_y),
120 orientation_z: Param::new(options.orientation_z),
121 ref_distance: options.ref_distance,
122 max_distance: options.max_distance,
123 rolloff_factor: options.rolloff_factor,
124 cone_inner_angle: options.cone_inner_angle,
125 cone_outer_angle: options.cone_outer_angle,
126 cone_outer_gain: options.cone_outer_gain,
127 listener_data: None,
128 }
129 }
130
131 pub fn update_parameters(&mut self, info: &BlockInfo, tick: Tick) -> bool {
132 let mut changed = self.position_x.update(info, tick);
133 changed |= self.position_y.update(info, tick);
134 changed |= self.position_z.update(info, tick);
135 changed |= self.orientation_x.update(info, tick);
136 changed |= self.orientation_y.update(info, tick);
137 changed |= self.orientation_z.update(info, tick);
138 changed
139 }
140
141 fn azimuth_elevation_distance(
148 &self,
149 listener: (Vector3D<f32>, Vector3D<f32>, Vector3D<f32>),
150 ) -> (f64, f64, f64) {
151 let (listener_position, listener_forward, listener_up) = listener;
152 let listener_position = listener_position.to_f64();
153 let listener_forward = listener_forward.to_f64();
154 let listener_up = listener_up.to_f64();
155 let source_position = Vector3D::new(
156 self.position_x.value(),
157 self.position_y.value(),
158 self.position_z.value(),
159 )
160 .to_f64();
161 let diff = source_position - listener_position;
162 let source_listener = normalize_zero(diff);
163 let distance = diff.length();
164
165 if source_listener.length().is_zero() {
167 return (0., 0., distance);
168 }
169
170 let listener_right = listener_forward.cross(listener_up);
171
172 if listener_right.length().is_zero() {
173 return (0., 0., distance);
176 }
177
178 let listener_right_norm = normalize_zero(listener_right);
179 let listener_forward_norm = normalize_zero(listener_forward);
180
181 let up = listener_right_norm.cross(listener_forward_norm);
182
183 let up_projection = source_listener.dot(up);
184 let projected_source = normalize_zero(source_listener - up * up_projection);
185 let mut azimuth = 180. * projected_source.dot(listener_right_norm).acos() / PI;
186
187 let front_back = projected_source.dot(listener_forward_norm);
188 if front_back < 0. {
189 azimuth = 360. - azimuth;
190 }
191 if (0. ..=270.).contains(&azimuth) {
192 azimuth = 90. - azimuth;
193 } else {
194 azimuth = 450. - azimuth;
195 }
196
197 let mut elevation = 90. - 180. * source_listener.dot(up).acos() / PI;
198
199 if elevation > 90. {
200 elevation = 180. - elevation;
201 } else if elevation < -90. {
202 elevation = -180. - elevation;
203 }
204
205 (azimuth, elevation, distance)
206 }
207
208 fn cone_gain(&self, listener: (Vector3D<f32>, Vector3D<f32>, Vector3D<f32>)) -> f64 {
210 let (listener_position, _, _) = listener;
211 let source_position = Vector3D::new(
212 self.position_x.value(),
213 self.position_y.value(),
214 self.position_z.value(),
215 );
216 let source_orientation = Vector3D::new(
217 self.orientation_x.value(),
218 self.orientation_y.value(),
219 self.orientation_z.value(),
220 );
221
222 if source_orientation.length().is_zero() ||
223 (self.cone_inner_angle == 360. && self.cone_outer_angle == 360.)
224 {
225 return 1.;
226 }
227
228 let normalized_source_orientation = normalize_zero(source_orientation);
229
230 let source_to_listener = normalize_zero(source_position - listener_position);
231 let angle = 180. * source_to_listener.dot(normalized_source_orientation).acos() / PI32;
233 let abs_angle = angle.abs() as f64;
234
235 let abs_inner_angle = self.cone_inner_angle.abs() / 2.;
237 let abs_outer_angle = self.cone_outer_angle.abs() / 2.;
238
239 if abs_angle < abs_inner_angle {
240 1.
242 } else if abs_angle >= abs_outer_angle {
243 self.cone_outer_gain
245 } else {
246 let x = (abs_angle - abs_inner_angle) / (abs_outer_angle - abs_inner_angle);
249 (1. - x) + self.cone_outer_gain * x
250 }
251 }
252
253 fn linear_distance(&self, mut distance: f64, rolloff_factor: f64) -> f64 {
254 if distance > self.max_distance {
255 distance = self.max_distance;
256 }
257 if distance < self.ref_distance {
258 distance = self.ref_distance;
259 }
260 let denom = self.max_distance - self.ref_distance;
261 1. - rolloff_factor * (distance - self.ref_distance) / denom
262 }
263
264 fn inverse_distance(&self, mut distance: f64, rolloff_factor: f64) -> f64 {
265 if distance < self.ref_distance {
266 distance = self.ref_distance;
267 }
268 let denom = self.ref_distance + rolloff_factor * (distance - self.ref_distance);
269 self.ref_distance / denom
270 }
271
272 fn exponential_distance(&self, mut distance: f64, rolloff_factor: f64) -> f64 {
273 if distance < self.ref_distance {
274 distance = self.ref_distance;
275 }
276
277 (distance / self.ref_distance).powf(-rolloff_factor)
278 }
279
280 fn distance_gain_fn(&self) -> fn(&Self, f64, f64) -> f64 {
281 match self.distance_model {
282 DistanceModel::Linear => |x, d, r| x.linear_distance(d, r),
283 DistanceModel::Inverse => |x, d, r| x.inverse_distance(d, r),
284 DistanceModel::Exponential => |x, d, r| x.exponential_distance(d, r),
285 }
286 }
287}
288
289impl AudioNodeEngine for PannerNode {
290 fn node_type(&self) -> AudioNodeType {
291 AudioNodeType::PannerNode
292 }
293
294 fn process(&mut self, mut inputs: Chunk, info: &BlockInfo) -> Chunk {
295 debug_assert!(inputs.len() == 1);
296
297 let listener_data = if let Some(listener_data) = self.listener_data.take() {
298 listener_data
299 } else {
300 return inputs;
301 };
302
303 let rolloff_factor =
305 if self.distance_model == DistanceModel::Linear && self.rolloff_factor > 1. {
306 1.
307 } else {
308 self.rolloff_factor
309 };
310
311 {
312 let block = &mut inputs.blocks[0];
313
314 block.explicit_repeat();
315
316 let mono = if block.chan_count() == 1 {
317 block.resize_silence(2);
318 true
319 } else {
320 debug_assert!(block.chan_count() == 2);
321 false
322 };
323
324 let distance_gain_fn = self.distance_gain_fn();
325
326 if self.panning_model == PanningModel::EqualPower {
327 let (l, r) = block.data_mut().split_at_mut(FRAMES_PER_BLOCK.0 as usize);
328 for frame in 0..FRAMES_PER_BLOCK.0 {
329 let frame = Tick(frame);
330 self.update_parameters(info, frame);
331 let data = listener_data.listener_data(frame);
332 let (mut azimuth, _elev, dist) = self.azimuth_elevation_distance(data);
333 let distance_gain = distance_gain_fn(self, dist, rolloff_factor);
334 let cone_gain = self.cone_gain(data);
335
336 azimuth = azimuth.clamp(-180., 180.);
340 if azimuth < -90. {
341 azimuth = -180. - azimuth;
342 } else if azimuth > 90. {
343 azimuth = 180. - azimuth;
344 }
345
346 let x = if mono {
347 (azimuth + 90.) / 180.
348 } else if azimuth <= 0. {
349 (azimuth + 90.) / 90.
350 } else {
351 azimuth / 90.
352 };
353 let x = x * PI / 2.;
354
355 let mut gain_l = x.cos();
356 let mut gain_r = x.sin();
357 if gain_l <= 0. {
359 gain_l = 0.
360 }
361 if gain_r <= 0. {
362 gain_r = 0.;
363 }
364
365 let index = frame.0 as usize;
366 if mono {
367 let input = l[index];
368 l[index] = input * gain_l as f32;
369 r[index] = input * gain_r as f32;
370 } else if azimuth <= 0. {
371 l[index] += r[index] * gain_l as f32;
372 r[index] *= gain_r as f32;
373 } else {
374 r[index] += l[index] * gain_r as f32;
375 l[index] *= gain_l as f32;
376 }
377 l[index] = l[index] * distance_gain as f32 * cone_gain as f32;
378 r[index] = r[index] * distance_gain as f32 * cone_gain as f32;
379 }
380 }
381 }
382
383 inputs
384 }
385
386 fn input_count(&self) -> u32 {
387 1
388 }
389
390 fn get_param(&mut self, id: ParamType) -> &mut Param {
391 match id {
392 ParamType::Position(ParamDir::X) => &mut self.position_x,
393 ParamType::Position(ParamDir::Y) => &mut self.position_y,
394 ParamType::Position(ParamDir::Z) => &mut self.position_z,
395 ParamType::Orientation(ParamDir::X) => &mut self.orientation_x,
396 ParamType::Orientation(ParamDir::Y) => &mut self.orientation_y,
397 ParamType::Orientation(ParamDir::Z) => &mut self.orientation_z,
398 _ => panic!("Unknown param {:?} for PannerNode", id),
399 }
400 }
401
402 fn set_listenerdata(&mut self, data: Block) {
403 self.listener_data = Some(data);
404 }
405
406 fn message_specific(&mut self, message: AudioNodeMessage, _sample_rate: f32) {
407 if let AudioNodeMessage::PannerNode(p) = message {
408 match p {
409 PannerNodeMessage::SetPanningModel(p) => {
410 if p == PanningModel::HRTF {
411 log::warn!("HRTF requested but not supported");
412 }
413 self.panning_model = p;
414 },
415 PannerNodeMessage::SetDistanceModel(d) => self.distance_model = d,
416 PannerNodeMessage::SetRefDistance(val) => self.ref_distance = val,
417 PannerNodeMessage::SetMaxDistance(val) => self.max_distance = val,
418 PannerNodeMessage::SetRolloff(val) => self.rolloff_factor = val,
419 PannerNodeMessage::SetConeInner(val) => self.cone_inner_angle = val,
420 PannerNodeMessage::SetConeOuter(val) => self.cone_outer_angle = val,
421 PannerNodeMessage::SetConeGain(val) => self.cone_outer_gain = val,
422 }
423 }
424 }
425}