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