1pub mod adaptive_acoustics;
4pub mod simulation;
5
6use crate::types::Position3D;
7use scirs2_core::ndarray::{Array1, Array2};
8use serde::{Deserialize, Serialize};
9pub use simulation::{
10 AcousticResponse, AdvancedRoomSimulator, DiffractionProcessor, DynamicEnvironmentManager,
11 FrequencyDependentProperty, Material as SimMaterial, MaterialDatabase, RayTracingEngine,
12 RoomGeometry, RoomSimulationConfig,
13};
14use std::collections::{HashMap, VecDeque};
15
16#[derive(Debug, Clone)]
18pub struct RoomSimulator {
19 config: RoomConfig,
21 reverb_processor: ReverbProcessor,
23 early_reflections: EarlyReflectionProcessor,
25 late_reverb: LateReverbProcessor,
27 room_ir: Option<RoomImpulseResponse>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RoomConfig {
34 pub dimensions: (f32, f32, f32),
36 pub wall_materials: WallMaterials,
38 pub reverb_time: f32,
40 pub volume: f32,
42 pub surface_area: f32,
44 pub temperature: f32,
46 pub humidity: f32,
48 pub enable_air_absorption: bool,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct WallMaterials {
55 pub floor: Material,
57 pub ceiling: Material,
59 pub left_wall: Material,
61 pub right_wall: Material,
63 pub front_wall: Material,
65 pub back_wall: Material,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Material {
72 pub name: String,
74 pub absorption_coefficients: Vec<FrequencyBandAbsorption>,
76 pub scattering_coefficient: f32,
78 pub transmission_coefficient: f32,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct FrequencyBandAbsorption {
85 pub frequency: f32,
87 pub coefficient: f32,
89}
90
91#[derive(Debug, Clone)]
93pub struct RoomImpulseResponse {
94 pub early_reflections: Array1<f32>,
96 pub late_reverb: Array1<f32>,
98 pub combined_ir: Array1<f32>,
100 pub sample_rate: u32,
102}
103
104#[derive(Debug, Clone)]
106pub struct EarlyReflectionProcessor {
107 #[allow(dead_code)]
109 reflection_paths: Vec<ReflectionPath>,
110 #[allow(dead_code)]
112 max_order: usize,
113 #[allow(dead_code)]
115 speed_of_sound: f32,
116 sample_rate: f32,
118}
119
120#[derive(Debug, Clone)]
122pub struct LateReverbProcessor {
123 #[allow(dead_code)]
125 feedback_networks: Vec<FeedbackDelayNetwork>,
126 #[allow(dead_code)]
128 diffusion_filters: Vec<AllPassFilter>,
129 reverb_time: f32,
131 #[allow(dead_code)]
133 diffusion: f32,
134}
135
136#[derive(Debug, Clone)]
138pub struct ReverbProcessor {
139 #[allow(dead_code)]
141 early_processor: EarlyReflectionProcessor,
142 #[allow(dead_code)]
144 late_processor: LateReverbProcessor,
145 #[allow(dead_code)]
147 crossover_frequency: f32,
148 #[allow(dead_code)]
150 dry_level: f32,
151 #[allow(dead_code)]
152 early_level: f32,
153 #[allow(dead_code)]
154 late_level: f32,
155}
156
157#[derive(Debug, Clone)]
159pub struct ReflectionPath {
160 pub path: Vec<Position3D>,
162 pub length: f32,
164 pub delay_samples: usize,
166 pub attenuation: f32,
168 pub surfaces: Vec<SurfaceReflection>,
170}
171
172#[derive(Debug, Clone)]
174pub struct SurfaceReflection {
175 pub position: Position3D,
177 pub normal: Position3D,
179 pub material: Material,
181 pub incident_angle: f32,
183}
184
185#[derive(Debug, Clone)]
187pub struct FeedbackDelayNetwork {
188 #[allow(dead_code)]
190 delay_lines: Vec<DelayLine>,
191 #[allow(dead_code)]
193 feedback_matrix: Array2<f32>,
194 #[allow(dead_code)]
196 input_gains: Array1<f32>,
197 #[allow(dead_code)]
199 output_gains: Array1<f32>,
200}
201
202#[derive(Debug, Clone)]
204pub struct AllPassFilter {
205 #[allow(dead_code)]
207 delay_line: DelayLine,
208 #[allow(dead_code)]
210 feedback: f32,
211 #[allow(dead_code)]
213 feedforward: f32,
214}
215
216#[derive(Debug, Clone)]
218pub struct DelayLine {
219 buffer: VecDeque<f32>,
221 delay_samples: f32,
223 max_delay: usize,
225}
226
227#[derive(Debug, Clone)]
229pub struct Wall {
230 pub normal: Position3D,
232 pub point: Position3D,
234 pub material: Material,
236}
237
238impl RoomSimulator {
239 pub fn new(dimensions: (f32, f32, f32), reverb_time: f32) -> crate::Result<Self> {
241 let config = RoomConfig::new(dimensions, reverb_time);
242 let reverb_processor = ReverbProcessor::new(&config)?;
243 let early_reflections = EarlyReflectionProcessor::new(&config)?;
244 let late_reverb = LateReverbProcessor::new(&config)?;
245
246 Ok(Self {
247 config,
248 reverb_processor,
249 early_reflections,
250 late_reverb,
251 room_ir: None,
252 })
253 }
254
255 pub fn with_config(config: RoomConfig) -> crate::Result<Self> {
257 let reverb_processor = ReverbProcessor::new(&config)?;
258 let early_reflections = EarlyReflectionProcessor::new(&config)?;
259 let late_reverb = LateReverbProcessor::new(&config)?;
260
261 Ok(Self {
262 config,
263 reverb_processor,
264 early_reflections,
265 late_reverb,
266 room_ir: None,
267 })
268 }
269
270 pub async fn process_reverb(
272 &self,
273 left_channel: &mut Array1<f32>,
274 right_channel: &mut Array1<f32>,
275 source_position: &Position3D,
276 ) -> crate::Result<()> {
277 self.early_reflections
279 .process(left_channel, right_channel, source_position)
280 .await?;
281
282 self.late_reverb
284 .process(left_channel, right_channel)
285 .await?;
286
287 Ok(())
288 }
289
290 pub fn calculate_room_ir(
292 &mut self,
293 source_position: Position3D,
294 listener_position: Position3D,
295 sample_rate: u32,
296 ) -> crate::Result<RoomImpulseResponse> {
297 let ir_length = (self.config.reverb_time * sample_rate as f32) as usize;
298 let mut early_reflections = Array1::zeros(ir_length);
299 let mut late_reverb = Array1::zeros(ir_length);
300
301 let reflection_paths =
303 self.calculate_reflection_paths(source_position, listener_position, 3)?;
304
305 for path in &reflection_paths {
306 if path.delay_samples < early_reflections.len() {
307 early_reflections[path.delay_samples] += path.attenuation;
308 }
309 }
310
311 self.generate_late_reverb(&mut late_reverb, sample_rate)?;
313
314 let mut combined_ir = Array1::zeros(ir_length);
316 for i in 0..ir_length {
317 combined_ir[i] = early_reflections[i] + late_reverb[i];
318 }
319
320 let room_ir = RoomImpulseResponse {
321 early_reflections,
322 late_reverb,
323 combined_ir,
324 sample_rate,
325 };
326
327 self.room_ir = Some(room_ir.clone());
328 Ok(room_ir)
329 }
330
331 fn calculate_reflection_paths(
333 &self,
334 source: Position3D,
335 listener: Position3D,
336 max_order: usize,
337 ) -> crate::Result<Vec<ReflectionPath>> {
338 let mut paths = Vec::new();
339 let (width, height, depth) = self.config.dimensions;
340
341 let direct_path = ReflectionPath {
343 path: vec![source, listener],
344 length: source.distance_to(&listener),
345 delay_samples: (source.distance_to(&listener) / 343.0 * 44100.0) as usize,
346 attenuation: 1.0 / (1.0 + source.distance_to(&listener)),
347 surfaces: Vec::new(),
348 };
349 paths.push(direct_path);
350
351 self.calculate_ray_traced_paths(&mut paths, source, listener, max_order)?;
353
354 Ok(paths)
355 }
356
357 fn calculate_ray_traced_paths(
359 &self,
360 paths: &mut Vec<ReflectionPath>,
361 source: Position3D,
362 listener: Position3D,
363 max_order: usize,
364 ) -> crate::Result<()> {
365 let (width, height, depth) = self.config.dimensions;
366
367 if max_order >= 1 {
369 self.add_wall_reflections(paths, source, listener, width, height, depth);
370 }
371
372 if max_order >= 2 {
374 self.add_ray_traced_reflections(paths, source, listener, max_order)?;
375 }
376
377 Ok(())
378 }
379
380 fn add_ray_traced_reflections(
382 &self,
383 paths: &mut Vec<ReflectionPath>,
384 source: Position3D,
385 listener: Position3D,
386 max_order: usize,
387 ) -> crate::Result<()> {
388 let walls = self.get_room_walls(
389 self.config.dimensions.0,
390 self.config.dimensions.1,
391 self.config.dimensions.2,
392 );
393
394 let to_listener = Position3D::new(
396 listener.x - source.x,
397 listener.y - source.y,
398 listener.z - source.z,
399 )
400 .normalized();
401
402 let base_directions = vec![
404 to_listener,
405 Position3D::new(to_listener.x, -to_listener.y, to_listener.z).normalized(),
406 Position3D::new(-to_listener.x, to_listener.y, to_listener.z).normalized(),
407 Position3D::new(to_listener.x, to_listener.y, -to_listener.z).normalized(),
408 ];
409
410 for base_direction in base_directions {
411 let mut current_pos = source;
412 let mut ray_direction = base_direction;
413 let mut current_path = vec![source];
414 let mut total_attenuation = 1.0;
415 let mut total_length = 0.0;
416 let mut surface_reflections = Vec::new();
417
418 for order in 1..=max_order {
419 if let Some((intersection_point, wall_normal, material)) =
420 self.find_nearest_wall_intersection(current_pos, ray_direction, &walls)?
421 {
422 let distance = current_pos.distance_to(&intersection_point);
423 total_length += distance;
424 current_path.push(intersection_point);
425
426 let frequency_attenuation = self.calculate_frequency_attenuation(&material);
428 total_attenuation *= frequency_attenuation;
429
430 surface_reflections.push(SurfaceReflection {
432 position: intersection_point,
433 normal: wall_normal,
434 material: material.clone(),
435 incident_angle: self.calculate_incident_angle(ray_direction, wall_normal),
436 });
437
438 let dot = ray_direction.dot(&wall_normal);
440 ray_direction = Position3D::new(
441 ray_direction.x - 2.0 * dot * wall_normal.x,
442 ray_direction.y - 2.0 * dot * wall_normal.y,
443 ray_direction.z - 2.0 * dot * wall_normal.z,
444 )
445 .normalized();
446
447 current_pos = intersection_point;
448
449 if order >= 2 {
451 let listener_distance = intersection_point.distance_to(&listener);
452 if listener_distance < 2.0 && total_attenuation > 0.01 {
453 current_path.push(listener);
454 total_length += listener_distance;
455
456 let reflection_path = ReflectionPath {
457 path: current_path.clone(),
458 length: total_length,
459 delay_samples: (total_length / 343.0 * 44100.0) as usize,
460 attenuation: total_attenuation / (1.0 + total_length),
461 surfaces: surface_reflections.clone(),
462 };
463
464 paths.push(reflection_path);
465 break;
466 }
467 }
468 } else {
469 break; }
471 }
472 }
473
474 Ok(())
475 }
476
477 fn random_ray_direction(
479 &self,
480 rng: &mut scirs2_core::random::prelude::ThreadRng,
481 ) -> Position3D {
482 use scirs2_core::random::Rng;
483
484 let theta = rng.random_range(0.0..std::f32::consts::PI * 2.0);
485 let phi = rng.random_range(0.0..std::f32::consts::PI);
486
487 Position3D::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos())
488 }
489
490 fn get_room_walls(&self, width: f32, height: f32, depth: f32) -> Vec<Wall> {
492 vec![
493 Wall {
494 normal: Position3D::new(-1.0, 0.0, 0.0),
495 point: Position3D::new(0.0, 0.0, 0.0),
496 material: self.config.wall_materials.left_wall.clone(),
497 },
498 Wall {
499 normal: Position3D::new(1.0, 0.0, 0.0),
500 point: Position3D::new(width, 0.0, 0.0),
501 material: self.config.wall_materials.right_wall.clone(),
502 },
503 Wall {
504 normal: Position3D::new(0.0, -1.0, 0.0),
505 point: Position3D::new(0.0, 0.0, 0.0),
506 material: self.config.wall_materials.floor.clone(),
507 },
508 Wall {
509 normal: Position3D::new(0.0, 1.0, 0.0),
510 point: Position3D::new(0.0, height, 0.0),
511 material: self.config.wall_materials.ceiling.clone(),
512 },
513 Wall {
514 normal: Position3D::new(0.0, 0.0, -1.0),
515 point: Position3D::new(0.0, 0.0, 0.0),
516 material: self.config.wall_materials.front_wall.clone(),
517 },
518 Wall {
519 normal: Position3D::new(0.0, 0.0, 1.0),
520 point: Position3D::new(0.0, 0.0, depth),
521 material: self.config.wall_materials.back_wall.clone(),
522 },
523 ]
524 }
525
526 fn find_nearest_wall_intersection(
528 &self,
529 ray_origin: Position3D,
530 ray_direction: Position3D,
531 walls: &[Wall],
532 ) -> crate::Result<Option<(Position3D, Position3D, Material)>> {
533 let mut nearest_intersection = None;
534 let mut nearest_distance = f32::INFINITY;
535
536 for wall in walls {
537 if let Some((intersection_point, distance)) =
538 self.ray_plane_intersection(ray_origin, ray_direction, &wall.point, &wall.normal)?
539 {
540 if distance > 0.001 && distance < nearest_distance {
541 if self.is_within_room_bounds(intersection_point) {
543 nearest_distance = distance;
544 nearest_intersection =
545 Some((intersection_point, wall.normal, wall.material.clone()));
546 }
547 }
548 }
549 }
550
551 Ok(nearest_intersection)
552 }
553
554 fn ray_plane_intersection(
556 &self,
557 ray_origin: Position3D,
558 ray_direction: Position3D,
559 plane_point: &Position3D,
560 plane_normal: &Position3D,
561 ) -> crate::Result<Option<(Position3D, f32)>> {
562 let denominator = ray_direction.dot(plane_normal);
563
564 if denominator.abs() < 1e-6 {
565 return Ok(None); }
567
568 let diff = Position3D::new(
569 plane_point.x - ray_origin.x,
570 plane_point.y - ray_origin.y,
571 plane_point.z - ray_origin.z,
572 );
573
574 let t = diff.dot(plane_normal) / denominator;
575
576 if t >= 0.0 {
577 let intersection = Position3D::new(
578 ray_origin.x + t * ray_direction.x,
579 ray_origin.y + t * ray_direction.y,
580 ray_origin.z + t * ray_direction.z,
581 );
582 Ok(Some((intersection, t)))
583 } else {
584 Ok(None)
585 }
586 }
587
588 fn is_within_room_bounds(&self, point: Position3D) -> bool {
590 let (width, height, depth) = self.config.dimensions;
591 point.x >= 0.0
592 && point.x <= width
593 && point.y >= 0.0
594 && point.y <= height
595 && point.z >= 0.0
596 && point.z <= depth
597 }
598
599 fn calculate_frequency_attenuation(&self, material: &Material) -> f32 {
601 1.0 - material.average_absorption()
603 }
604
605 fn calculate_incident_angle(&self, ray_direction: Position3D, normal: Position3D) -> f32 {
607 let dot_product = ray_direction.dot(&normal).abs();
608 dot_product.acos()
609 }
610
611 fn calculate_reflection_direction(
613 &self,
614 incident: Position3D,
615 normal: Position3D,
616 scattering_coeff: f32,
617 rng: &mut scirs2_core::random::prelude::ThreadRng,
618 ) -> Position3D {
619 use scirs2_core::random::Rng;
620
621 let dot = incident.dot(&normal);
623 let specular = Position3D::new(
624 incident.x - 2.0 * dot * normal.x,
625 incident.y - 2.0 * dot * normal.y,
626 incident.z - 2.0 * dot * normal.z,
627 );
628
629 let diffuse = self.random_ray_direction(rng);
631
632 let mix_factor = 1.0 - scattering_coeff;
634 Position3D::new(
635 mix_factor * specular.x + scattering_coeff * diffuse.x,
636 mix_factor * specular.y + scattering_coeff * diffuse.y,
637 mix_factor * specular.z + scattering_coeff * diffuse.z,
638 )
639 .normalized()
640 }
641
642 fn add_wall_reflections(
644 &self,
645 paths: &mut Vec<ReflectionPath>,
646 source: Position3D,
647 listener: Position3D,
648 width: f32,
649 height: f32,
650 depth: f32,
651 ) {
652 let walls = [
653 (
655 Position3D::new(0.0, source.y, source.z),
656 Position3D::new(-1.0, 0.0, 0.0),
657 ),
658 (
660 Position3D::new(width, source.y, source.z),
661 Position3D::new(1.0, 0.0, 0.0),
662 ),
663 (
665 Position3D::new(source.x, 0.0, source.z),
666 Position3D::new(0.0, -1.0, 0.0),
667 ),
668 (
670 Position3D::new(source.x, height, source.z),
671 Position3D::new(0.0, 1.0, 0.0),
672 ),
673 (
675 Position3D::new(source.x, source.y, 0.0),
676 Position3D::new(0.0, 0.0, -1.0),
677 ),
678 (
680 Position3D::new(source.x, source.y, depth),
681 Position3D::new(0.0, 0.0, 1.0),
682 ),
683 ];
684
685 for (reflection_point, normal) in walls {
686 let source_to_reflection = reflection_point.distance_to(&source);
688 let reflection_to_listener = reflection_point.distance_to(&listener);
689 let total_length = source_to_reflection + reflection_to_listener;
690
691 let path = ReflectionPath {
692 path: vec![source, reflection_point, listener],
693 length: total_length,
694 delay_samples: (total_length / 343.0 * 44100.0) as usize,
695 attenuation: 0.7 / (1.0 + total_length), surfaces: vec![SurfaceReflection {
697 position: reflection_point,
698 normal,
699 material: Material::default(),
700 incident_angle: 0.0, }],
702 };
703
704 paths.push(path);
705 }
706 }
707
708 fn generate_late_reverb(
710 &self,
711 buffer: &mut Array1<f32>,
712 sample_rate: u32,
713 ) -> crate::Result<()> {
714 let decay_rate = -60.0 / (self.config.reverb_time * sample_rate as f32); for (i, sample) in buffer.iter_mut().enumerate() {
717 let time = i as f32 / sample_rate as f32;
718 let amplitude = (decay_rate * time / 20.0).exp(); *sample = amplitude * (scirs2_core::random::random::<f32>() - 0.5) * 2.0;
720 }
722
723 Ok(())
724 }
725
726 pub fn config(&self) -> &RoomConfig {
728 &self.config
729 }
730
731 pub fn set_config(&mut self, config: RoomConfig) -> crate::Result<()> {
733 self.config = config;
734 self.reverb_processor = ReverbProcessor::new(&self.config)?;
735 self.early_reflections = EarlyReflectionProcessor::new(&self.config)?;
736 self.late_reverb = LateReverbProcessor::new(&self.config)?;
737 Ok(())
738 }
739}
740
741pub trait RoomAcoustics {
743 fn process_acoustics(
745 &mut self,
746 input: &Array1<f32>,
747 output: &mut Array1<f32>,
748 source_position: Position3D,
749 listener_position: Position3D,
750 ) -> crate::Result<()>;
751
752 fn calculate_reverb_time(&self) -> f32;
754
755 fn room_properties(&self) -> RoomProperties;
757}
758
759#[derive(Debug, Clone)]
761pub struct RoomProperties {
762 pub volume: f32,
764 pub surface_area: f32,
766 pub average_absorption: f32,
768 pub critical_distance: f32,
770 pub reverb_time: f32,
772}
773
774impl RoomConfig {
775 pub fn new(dimensions: (f32, f32, f32), reverb_time: f32) -> Self {
777 let (width, height, depth) = dimensions;
778 let volume = width * height * depth;
779 let surface_area = 2.0 * (width * height + width * depth + height * depth);
780
781 Self {
782 dimensions,
783 wall_materials: WallMaterials::default(),
784 reverb_time,
785 volume,
786 surface_area,
787 temperature: 20.0,
788 humidity: 50.0,
789 enable_air_absorption: true,
790 }
791 }
792
793 pub fn average_absorption(&self) -> f32 {
795 let materials = [
797 &self.wall_materials.floor,
798 &self.wall_materials.ceiling,
799 &self.wall_materials.left_wall,
800 &self.wall_materials.right_wall,
801 &self.wall_materials.front_wall,
802 &self.wall_materials.back_wall,
803 ];
804
805 let total_absorption: f32 = materials
806 .iter()
807 .map(|material| material.average_absorption())
808 .sum();
809
810 total_absorption / materials.len() as f32
811 }
812}
813
814impl Material {
815 pub fn average_absorption(&self) -> f32 {
817 if self.absorption_coefficients.is_empty() {
818 return 0.1; }
820
821 let sum: f32 = self
822 .absorption_coefficients
823 .iter()
824 .map(|band| band.coefficient)
825 .sum();
826
827 sum / self.absorption_coefficients.len() as f32
828 }
829
830 pub fn concrete() -> Self {
832 Self {
833 name: "Concrete".to_string(),
834 absorption_coefficients: vec![
835 FrequencyBandAbsorption {
836 frequency: 125.0,
837 coefficient: 0.01,
838 },
839 FrequencyBandAbsorption {
840 frequency: 250.0,
841 coefficient: 0.01,
842 },
843 FrequencyBandAbsorption {
844 frequency: 500.0,
845 coefficient: 0.02,
846 },
847 FrequencyBandAbsorption {
848 frequency: 1000.0,
849 coefficient: 0.02,
850 },
851 FrequencyBandAbsorption {
852 frequency: 2000.0,
853 coefficient: 0.02,
854 },
855 FrequencyBandAbsorption {
856 frequency: 4000.0,
857 coefficient: 0.02,
858 },
859 ],
860 scattering_coefficient: 0.1,
861 transmission_coefficient: 0.01,
862 }
863 }
864
865 pub fn carpet() -> Self {
867 Self {
868 name: "Carpet".to_string(),
869 absorption_coefficients: vec![
870 FrequencyBandAbsorption {
871 frequency: 125.0,
872 coefficient: 0.08,
873 },
874 FrequencyBandAbsorption {
875 frequency: 250.0,
876 coefficient: 0.24,
877 },
878 FrequencyBandAbsorption {
879 frequency: 500.0,
880 coefficient: 0.57,
881 },
882 FrequencyBandAbsorption {
883 frequency: 1000.0,
884 coefficient: 0.69,
885 },
886 FrequencyBandAbsorption {
887 frequency: 2000.0,
888 coefficient: 0.71,
889 },
890 FrequencyBandAbsorption {
891 frequency: 4000.0,
892 coefficient: 0.73,
893 },
894 ],
895 scattering_coefficient: 0.3,
896 transmission_coefficient: 0.05,
897 }
898 }
899}
900
901impl Default for Material {
902 fn default() -> Self {
903 Self::concrete()
904 }
905}
906
907impl Default for WallMaterials {
908 fn default() -> Self {
909 Self {
910 floor: Material::carpet(),
911 ceiling: Material::concrete(),
912 left_wall: Material::concrete(),
913 right_wall: Material::concrete(),
914 front_wall: Material::concrete(),
915 back_wall: Material::concrete(),
916 }
917 }
918}
919
920impl EarlyReflectionProcessor {
921 pub fn new(_config: &RoomConfig) -> crate::Result<Self> {
923 Ok(Self {
924 reflection_paths: Vec::new(),
925 max_order: 3,
926 speed_of_sound: 343.0,
927 sample_rate: 44100.0,
928 })
929 }
930
931 pub async fn process(
933 &self,
934 left_channel: &mut Array1<f32>,
935 right_channel: &mut Array1<f32>,
936 _source_position: &Position3D,
937 ) -> crate::Result<()> {
938 let delay_samples = (0.02 * self.sample_rate) as usize; let attenuation = 0.3;
942
943 if delay_samples < left_channel.len() {
944 for i in delay_samples..left_channel.len() {
945 left_channel[i] += left_channel[i - delay_samples] * attenuation;
946 right_channel[i] += right_channel[i - delay_samples] * attenuation;
947 }
948 }
949
950 Ok(())
951 }
952}
953
954impl LateReverbProcessor {
955 pub fn new(config: &RoomConfig) -> crate::Result<Self> {
957 let feedback_networks = vec![FeedbackDelayNetwork::new(&[0.03, 0.032, 0.034, 0.036])?];
958
959 let diffusion_filters = vec![
960 AllPassFilter::new(0.005, 0.7)?,
961 AllPassFilter::new(0.012, 0.5)?,
962 ];
963
964 Ok(Self {
965 feedback_networks,
966 diffusion_filters,
967 reverb_time: config.reverb_time,
968 diffusion: 0.7,
969 })
970 }
971
972 pub async fn process(
974 &self,
975 left_channel: &mut Array1<f32>,
976 right_channel: &mut Array1<f32>,
977 ) -> crate::Result<()> {
978 let decay_rate = (-60.0 / (self.reverb_time * 44100.0)).exp();
981
982 for i in 1..left_channel.len() {
983 left_channel[i] += left_channel[i - 1] * decay_rate * 0.1;
984 right_channel[i] += right_channel[i - 1] * decay_rate * 0.1;
985 }
986
987 Ok(())
988 }
989}
990
991impl ReverbProcessor {
992 pub fn new(config: &RoomConfig) -> crate::Result<Self> {
994 Ok(Self {
995 early_processor: EarlyReflectionProcessor::new(config)?,
996 late_processor: LateReverbProcessor::new(config)?,
997 crossover_frequency: 500.0,
998 dry_level: 0.7,
999 early_level: 0.3,
1000 late_level: 0.4,
1001 })
1002 }
1003}
1004
1005impl FeedbackDelayNetwork {
1006 pub fn new(delays: &[f32]) -> crate::Result<Self> {
1008 let mut delay_lines = Vec::new();
1009 for &delay in delays {
1010 delay_lines.push(DelayLine::new(delay, 44100.0)?);
1011 }
1012
1013 let size = delays.len();
1014 let feedback_matrix = Array2::eye(size) * 0.7; let input_gains = Array1::ones(size);
1016 let output_gains = Array1::ones(size);
1017
1018 Ok(Self {
1019 delay_lines,
1020 feedback_matrix,
1021 input_gains,
1022 output_gains,
1023 })
1024 }
1025}
1026
1027impl AllPassFilter {
1028 pub fn new(delay: f32, feedback: f32) -> crate::Result<Self> {
1030 Ok(Self {
1031 delay_line: DelayLine::new(delay, 44100.0)?,
1032 feedback,
1033 feedforward: -feedback,
1034 })
1035 }
1036}
1037
1038impl DelayLine {
1039 pub fn new(delay_time: f32, sample_rate: f32) -> crate::Result<Self> {
1041 let delay_samples = delay_time * sample_rate;
1042 let max_delay = delay_samples.ceil() as usize + 1;
1043 let buffer = VecDeque::with_capacity(max_delay);
1044
1045 Ok(Self {
1046 buffer,
1047 delay_samples,
1048 max_delay,
1049 })
1050 }
1051
1052 pub fn process(&mut self, input: f32) -> f32 {
1054 self.buffer.push_back(input);
1056
1057 while self.buffer.len() > self.max_delay {
1059 self.buffer.pop_front();
1060 }
1061
1062 let delay_index = self.delay_samples as usize;
1064 if self.buffer.len() > delay_index {
1065 self.buffer[self.buffer.len() - 1 - delay_index]
1066 } else {
1067 0.0
1068 }
1069 }
1070}
1071
1072pub struct MultiRoomEnvironment {
1074 pub rooms: HashMap<String, Room>,
1076 pub connections: Vec<RoomConnection>,
1078 pub global_config: GlobalAcousticConfig,
1080 propagation_cache: HashMap<(String, String), PropagationPath>,
1082}
1083
1084#[derive(Debug, Clone)]
1086pub struct Room {
1087 pub id: String,
1089 pub simulator: RoomSimulator,
1091 pub position: Position3D,
1093 pub orientation: (f32, f32, f32),
1095 pub volume_adjustment: f32,
1097}
1098
1099#[derive(Debug, Clone, Serialize, Deserialize)]
1101pub struct RoomConnection {
1102 pub id: String,
1104 pub from_room: String,
1106 pub to_room: String,
1108 pub connection_type: ConnectionType,
1110 pub from_position: Position3D,
1112 pub to_position: Position3D,
1114 pub dimensions: (f32, f32),
1116 pub acoustic_properties: ConnectionAcousticProperties,
1118 pub state: ConnectionState,
1120}
1121
1122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1124pub enum ConnectionType {
1125 Door,
1127 Doorway,
1129 Window,
1131 Vent,
1133 Opening,
1135 SoundBarrier,
1137}
1138
1139#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1141pub enum ConnectionState {
1142 Open,
1144 Closed,
1146 PartiallyOpen(f32),
1148}
1149
1150#[derive(Debug, Clone, Serialize, Deserialize)]
1152pub struct ConnectionAcousticProperties {
1153 pub transmission_coefficient: f32,
1155 pub frequency_transmission: Vec<FrequencyBandAbsorption>,
1157 pub attenuation_db: f32,
1159 pub reflection_coefficient: f32,
1161 pub diffraction_enabled: bool,
1163}
1164
1165#[derive(Debug, Clone, Serialize, Deserialize)]
1167pub struct GlobalAcousticConfig {
1168 pub speed_of_sound: f32,
1170 pub temperature: f32,
1172 pub humidity: f32,
1174 pub enable_air_absorption: bool,
1176 pub max_propagation_distance: f32,
1178 pub enable_inter_room_delays: bool,
1180}
1181
1182#[derive(Debug, Clone)]
1184pub struct PropagationPath {
1185 pub room_sequence: Vec<String>,
1187 pub connections: Vec<String>,
1189 pub total_attenuation: f32,
1191 pub total_delay_samples: usize,
1193 pub frequency_response: Vec<f32>,
1195}
1196
1197impl Default for MultiRoomEnvironment {
1198 fn default() -> Self {
1199 Self::new()
1200 }
1201}
1202
1203impl MultiRoomEnvironment {
1204 pub fn new() -> Self {
1206 Self {
1207 rooms: HashMap::new(),
1208 connections: Vec::new(),
1209 global_config: GlobalAcousticConfig::default(),
1210 propagation_cache: HashMap::new(),
1211 }
1212 }
1213
1214 pub fn add_room(&mut self, room: Room) -> crate::Result<()> {
1216 if self.rooms.contains_key(&room.id) {
1217 return Err(crate::Error::LegacyRoom(format!(
1218 "Room with ID '{}' already exists",
1219 room.id
1220 )));
1221 }
1222 self.rooms.insert(room.id.clone(), room);
1223 Ok(())
1224 }
1225
1226 pub fn add_connection(&mut self, connection: RoomConnection) -> crate::Result<()> {
1228 if !self.rooms.contains_key(&connection.from_room) {
1230 return Err(crate::Error::LegacyRoom(format!(
1231 "Source room '{}' does not exist",
1232 connection.from_room
1233 )));
1234 }
1235 if !self.rooms.contains_key(&connection.to_room) {
1236 return Err(crate::Error::LegacyRoom(format!(
1237 "Target room '{}' does not exist",
1238 connection.to_room
1239 )));
1240 }
1241
1242 self.connections.push(connection);
1243 self.invalidate_propagation_cache();
1244 Ok(())
1245 }
1246
1247 pub async fn process_multi_room_audio(
1249 &mut self,
1250 source_room_id: &str,
1251 source_position: Position3D,
1252 listener_room_id: &str,
1253 listener_position: Position3D,
1254 input_audio: &Array1<f32>,
1255 sample_rate: u32,
1256 ) -> crate::Result<(Array1<f32>, Array1<f32>)> {
1257 let mut left_output = Array1::zeros(input_audio.len());
1258 let mut right_output = Array1::zeros(input_audio.len());
1259
1260 if source_room_id == listener_room_id {
1261 let room = self.rooms.get(source_room_id).ok_or_else(|| {
1263 crate::Error::LegacyRoom(format!("Room '{source_room_id}' not found"))
1264 })?;
1265
1266 self.apply_room_acoustics(
1268 &room.simulator,
1269 input_audio,
1270 &mut left_output,
1271 &mut right_output,
1272 source_position,
1273 listener_position,
1274 )
1275 .await?;
1276 } else {
1277 let propagation_paths = self
1279 .find_propagation_paths(source_room_id, listener_room_id)
1280 .await?;
1281
1282 for path in &propagation_paths {
1283 let mut path_left = input_audio.clone();
1284 let mut path_right = input_audio.clone();
1285
1286 self.apply_propagation_path(path, &mut path_left, &mut path_right, sample_rate)
1288 .await?;
1289
1290 for i in 0..left_output.len() {
1292 left_output[i] += path_left[i];
1293 right_output[i] += path_right[i];
1294 }
1295 }
1296 }
1297
1298 Ok((left_output, right_output))
1299 }
1300
1301 async fn apply_room_acoustics(
1303 &self,
1304 room_simulator: &RoomSimulator,
1305 input: &Array1<f32>,
1306 left_output: &mut Array1<f32>,
1307 right_output: &mut Array1<f32>,
1308 source_position: Position3D,
1309 _listener_position: Position3D,
1310 ) -> crate::Result<()> {
1311 for i in 0..input.len() {
1313 left_output[i] = input[i];
1314 right_output[i] = input[i];
1315 }
1316
1317 room_simulator
1319 .process_reverb(left_output, right_output, &source_position)
1320 .await?;
1321
1322 Ok(())
1323 }
1324
1325 async fn find_propagation_paths(
1327 &mut self,
1328 source_room: &str,
1329 target_room: &str,
1330 ) -> crate::Result<Vec<PropagationPath>> {
1331 let cache_key = (source_room.to_string(), target_room.to_string());
1333 if let Some(cached_path) = self.propagation_cache.get(&cache_key) {
1334 return Ok(vec![cached_path.clone()]);
1335 }
1336
1337 let mut paths = Vec::new();
1339 let mut visited = std::collections::HashSet::new();
1340 let mut queue = std::collections::VecDeque::new();
1341
1342 queue.push_back(PropagationPath {
1344 room_sequence: vec![source_room.to_string()],
1345 connections: Vec::new(),
1346 total_attenuation: 1.0,
1347 total_delay_samples: 0,
1348 frequency_response: vec![1.0; 10], });
1350
1351 while let Some(current_path) = queue.pop_front() {
1352 let current_room = current_path.room_sequence.last().ok_or_else(|| {
1355 crate::Error::LegacyProcessing(
1356 "Internal error: room sequence unexpectedly empty in path finding".to_string(),
1357 )
1358 })?;
1359
1360 if current_room == target_room {
1361 paths.push(current_path.clone());
1362 continue;
1363 }
1364
1365 if visited.contains(current_room) || current_path.room_sequence.len() > 5 {
1366 continue; }
1368
1369 visited.insert(current_room.clone());
1370
1371 for connection in &self.connections {
1373 if connection.from_room == *current_room
1374 && connection.state != ConnectionState::Closed
1375 {
1376 let mut new_path = current_path.clone();
1377 new_path.room_sequence.push(connection.to_room.clone());
1378 new_path.connections.push(connection.id.clone());
1379
1380 let connection_attenuation = self.calculate_connection_attenuation(connection);
1382 new_path.total_attenuation *= connection_attenuation;
1383
1384 let distance = connection
1386 .from_position
1387 .distance_to(&connection.to_position);
1388 let delay_samples =
1389 (distance / self.global_config.speed_of_sound * 44100.0) as usize;
1390 new_path.total_delay_samples += delay_samples;
1391
1392 queue.push_back(new_path);
1393 }
1394 }
1395 }
1396
1397 if !paths.is_empty() {
1399 self.propagation_cache.insert(cache_key, paths[0].clone());
1400 }
1401
1402 Ok(paths)
1403 }
1404
1405 fn calculate_connection_attenuation(&self, connection: &RoomConnection) -> f32 {
1407 let base_attenuation = match connection.state {
1408 ConnectionState::Open => connection.acoustic_properties.transmission_coefficient,
1409 ConnectionState::Closed => 0.01, ConnectionState::PartiallyOpen(ratio) => {
1411 connection.acoustic_properties.transmission_coefficient * ratio
1412 }
1413 };
1414
1415 base_attenuation * 10_f32.powf(-connection.acoustic_properties.attenuation_db / 20.0)
1417 }
1418
1419 async fn apply_propagation_path(
1421 &self,
1422 path: &PropagationPath,
1423 left_audio: &mut Array1<f32>,
1424 right_audio: &mut Array1<f32>,
1425 _sample_rate: u32,
1426 ) -> crate::Result<()> {
1427 for sample in left_audio.iter_mut() {
1429 *sample *= path.total_attenuation;
1430 }
1431 for sample in right_audio.iter_mut() {
1432 *sample *= path.total_attenuation;
1433 }
1434
1435 if path.total_delay_samples > 0 && path.total_delay_samples < left_audio.len() {
1437 for i in (path.total_delay_samples..left_audio.len()).rev() {
1439 left_audio[i] = left_audio[i - path.total_delay_samples];
1440 right_audio[i] = right_audio[i - path.total_delay_samples];
1441 }
1442 for i in 0..path.total_delay_samples {
1443 left_audio[i] = 0.0;
1444 right_audio[i] = 0.0;
1445 }
1446 }
1447
1448 Ok(())
1449 }
1450
1451 fn invalidate_propagation_cache(&mut self) {
1453 self.propagation_cache.clear();
1454 }
1455
1456 pub fn get_room(&self, room_id: &str) -> Option<&Room> {
1458 self.rooms.get(room_id)
1459 }
1460
1461 pub fn get_room_mut(&mut self, room_id: &str) -> Option<&mut Room> {
1463 self.rooms.get_mut(room_id)
1464 }
1465
1466 pub fn set_connection_state(
1468 &mut self,
1469 connection_id: &str,
1470 state: ConnectionState,
1471 ) -> crate::Result<()> {
1472 if let Some(connection) = self.connections.iter_mut().find(|c| c.id == connection_id) {
1473 connection.state = state;
1474 self.invalidate_propagation_cache();
1475 Ok(())
1476 } else {
1477 Err(crate::Error::LegacyRoom(format!(
1478 "Connection '{connection_id}' not found"
1479 )))
1480 }
1481 }
1482}
1483
1484impl Room {
1485 pub fn new(
1487 id: String,
1488 dimensions: (f32, f32, f32),
1489 reverb_time: f32,
1490 position: Position3D,
1491 ) -> crate::Result<Self> {
1492 Ok(Self {
1493 id,
1494 simulator: RoomSimulator::new(dimensions, reverb_time)?,
1495 position,
1496 orientation: (0.0, 0.0, 0.0),
1497 volume_adjustment: 1.0,
1498 })
1499 }
1500
1501 pub fn with_config(
1503 id: String,
1504 config: RoomConfig,
1505 position: Position3D,
1506 ) -> crate::Result<Self> {
1507 Ok(Self {
1508 id,
1509 simulator: RoomSimulator::with_config(config)?,
1510 position,
1511 orientation: (0.0, 0.0, 0.0),
1512 volume_adjustment: 1.0,
1513 })
1514 }
1515}
1516
1517impl Default for GlobalAcousticConfig {
1518 fn default() -> Self {
1519 Self {
1520 speed_of_sound: 343.0,
1521 temperature: 20.0,
1522 humidity: 50.0,
1523 enable_air_absorption: true,
1524 max_propagation_distance: 100.0,
1525 enable_inter_room_delays: true,
1526 }
1527 }
1528}
1529
1530impl Default for ConnectionAcousticProperties {
1531 fn default() -> Self {
1532 Self {
1533 transmission_coefficient: 0.3,
1534 frequency_transmission: vec![
1535 FrequencyBandAbsorption {
1536 frequency: 125.0,
1537 coefficient: 0.2,
1538 },
1539 FrequencyBandAbsorption {
1540 frequency: 500.0,
1541 coefficient: 0.3,
1542 },
1543 FrequencyBandAbsorption {
1544 frequency: 2000.0,
1545 coefficient: 0.4,
1546 },
1547 FrequencyBandAbsorption {
1548 frequency: 8000.0,
1549 coefficient: 0.2,
1550 },
1551 ],
1552 attenuation_db: 3.0,
1553 reflection_coefficient: 0.1,
1554 diffraction_enabled: true,
1555 }
1556 }
1557}
1558
1559#[cfg(test)]
1560mod tests {
1561 use super::*;
1562
1563 #[test]
1564 fn test_room_simulator_creation() {
1565 let simulator = RoomSimulator::new((10.0, 8.0, 3.0), 1.2);
1566 assert!(simulator.is_ok());
1567 }
1568
1569 #[test]
1570 fn test_room_config() {
1571 let config = RoomConfig::new((5.0, 4.0, 3.0), 1.0);
1572 assert_eq!(config.dimensions, (5.0, 4.0, 3.0));
1573 assert_eq!(config.volume, 60.0);
1574 assert!(config.average_absorption() > 0.0);
1575 }
1576
1577 #[test]
1578 fn test_material_properties() {
1579 let concrete = Material::concrete();
1580 let carpet = Material::carpet();
1581
1582 assert!(carpet.average_absorption() > concrete.average_absorption());
1583 }
1584
1585 #[test]
1586 fn test_delay_line() {
1587 let mut delay_line =
1588 DelayLine::new(0.001, 44100.0).expect("Should successfully create delay line"); let output1 = delay_line.process(1.0);
1592 assert_eq!(output1, 0.0); for _ in 0..50 {
1596 delay_line.process(0.0);
1597 }
1598
1599 let _output2 = delay_line.process(0.0);
1600 }
1602
1603 #[test]
1604 fn test_reflection_path_calculation() {
1605 let simulator = RoomSimulator::new((10.0, 8.0, 3.0), 1.2)
1606 .expect("Should successfully create room simulator");
1607 let source = Position3D::new(2.0, 1.0, 1.0);
1608 let listener = Position3D::new(8.0, 1.0, 2.0);
1609
1610 let paths = simulator
1611 .calculate_reflection_paths(source, listener, 1)
1612 .expect("Should successfully calculate reflection paths");
1613 assert!(!paths.is_empty());
1614
1615 assert!(paths.len() >= 7); }
1618
1619 #[test]
1620 fn test_multi_room_environment_creation() {
1621 let mut env = MultiRoomEnvironment::new();
1622 assert_eq!(env.rooms.len(), 0);
1623 assert_eq!(env.connections.len(), 0);
1624 }
1625
1626 #[test]
1627 fn test_room_creation() {
1628 let room = Room::new(
1629 "living_room".to_string(),
1630 (5.0, 4.0, 3.0),
1631 1.2,
1632 Position3D::new(0.0, 0.0, 0.0),
1633 )
1634 .expect("Should successfully create room");
1635
1636 assert_eq!(room.id, "living_room");
1637 assert_eq!(room.position, Position3D::new(0.0, 0.0, 0.0));
1638 }
1639
1640 #[test]
1641 fn test_multi_room_environment_add_room() {
1642 let mut env = MultiRoomEnvironment::new();
1643
1644 let room = Room::new(
1645 "kitchen".to_string(),
1646 (4.0, 3.0, 2.5),
1647 0.8,
1648 Position3D::new(5.0, 0.0, 0.0),
1649 )
1650 .expect("Should successfully create kitchen");
1651
1652 env.add_room(room)
1653 .expect("Should successfully add room to environment");
1654 assert_eq!(env.rooms.len(), 1);
1655 assert!(env.get_room("kitchen").is_some());
1656 }
1657
1658 #[test]
1659 fn test_room_connection() {
1660 let mut env = MultiRoomEnvironment::new();
1661
1662 let living_room = Room::new(
1664 "living_room".to_string(),
1665 (5.0, 4.0, 3.0),
1666 1.2,
1667 Position3D::new(0.0, 0.0, 0.0),
1668 )
1669 .expect("Should successfully create living room");
1670
1671 let kitchen = Room::new(
1672 "kitchen".to_string(),
1673 (4.0, 3.0, 2.5),
1674 0.8,
1675 Position3D::new(5.0, 0.0, 0.0),
1676 )
1677 .expect("Should successfully create kitchen");
1678
1679 env.add_room(living_room)
1680 .expect("Should successfully add living room");
1681 env.add_room(kitchen)
1682 .expect("Should successfully add kitchen");
1683
1684 let connection = RoomConnection {
1686 id: "door_1".to_string(),
1687 from_room: "living_room".to_string(),
1688 to_room: "kitchen".to_string(),
1689 connection_type: ConnectionType::Door,
1690 from_position: Position3D::new(5.0, 2.0, 1.0),
1691 to_position: Position3D::new(0.0, 2.0, 1.0),
1692 dimensions: (0.8, 2.0),
1693 acoustic_properties: ConnectionAcousticProperties::default(),
1694 state: ConnectionState::Open,
1695 };
1696
1697 env.add_connection(connection)
1698 .expect("Should successfully add connection");
1699 assert_eq!(env.connections.len(), 1);
1700 }
1701
1702 #[test]
1703 fn test_connection_state_changes() {
1704 let mut env = MultiRoomEnvironment::new();
1705
1706 let living_room = Room::new(
1708 "living_room".to_string(),
1709 (5.0, 4.0, 3.0),
1710 1.2,
1711 Position3D::new(0.0, 0.0, 0.0),
1712 )
1713 .expect("Should successfully create living room");
1714 let kitchen = Room::new(
1715 "kitchen".to_string(),
1716 (4.0, 3.0, 2.5),
1717 0.8,
1718 Position3D::new(5.0, 0.0, 0.0),
1719 )
1720 .expect("Should successfully create kitchen");
1721
1722 env.add_room(living_room)
1723 .expect("Should successfully add living room");
1724 env.add_room(kitchen)
1725 .expect("Should successfully add kitchen");
1726
1727 let connection = RoomConnection {
1728 id: "door_1".to_string(),
1729 from_room: "living_room".to_string(),
1730 to_room: "kitchen".to_string(),
1731 connection_type: ConnectionType::Door,
1732 from_position: Position3D::new(5.0, 2.0, 1.0),
1733 to_position: Position3D::new(0.0, 2.0, 1.0),
1734 dimensions: (0.8, 2.0),
1735 acoustic_properties: ConnectionAcousticProperties::default(),
1736 state: ConnectionState::Open,
1737 };
1738
1739 env.add_connection(connection)
1740 .expect("Should successfully add connection");
1741
1742 env.set_connection_state("door_1", ConnectionState::Closed)
1744 .expect("Should successfully set connection state to closed");
1745 assert_eq!(env.connections[0].state, ConnectionState::Closed);
1746
1747 env.set_connection_state("door_1", ConnectionState::PartiallyOpen(0.5))
1748 .expect("Should successfully set connection state to partially open");
1749 assert_eq!(
1750 env.connections[0].state,
1751 ConnectionState::PartiallyOpen(0.5)
1752 );
1753 }
1754
1755 #[tokio::test]
1756 async fn test_multi_room_audio_processing() {
1757 let mut env = MultiRoomEnvironment::new();
1758
1759 let living_room = Room::new(
1761 "living_room".to_string(),
1762 (5.0, 4.0, 3.0),
1763 1.2,
1764 Position3D::new(0.0, 0.0, 0.0),
1765 )
1766 .expect("Should successfully create living room");
1767 let kitchen = Room::new(
1768 "kitchen".to_string(),
1769 (4.0, 3.0, 2.5),
1770 0.8,
1771 Position3D::new(5.0, 0.0, 0.0),
1772 )
1773 .expect("Should successfully create kitchen");
1774
1775 env.add_room(living_room)
1776 .expect("Should successfully add living room");
1777 env.add_room(kitchen)
1778 .expect("Should successfully add kitchen");
1779
1780 let connection = RoomConnection {
1782 id: "door_1".to_string(),
1783 from_room: "living_room".to_string(),
1784 to_room: "kitchen".to_string(),
1785 connection_type: ConnectionType::Door,
1786 from_position: Position3D::new(5.0, 2.0, 1.0),
1787 to_position: Position3D::new(0.0, 2.0, 1.0),
1788 dimensions: (0.8, 2.0),
1789 acoustic_properties: ConnectionAcousticProperties::default(),
1790 state: ConnectionState::Open,
1791 };
1792
1793 env.add_connection(connection)
1794 .expect("Should successfully add connection");
1795
1796 let input_audio = Array1::from_vec(vec![0.5; 1000]);
1798 let source_pos = Position3D::new(2.0, 2.0, 1.5);
1799 let listener_pos = Position3D::new(2.0, 1.5, 1.5);
1800
1801 let result = env
1803 .process_multi_room_audio(
1804 "living_room",
1805 source_pos,
1806 "living_room",
1807 listener_pos,
1808 &input_audio,
1809 44100,
1810 )
1811 .await;
1812
1813 assert!(result.is_ok());
1814 let (left, right) = result.expect("Should successfully process multi-room audio");
1815 assert_eq!(left.len(), input_audio.len());
1816 assert_eq!(right.len(), input_audio.len());
1817 }
1818}