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