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