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