ringkernel_wavesim3d/audio/
binaural.rs1use crate::simulation::grid3d::SimulationGrid3D;
10use crate::simulation::physics::{constants, Orientation3D, Position3D};
11use std::collections::VecDeque;
12
13#[derive(Debug, Clone)]
15pub struct VirtualHead {
16 pub position: Position3D,
18 pub orientation: Orientation3D,
20 pub ear_spacing: f32,
22 pub head_radius: f32,
24}
25
26impl Default for VirtualHead {
27 fn default() -> Self {
28 Self {
29 position: Position3D::origin(),
30 orientation: Orientation3D::default(),
31 ear_spacing: constants::EAR_SPACING,
32 head_radius: 0.0875, }
34 }
35}
36
37impl VirtualHead {
38 pub fn new(position: Position3D) -> Self {
40 Self {
41 position,
42 ..Default::default()
43 }
44 }
45
46 pub fn with_ear_spacing(mut self, spacing: f32) -> Self {
48 self.ear_spacing = spacing.max(0.05);
49 self
50 }
51
52 pub fn with_orientation(mut self, orientation: Orientation3D) -> Self {
54 self.orientation = orientation;
55 self
56 }
57
58 pub fn set_position(&mut self, pos: Position3D) {
60 self.position = pos;
61 }
62
63 pub fn set_orientation(&mut self, orientation: Orientation3D) {
65 self.orientation = orientation;
66 }
67
68 pub fn left_ear_position(&self) -> Position3D {
70 let (rx, ry, rz) = self.orientation.right();
71 let half_spacing = self.ear_spacing / 2.0;
72 Position3D::new(
73 self.position.x - rx * half_spacing,
74 self.position.y - ry * half_spacing,
75 self.position.z - rz * half_spacing,
76 )
77 }
78
79 pub fn right_ear_position(&self) -> Position3D {
81 let (rx, ry, rz) = self.orientation.right();
82 let half_spacing = self.ear_spacing / 2.0;
83 Position3D::new(
84 self.position.x + rx * half_spacing,
85 self.position.y + ry * half_spacing,
86 self.position.z + rz * half_spacing,
87 )
88 }
89
90 pub fn angle_to_source(&self, source: Position3D) -> (f32, f32) {
92 let dx = source.x - self.position.x;
94 let dy = source.y - self.position.y;
95 let dz = source.z - self.position.z;
96
97 let (fx, fy, fz) = self.orientation.forward();
99 let (rx, ry, rz) = self.orientation.right();
100
101 let forward_comp = dx * fx + dy * fy + dz * fz;
103 let right_comp = dx * rx + dy * ry + dz * rz;
104 let azimuth = right_comp.atan2(forward_comp);
105
106 let horizontal_dist = (dx * dx + dz * dz).sqrt();
108 let elevation = dy.atan2(horizontal_dist);
109
110 (azimuth, elevation)
111 }
112
113 pub fn calculate_itd(&self, source: Position3D, speed_of_sound: f32) -> f32 {
117 let left_dist = self.left_ear_position().distance_to(&source);
118 let right_dist = self.right_ear_position().distance_to(&source);
119
120 (left_dist - right_dist) / speed_of_sound
122 }
123
124 pub fn calculate_ild(&self, source: Position3D) -> (f32, f32) {
128 let (azimuth, _elevation) = self.angle_to_source(source);
129
130 let shadow_factor = 0.4; let left_gain;
137 let right_gain;
138
139 if azimuth >= 0.0 {
140 right_gain = 1.0;
142 left_gain = 1.0 - shadow_factor * (azimuth / std::f32::consts::FRAC_PI_2).min(1.0);
143 } else {
144 left_gain = 1.0;
146 right_gain = 1.0 - shadow_factor * ((-azimuth) / std::f32::consts::FRAC_PI_2).min(1.0);
147 }
148
149 (left_gain, right_gain)
150 }
151}
152
153pub struct BinauralMicrophone {
155 pub head: VirtualHead,
157 pub sample_rate: u32,
159 left_buffer: VecDeque<f32>,
161 right_buffer: VecDeque<f32>,
163 buffer_size: usize,
165 interp_factor: f32,
167 accumulated_left: Vec<f32>,
169 accumulated_right: Vec<f32>,
170}
171
172impl BinauralMicrophone {
173 pub fn new(head: VirtualHead, sample_rate: u32, simulation_dt: f32) -> Self {
180 let buffer_size = sample_rate as usize * 2; let audio_period = 1.0 / sample_rate as f32;
184 let interp_factor = audio_period / simulation_dt;
185
186 Self {
187 head,
188 sample_rate,
189 left_buffer: VecDeque::with_capacity(buffer_size),
190 right_buffer: VecDeque::with_capacity(buffer_size),
191 buffer_size,
192 interp_factor,
193 accumulated_left: Vec::new(),
194 accumulated_right: Vec::new(),
195 }
196 }
197
198 pub fn capture(&mut self, grid: &SimulationGrid3D) {
202 let left_pos = self.head.left_ear_position();
203 let right_pos = self.head.right_ear_position();
204
205 let left_pressure = grid.sample_pressure(left_pos);
207 let right_pressure = grid.sample_pressure(right_pos);
208
209 self.accumulated_left.push(left_pressure);
211 self.accumulated_right.push(right_pressure);
212
213 let min_remove = self.interp_factor.max(1.0).ceil() as usize;
216 while self.accumulated_left.len() >= min_remove {
217 let n = min_remove;
218
219 let left_sample: f32 = self.accumulated_left.iter().take(n).sum::<f32>() / n as f32;
221 let right_sample: f32 = self.accumulated_right.iter().take(n).sum::<f32>() / n as f32;
222
223 if self.left_buffer.len() < self.buffer_size {
225 self.left_buffer.push_back(left_sample);
226 self.right_buffer.push_back(right_sample);
227 }
228
229 let to_remove = min_remove.max(1);
231 self.accumulated_left
232 .drain(0..to_remove.min(self.accumulated_left.len()));
233 self.accumulated_right
234 .drain(0..to_remove.min(self.accumulated_right.len()));
235 }
236 }
237
238 pub fn get_samples(&mut self, count: usize) -> (Vec<f32>, Vec<f32>) {
242 let count = count.min(self.left_buffer.len());
243 let left: Vec<f32> = self.left_buffer.drain(0..count).collect();
244 let right: Vec<f32> = self.right_buffer.drain(0..count).collect();
245 (left, right)
246 }
247
248 pub fn get_interleaved(&mut self, frames: usize) -> Vec<f32> {
250 let frames = frames.min(self.left_buffer.len());
251 let mut output = Vec::with_capacity(frames * 2);
252
253 for _ in 0..frames {
254 if let (Some(left), Some(right)) =
255 (self.left_buffer.pop_front(), self.right_buffer.pop_front())
256 {
257 output.push(left);
258 output.push(right);
259 }
260 }
261
262 output
263 }
264
265 pub fn available_samples(&self) -> usize {
267 self.left_buffer.len()
268 }
269
270 pub fn clear(&mut self) {
272 self.left_buffer.clear();
273 self.right_buffer.clear();
274 self.accumulated_left.clear();
275 self.accumulated_right.clear();
276 }
277
278 pub fn set_head_position(&mut self, pos: Position3D) {
280 self.head.set_position(pos);
281 }
282
283 pub fn set_head_orientation(&mut self, orientation: Orientation3D) {
285 self.head.set_orientation(orientation);
286 }
287}
288
289#[cfg(feature = "audio-output")]
291pub struct AudioOutput {
292 stream: Option<cpal::Stream>,
293 sample_rate: u32,
294}
295
296pub struct DelayLine {
298 buffer: VecDeque<f32>,
299 max_delay_samples: usize,
300}
301
302impl DelayLine {
303 pub fn new(max_delay_seconds: f32, sample_rate: u32) -> Self {
305 let max_delay_samples = (max_delay_seconds * sample_rate as f32).ceil() as usize;
306 Self {
307 buffer: VecDeque::from(vec![0.0; max_delay_samples]),
308 max_delay_samples,
309 }
310 }
311
312 pub fn process(&mut self, input: f32, delay_samples: f32) -> f32 {
314 self.buffer.push_back(input);
316
317 let delay_int = delay_samples.floor() as usize;
319 let delay_frac = delay_samples - delay_int as f32;
320
321 let idx1 = self.buffer.len().saturating_sub(1 + delay_int);
322 let idx2 = idx1.saturating_sub(1);
323
324 let s1 = self.buffer.get(idx1).copied().unwrap_or(0.0);
325 let s2 = self.buffer.get(idx2).copied().unwrap_or(0.0);
326
327 while self.buffer.len() > self.max_delay_samples {
329 self.buffer.pop_front();
330 }
331
332 s1 * (1.0 - delay_frac) + s2 * delay_frac
334 }
335
336 pub fn clear(&mut self) {
338 self.buffer.clear();
339 self.buffer.resize(self.max_delay_samples, 0.0);
340 }
341}
342
343pub struct BinauralProcessor {
347 left_delay: DelayLine,
349 right_delay: DelayLine,
351 sample_rate: u32,
353 speed_of_sound: f32,
355}
356
357impl BinauralProcessor {
358 pub fn new(sample_rate: u32, speed_of_sound: f32) -> Self {
360 let max_delay = 0.001;
362
363 Self {
364 left_delay: DelayLine::new(max_delay, sample_rate),
365 right_delay: DelayLine::new(max_delay, sample_rate),
366 sample_rate,
367 speed_of_sound,
368 }
369 }
370
371 pub fn process(&mut self, input: f32, head: &VirtualHead, source: Position3D) -> (f32, f32) {
373 let itd = head.calculate_itd(source, self.speed_of_sound);
375 let itd_samples = itd * self.sample_rate as f32;
376
377 let (left_gain, right_gain) = head.calculate_ild(source);
379
380 let (left_delay, right_delay) = if itd_samples >= 0.0 {
382 (itd_samples, 0.0)
384 } else {
385 (0.0, -itd_samples)
387 };
388
389 let left = self.left_delay.process(input * left_gain, left_delay);
390 let right = self.right_delay.process(input * right_gain, right_delay);
391
392 (left, right)
393 }
394
395 pub fn clear(&mut self) {
397 self.left_delay.clear();
398 self.right_delay.clear();
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_virtual_head_ear_positions() {
408 let head = VirtualHead::new(Position3D::new(0.0, 0.0, 0.0));
409
410 let left = head.left_ear_position();
411 let right = head.right_ear_position();
412
413 assert!((left.x + right.x).abs() < 0.001);
415
416 let distance = left.distance_to(&right);
418 assert!((distance - head.ear_spacing).abs() < 0.001);
419 }
420
421 #[test]
422 fn test_itd_calculation() {
423 let head = VirtualHead::new(Position3D::new(0.0, 0.0, 0.0));
424 let speed = 343.0;
425
426 let source_right = Position3D::new(10.0, 0.0, 0.0);
428 let itd_right = head.calculate_itd(source_right, speed);
429
430 let source_left = Position3D::new(-10.0, 0.0, 0.0);
432 let itd_left = head.calculate_itd(source_left, speed);
433
434 assert!(itd_right > 0.0);
436 assert!(itd_left < 0.0);
437 assert!((itd_right + itd_left).abs() < 0.001);
438 }
439
440 #[test]
441 fn test_ild_calculation() {
442 let head = VirtualHead::new(Position3D::new(0.0, 0.0, 0.0));
443
444 let source_front = Position3D::new(0.0, 0.0, 10.0);
446 let (left_front, right_front) = head.calculate_ild(source_front);
447
448 assert!((left_front - right_front).abs() < 0.1);
450
451 let source_right = Position3D::new(10.0, 0.0, 0.0);
453 let (left_right, right_right) = head.calculate_ild(source_right);
454
455 assert!(right_right > left_right);
457 }
458
459 #[test]
460 fn test_delay_line() {
461 let mut delay = DelayLine::new(0.01, 44100);
462
463 delay.process(1.0, 0.0);
465 for _ in 0..100 {
466 delay.process(0.0, 0.0);
467 }
468
469 let output = delay.process(0.0, 50.0);
471 assert!(output.abs() < 0.1);
473 }
474
475 #[test]
476 fn test_binaural_processor() {
477 let mut processor = BinauralProcessor::new(44100, 343.0);
478 let head = VirtualHead::new(Position3D::new(0.0, 0.0, 0.0));
479
480 let source = Position3D::new(5.0, 0.0, 0.0);
482
483 let mut left_energy = 0.0;
485 let mut right_energy = 0.0;
486
487 for i in 0..100 {
488 let input = if i < 10 { 1.0 } else { 0.0 };
489 let (left, right) = processor.process(input, &head, source);
490 left_energy += left * left;
491 right_energy += right * right;
492 }
493
494 assert!(
496 right_energy > left_energy,
497 "Right energy {} should be > left {}",
498 right_energy,
499 left_energy
500 );
501 }
502
503 #[test]
504 fn test_head_rotation() {
505 let mut head = VirtualHead::new(Position3D::new(0.0, 0.0, 0.0));
506
507 let source = Position3D::new(5.0, 0.0, 0.0);
509 let (left1, right1) = head.calculate_ild(source);
510
511 head.set_orientation(Orientation3D::new(std::f32::consts::FRAC_PI_2, 0.0, 0.0));
513 let (left2, right2) = head.calculate_ild(source);
514
515 let diff1 = (right1 - left1).abs();
517 let diff2 = (right2 - left2).abs();
518 assert!(
519 diff2 < diff1,
520 "After rotation, ILD diff {} should be < {}",
521 diff2,
522 diff1
523 );
524 }
525}