pub mod adaptive_acoustics;
pub mod simulation;
use crate::types::Position3D;
use scirs2_core::ndarray::{Array1, Array2};
use serde::{Deserialize, Serialize};
pub use simulation::{
AcousticResponse, AdvancedRoomSimulator, DiffractionProcessor, DynamicEnvironmentManager,
FrequencyDependentProperty, Material as SimMaterial, MaterialDatabase, RayTracingEngine,
RoomGeometry, RoomSimulationConfig,
};
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone)]
pub struct RoomSimulator {
config: RoomConfig,
reverb_processor: ReverbProcessor,
early_reflections: EarlyReflectionProcessor,
late_reverb: LateReverbProcessor,
room_ir: Option<RoomImpulseResponse>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoomConfig {
pub dimensions: (f32, f32, f32),
pub wall_materials: WallMaterials,
pub reverb_time: f32,
pub volume: f32,
pub surface_area: f32,
pub temperature: f32,
pub humidity: f32,
pub enable_air_absorption: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WallMaterials {
pub floor: Material,
pub ceiling: Material,
pub left_wall: Material,
pub right_wall: Material,
pub front_wall: Material,
pub back_wall: Material,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Material {
pub name: String,
pub absorption_coefficients: Vec<FrequencyBandAbsorption>,
pub scattering_coefficient: f32,
pub transmission_coefficient: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrequencyBandAbsorption {
pub frequency: f32,
pub coefficient: f32,
}
#[derive(Debug, Clone)]
pub struct RoomImpulseResponse {
pub early_reflections: Array1<f32>,
pub late_reverb: Array1<f32>,
pub combined_ir: Array1<f32>,
pub sample_rate: u32,
}
#[derive(Debug, Clone)]
pub struct EarlyReflectionProcessor {
#[allow(dead_code)]
reflection_paths: Vec<ReflectionPath>,
#[allow(dead_code)]
max_order: usize,
#[allow(dead_code)]
speed_of_sound: f32,
sample_rate: f32,
}
#[derive(Debug, Clone)]
pub struct LateReverbProcessor {
#[allow(dead_code)]
feedback_networks: Vec<FeedbackDelayNetwork>,
#[allow(dead_code)]
diffusion_filters: Vec<AllPassFilter>,
reverb_time: f32,
#[allow(dead_code)]
diffusion: f32,
}
#[derive(Debug, Clone)]
pub struct ReverbProcessor {
#[allow(dead_code)]
early_processor: EarlyReflectionProcessor,
#[allow(dead_code)]
late_processor: LateReverbProcessor,
#[allow(dead_code)]
crossover_frequency: f32,
#[allow(dead_code)]
dry_level: f32,
#[allow(dead_code)]
early_level: f32,
#[allow(dead_code)]
late_level: f32,
}
#[derive(Debug, Clone)]
pub struct ReflectionPath {
pub path: Vec<Position3D>,
pub length: f32,
pub delay_samples: usize,
pub attenuation: f32,
pub surfaces: Vec<SurfaceReflection>,
}
#[derive(Debug, Clone)]
pub struct SurfaceReflection {
pub position: Position3D,
pub normal: Position3D,
pub material: Material,
pub incident_angle: f32,
}
#[derive(Debug, Clone)]
pub struct FeedbackDelayNetwork {
#[allow(dead_code)]
delay_lines: Vec<DelayLine>,
#[allow(dead_code)]
feedback_matrix: Array2<f32>,
#[allow(dead_code)]
input_gains: Array1<f32>,
#[allow(dead_code)]
output_gains: Array1<f32>,
}
#[derive(Debug, Clone)]
pub struct AllPassFilter {
#[allow(dead_code)]
delay_line: DelayLine,
#[allow(dead_code)]
feedback: f32,
#[allow(dead_code)]
feedforward: f32,
}
#[derive(Debug, Clone)]
pub struct DelayLine {
buffer: VecDeque<f32>,
delay_samples: f32,
max_delay: usize,
}
#[derive(Debug, Clone)]
pub struct Wall {
pub normal: Position3D,
pub point: Position3D,
pub material: Material,
}
impl RoomSimulator {
pub fn new(dimensions: (f32, f32, f32), reverb_time: f32) -> crate::Result<Self> {
let config = RoomConfig::new(dimensions, reverb_time);
let reverb_processor = ReverbProcessor::new(&config)?;
let early_reflections = EarlyReflectionProcessor::new(&config)?;
let late_reverb = LateReverbProcessor::new(&config)?;
Ok(Self {
config,
reverb_processor,
early_reflections,
late_reverb,
room_ir: None,
})
}
pub fn with_config(config: RoomConfig) -> crate::Result<Self> {
let reverb_processor = ReverbProcessor::new(&config)?;
let early_reflections = EarlyReflectionProcessor::new(&config)?;
let late_reverb = LateReverbProcessor::new(&config)?;
Ok(Self {
config,
reverb_processor,
early_reflections,
late_reverb,
room_ir: None,
})
}
pub async fn process_reverb(
&self,
left_channel: &mut Array1<f32>,
right_channel: &mut Array1<f32>,
source_position: &Position3D,
) -> crate::Result<()> {
self.early_reflections
.process(left_channel, right_channel, source_position)
.await?;
self.late_reverb
.process(left_channel, right_channel)
.await?;
Ok(())
}
pub fn calculate_room_ir(
&mut self,
source_position: Position3D,
listener_position: Position3D,
sample_rate: u32,
) -> crate::Result<RoomImpulseResponse> {
let ir_length = (self.config.reverb_time * sample_rate as f32) as usize;
let mut early_reflections = Array1::zeros(ir_length);
let mut late_reverb = Array1::zeros(ir_length);
let reflection_paths =
self.calculate_reflection_paths(source_position, listener_position, 3)?;
for path in &reflection_paths {
if path.delay_samples < early_reflections.len() {
early_reflections[path.delay_samples] += path.attenuation;
}
}
self.generate_late_reverb(&mut late_reverb, sample_rate)?;
let mut combined_ir = Array1::zeros(ir_length);
for i in 0..ir_length {
combined_ir[i] = early_reflections[i] + late_reverb[i];
}
let room_ir = RoomImpulseResponse {
early_reflections,
late_reverb,
combined_ir,
sample_rate,
};
self.room_ir = Some(room_ir.clone());
Ok(room_ir)
}
fn calculate_reflection_paths(
&self,
source: Position3D,
listener: Position3D,
max_order: usize,
) -> crate::Result<Vec<ReflectionPath>> {
let mut paths = Vec::new();
let (width, height, depth) = self.config.dimensions;
let direct_path = ReflectionPath {
path: vec![source, listener],
length: source.distance_to(&listener),
delay_samples: (source.distance_to(&listener) / 343.0 * 44100.0) as usize,
attenuation: 1.0 / (1.0 + source.distance_to(&listener)),
surfaces: Vec::new(),
};
paths.push(direct_path);
self.calculate_ray_traced_paths(&mut paths, source, listener, max_order)?;
Ok(paths)
}
fn calculate_ray_traced_paths(
&self,
paths: &mut Vec<ReflectionPath>,
source: Position3D,
listener: Position3D,
max_order: usize,
) -> crate::Result<()> {
let (width, height, depth) = self.config.dimensions;
if max_order >= 1 {
self.add_wall_reflections(paths, source, listener, width, height, depth);
}
if max_order >= 2 {
self.add_ray_traced_reflections(paths, source, listener, max_order)?;
}
Ok(())
}
fn add_ray_traced_reflections(
&self,
paths: &mut Vec<ReflectionPath>,
source: Position3D,
listener: Position3D,
max_order: usize,
) -> crate::Result<()> {
let walls = self.get_room_walls(
self.config.dimensions.0,
self.config.dimensions.1,
self.config.dimensions.2,
);
let to_listener = Position3D::new(
listener.x - source.x,
listener.y - source.y,
listener.z - source.z,
)
.normalized();
let base_directions = vec![
to_listener,
Position3D::new(to_listener.x, -to_listener.y, to_listener.z).normalized(),
Position3D::new(-to_listener.x, to_listener.y, to_listener.z).normalized(),
Position3D::new(to_listener.x, to_listener.y, -to_listener.z).normalized(),
];
for base_direction in base_directions {
let mut current_pos = source;
let mut ray_direction = base_direction;
let mut current_path = vec![source];
let mut total_attenuation = 1.0;
let mut total_length = 0.0;
let mut surface_reflections = Vec::new();
for order in 1..=max_order {
if let Some((intersection_point, wall_normal, material)) =
self.find_nearest_wall_intersection(current_pos, ray_direction, &walls)?
{
let distance = current_pos.distance_to(&intersection_point);
total_length += distance;
current_path.push(intersection_point);
let frequency_attenuation = self.calculate_frequency_attenuation(&material);
total_attenuation *= frequency_attenuation;
surface_reflections.push(SurfaceReflection {
position: intersection_point,
normal: wall_normal,
material: material.clone(),
incident_angle: self.calculate_incident_angle(ray_direction, wall_normal),
});
let dot = ray_direction.dot(&wall_normal);
ray_direction = Position3D::new(
ray_direction.x - 2.0 * dot * wall_normal.x,
ray_direction.y - 2.0 * dot * wall_normal.y,
ray_direction.z - 2.0 * dot * wall_normal.z,
)
.normalized();
current_pos = intersection_point;
if order >= 2 {
let listener_distance = intersection_point.distance_to(&listener);
if listener_distance < 2.0 && total_attenuation > 0.01 {
current_path.push(listener);
total_length += listener_distance;
let reflection_path = ReflectionPath {
path: current_path.clone(),
length: total_length,
delay_samples: (total_length / 343.0 * 44100.0) as usize,
attenuation: total_attenuation / (1.0 + total_length),
surfaces: surface_reflections.clone(),
};
paths.push(reflection_path);
break;
}
}
} else {
break; }
}
}
Ok(())
}
fn random_ray_direction(
&self,
rng: &mut scirs2_core::random::prelude::ThreadRng,
) -> Position3D {
use scirs2_core::random::Rng;
let theta = rng.random_range(0.0..std::f32::consts::PI * 2.0);
let phi = rng.random_range(0.0..std::f32::consts::PI);
Position3D::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos())
}
fn get_room_walls(&self, width: f32, height: f32, depth: f32) -> Vec<Wall> {
vec![
Wall {
normal: Position3D::new(-1.0, 0.0, 0.0),
point: Position3D::new(0.0, 0.0, 0.0),
material: self.config.wall_materials.left_wall.clone(),
},
Wall {
normal: Position3D::new(1.0, 0.0, 0.0),
point: Position3D::new(width, 0.0, 0.0),
material: self.config.wall_materials.right_wall.clone(),
},
Wall {
normal: Position3D::new(0.0, -1.0, 0.0),
point: Position3D::new(0.0, 0.0, 0.0),
material: self.config.wall_materials.floor.clone(),
},
Wall {
normal: Position3D::new(0.0, 1.0, 0.0),
point: Position3D::new(0.0, height, 0.0),
material: self.config.wall_materials.ceiling.clone(),
},
Wall {
normal: Position3D::new(0.0, 0.0, -1.0),
point: Position3D::new(0.0, 0.0, 0.0),
material: self.config.wall_materials.front_wall.clone(),
},
Wall {
normal: Position3D::new(0.0, 0.0, 1.0),
point: Position3D::new(0.0, 0.0, depth),
material: self.config.wall_materials.back_wall.clone(),
},
]
}
fn find_nearest_wall_intersection(
&self,
ray_origin: Position3D,
ray_direction: Position3D,
walls: &[Wall],
) -> crate::Result<Option<(Position3D, Position3D, Material)>> {
let mut nearest_intersection = None;
let mut nearest_distance = f32::INFINITY;
for wall in walls {
if let Some((intersection_point, distance)) =
self.ray_plane_intersection(ray_origin, ray_direction, &wall.point, &wall.normal)?
{
if distance > 0.001 && distance < nearest_distance {
if self.is_within_room_bounds(intersection_point) {
nearest_distance = distance;
nearest_intersection =
Some((intersection_point, wall.normal, wall.material.clone()));
}
}
}
}
Ok(nearest_intersection)
}
fn ray_plane_intersection(
&self,
ray_origin: Position3D,
ray_direction: Position3D,
plane_point: &Position3D,
plane_normal: &Position3D,
) -> crate::Result<Option<(Position3D, f32)>> {
let denominator = ray_direction.dot(plane_normal);
if denominator.abs() < 1e-6 {
return Ok(None); }
let diff = Position3D::new(
plane_point.x - ray_origin.x,
plane_point.y - ray_origin.y,
plane_point.z - ray_origin.z,
);
let t = diff.dot(plane_normal) / denominator;
if t >= 0.0 {
let intersection = Position3D::new(
ray_origin.x + t * ray_direction.x,
ray_origin.y + t * ray_direction.y,
ray_origin.z + t * ray_direction.z,
);
Ok(Some((intersection, t)))
} else {
Ok(None)
}
}
fn is_within_room_bounds(&self, point: Position3D) -> bool {
let (width, height, depth) = self.config.dimensions;
point.x >= 0.0
&& point.x <= width
&& point.y >= 0.0
&& point.y <= height
&& point.z >= 0.0
&& point.z <= depth
}
fn calculate_frequency_attenuation(&self, material: &Material) -> f32 {
1.0 - material.average_absorption()
}
fn calculate_incident_angle(&self, ray_direction: Position3D, normal: Position3D) -> f32 {
let dot_product = ray_direction.dot(&normal).abs();
dot_product.acos()
}
fn calculate_reflection_direction(
&self,
incident: Position3D,
normal: Position3D,
scattering_coeff: f32,
rng: &mut scirs2_core::random::prelude::ThreadRng,
) -> Position3D {
use scirs2_core::random::Rng;
let dot = incident.dot(&normal);
let specular = Position3D::new(
incident.x - 2.0 * dot * normal.x,
incident.y - 2.0 * dot * normal.y,
incident.z - 2.0 * dot * normal.z,
);
let diffuse = self.random_ray_direction(rng);
let mix_factor = 1.0 - scattering_coeff;
Position3D::new(
mix_factor * specular.x + scattering_coeff * diffuse.x,
mix_factor * specular.y + scattering_coeff * diffuse.y,
mix_factor * specular.z + scattering_coeff * diffuse.z,
)
.normalized()
}
fn add_wall_reflections(
&self,
paths: &mut Vec<ReflectionPath>,
source: Position3D,
listener: Position3D,
width: f32,
height: f32,
depth: f32,
) {
let walls = [
(
Position3D::new(0.0, source.y, source.z),
Position3D::new(-1.0, 0.0, 0.0),
),
(
Position3D::new(width, source.y, source.z),
Position3D::new(1.0, 0.0, 0.0),
),
(
Position3D::new(source.x, 0.0, source.z),
Position3D::new(0.0, -1.0, 0.0),
),
(
Position3D::new(source.x, height, source.z),
Position3D::new(0.0, 1.0, 0.0),
),
(
Position3D::new(source.x, source.y, 0.0),
Position3D::new(0.0, 0.0, -1.0),
),
(
Position3D::new(source.x, source.y, depth),
Position3D::new(0.0, 0.0, 1.0),
),
];
for (reflection_point, normal) in walls {
let source_to_reflection = reflection_point.distance_to(&source);
let reflection_to_listener = reflection_point.distance_to(&listener);
let total_length = source_to_reflection + reflection_to_listener;
let path = ReflectionPath {
path: vec![source, reflection_point, listener],
length: total_length,
delay_samples: (total_length / 343.0 * 44100.0) as usize,
attenuation: 0.7 / (1.0 + total_length), surfaces: vec![SurfaceReflection {
position: reflection_point,
normal,
material: Material::default(),
incident_angle: 0.0, }],
};
paths.push(path);
}
}
fn generate_late_reverb(
&self,
buffer: &mut Array1<f32>,
sample_rate: u32,
) -> crate::Result<()> {
let decay_rate = -60.0 / (self.config.reverb_time * sample_rate as f32);
for (i, sample) in buffer.iter_mut().enumerate() {
let time = i as f32 / sample_rate as f32;
let amplitude = (decay_rate * time / 20.0).exp(); *sample = amplitude * (scirs2_core::random::random::<f32>() - 0.5) * 2.0;
}
Ok(())
}
pub fn config(&self) -> &RoomConfig {
&self.config
}
pub fn set_config(&mut self, config: RoomConfig) -> crate::Result<()> {
self.config = config;
self.reverb_processor = ReverbProcessor::new(&self.config)?;
self.early_reflections = EarlyReflectionProcessor::new(&self.config)?;
self.late_reverb = LateReverbProcessor::new(&self.config)?;
Ok(())
}
}
pub trait RoomAcoustics {
fn process_acoustics(
&mut self,
input: &Array1<f32>,
output: &mut Array1<f32>,
source_position: Position3D,
listener_position: Position3D,
) -> crate::Result<()>;
fn calculate_reverb_time(&self) -> f32;
fn room_properties(&self) -> RoomProperties;
}
#[derive(Debug, Clone)]
pub struct RoomProperties {
pub volume: f32,
pub surface_area: f32,
pub average_absorption: f32,
pub critical_distance: f32,
pub reverb_time: f32,
}
impl RoomConfig {
pub fn new(dimensions: (f32, f32, f32), reverb_time: f32) -> Self {
let (width, height, depth) = dimensions;
let volume = width * height * depth;
let surface_area = 2.0 * (width * height + width * depth + height * depth);
Self {
dimensions,
wall_materials: WallMaterials::default(),
reverb_time,
volume,
surface_area,
temperature: 20.0,
humidity: 50.0,
enable_air_absorption: true,
}
}
pub fn average_absorption(&self) -> f32 {
let materials = [
&self.wall_materials.floor,
&self.wall_materials.ceiling,
&self.wall_materials.left_wall,
&self.wall_materials.right_wall,
&self.wall_materials.front_wall,
&self.wall_materials.back_wall,
];
let total_absorption: f32 = materials
.iter()
.map(|material| material.average_absorption())
.sum();
total_absorption / materials.len() as f32
}
}
impl Material {
pub fn average_absorption(&self) -> f32 {
if self.absorption_coefficients.is_empty() {
return 0.1; }
let sum: f32 = self
.absorption_coefficients
.iter()
.map(|band| band.coefficient)
.sum();
sum / self.absorption_coefficients.len() as f32
}
pub fn concrete() -> Self {
Self {
name: "Concrete".to_string(),
absorption_coefficients: vec![
FrequencyBandAbsorption {
frequency: 125.0,
coefficient: 0.01,
},
FrequencyBandAbsorption {
frequency: 250.0,
coefficient: 0.01,
},
FrequencyBandAbsorption {
frequency: 500.0,
coefficient: 0.02,
},
FrequencyBandAbsorption {
frequency: 1000.0,
coefficient: 0.02,
},
FrequencyBandAbsorption {
frequency: 2000.0,
coefficient: 0.02,
},
FrequencyBandAbsorption {
frequency: 4000.0,
coefficient: 0.02,
},
],
scattering_coefficient: 0.1,
transmission_coefficient: 0.01,
}
}
pub fn carpet() -> Self {
Self {
name: "Carpet".to_string(),
absorption_coefficients: vec![
FrequencyBandAbsorption {
frequency: 125.0,
coefficient: 0.08,
},
FrequencyBandAbsorption {
frequency: 250.0,
coefficient: 0.24,
},
FrequencyBandAbsorption {
frequency: 500.0,
coefficient: 0.57,
},
FrequencyBandAbsorption {
frequency: 1000.0,
coefficient: 0.69,
},
FrequencyBandAbsorption {
frequency: 2000.0,
coefficient: 0.71,
},
FrequencyBandAbsorption {
frequency: 4000.0,
coefficient: 0.73,
},
],
scattering_coefficient: 0.3,
transmission_coefficient: 0.05,
}
}
}
impl Default for Material {
fn default() -> Self {
Self::concrete()
}
}
impl Default for WallMaterials {
fn default() -> Self {
Self {
floor: Material::carpet(),
ceiling: Material::concrete(),
left_wall: Material::concrete(),
right_wall: Material::concrete(),
front_wall: Material::concrete(),
back_wall: Material::concrete(),
}
}
}
impl EarlyReflectionProcessor {
pub fn new(_config: &RoomConfig) -> crate::Result<Self> {
Ok(Self {
reflection_paths: Vec::new(),
max_order: 3,
speed_of_sound: 343.0,
sample_rate: 44100.0,
})
}
pub async fn process(
&self,
left_channel: &mut Array1<f32>,
right_channel: &mut Array1<f32>,
_source_position: &Position3D,
) -> crate::Result<()> {
let delay_samples = (0.02 * self.sample_rate) as usize; let attenuation = 0.3;
if delay_samples < left_channel.len() {
for i in delay_samples..left_channel.len() {
left_channel[i] += left_channel[i - delay_samples] * attenuation;
right_channel[i] += right_channel[i - delay_samples] * attenuation;
}
}
Ok(())
}
}
impl LateReverbProcessor {
pub fn new(config: &RoomConfig) -> crate::Result<Self> {
let feedback_networks = vec![FeedbackDelayNetwork::new(&[0.03, 0.032, 0.034, 0.036])?];
let diffusion_filters = vec![
AllPassFilter::new(0.005, 0.7)?,
AllPassFilter::new(0.012, 0.5)?,
];
Ok(Self {
feedback_networks,
diffusion_filters,
reverb_time: config.reverb_time,
diffusion: 0.7,
})
}
pub async fn process(
&self,
left_channel: &mut Array1<f32>,
right_channel: &mut Array1<f32>,
) -> crate::Result<()> {
let decay_rate = (-60.0 / (self.reverb_time * 44100.0)).exp();
for i in 1..left_channel.len() {
left_channel[i] += left_channel[i - 1] * decay_rate * 0.1;
right_channel[i] += right_channel[i - 1] * decay_rate * 0.1;
}
Ok(())
}
}
impl ReverbProcessor {
pub fn new(config: &RoomConfig) -> crate::Result<Self> {
Ok(Self {
early_processor: EarlyReflectionProcessor::new(config)?,
late_processor: LateReverbProcessor::new(config)?,
crossover_frequency: 500.0,
dry_level: 0.7,
early_level: 0.3,
late_level: 0.4,
})
}
}
impl FeedbackDelayNetwork {
pub fn new(delays: &[f32]) -> crate::Result<Self> {
let mut delay_lines = Vec::new();
for &delay in delays {
delay_lines.push(DelayLine::new(delay, 44100.0)?);
}
let size = delays.len();
let feedback_matrix = Array2::eye(size) * 0.7; let input_gains = Array1::ones(size);
let output_gains = Array1::ones(size);
Ok(Self {
delay_lines,
feedback_matrix,
input_gains,
output_gains,
})
}
}
impl AllPassFilter {
pub fn new(delay: f32, feedback: f32) -> crate::Result<Self> {
Ok(Self {
delay_line: DelayLine::new(delay, 44100.0)?,
feedback,
feedforward: -feedback,
})
}
}
impl DelayLine {
pub fn new(delay_time: f32, sample_rate: f32) -> crate::Result<Self> {
let delay_samples = delay_time * sample_rate;
let max_delay = delay_samples.ceil() as usize + 1;
let buffer = VecDeque::with_capacity(max_delay);
Ok(Self {
buffer,
delay_samples,
max_delay,
})
}
pub fn process(&mut self, input: f32) -> f32 {
self.buffer.push_back(input);
while self.buffer.len() > self.max_delay {
self.buffer.pop_front();
}
let delay_index = self.delay_samples as usize;
if self.buffer.len() > delay_index {
self.buffer[self.buffer.len() - 1 - delay_index]
} else {
0.0
}
}
}
pub struct MultiRoomEnvironment {
pub rooms: HashMap<String, Room>,
pub connections: Vec<RoomConnection>,
pub global_config: GlobalAcousticConfig,
propagation_cache: HashMap<(String, String), PropagationPath>,
}
#[derive(Debug, Clone)]
pub struct Room {
pub id: String,
pub simulator: RoomSimulator,
pub position: Position3D,
pub orientation: (f32, f32, f32),
pub volume_adjustment: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoomConnection {
pub id: String,
pub from_room: String,
pub to_room: String,
pub connection_type: ConnectionType,
pub from_position: Position3D,
pub to_position: Position3D,
pub dimensions: (f32, f32),
pub acoustic_properties: ConnectionAcousticProperties,
pub state: ConnectionState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConnectionType {
Door,
Doorway,
Window,
Vent,
Opening,
SoundBarrier,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum ConnectionState {
Open,
Closed,
PartiallyOpen(f32),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionAcousticProperties {
pub transmission_coefficient: f32,
pub frequency_transmission: Vec<FrequencyBandAbsorption>,
pub attenuation_db: f32,
pub reflection_coefficient: f32,
pub diffraction_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlobalAcousticConfig {
pub speed_of_sound: f32,
pub temperature: f32,
pub humidity: f32,
pub enable_air_absorption: bool,
pub max_propagation_distance: f32,
pub enable_inter_room_delays: bool,
}
#[derive(Debug, Clone)]
pub struct PropagationPath {
pub room_sequence: Vec<String>,
pub connections: Vec<String>,
pub total_attenuation: f32,
pub total_delay_samples: usize,
pub frequency_response: Vec<f32>,
}
impl Default for MultiRoomEnvironment {
fn default() -> Self {
Self::new()
}
}
impl MultiRoomEnvironment {
pub fn new() -> Self {
Self {
rooms: HashMap::new(),
connections: Vec::new(),
global_config: GlobalAcousticConfig::default(),
propagation_cache: HashMap::new(),
}
}
pub fn add_room(&mut self, room: Room) -> crate::Result<()> {
if self.rooms.contains_key(&room.id) {
return Err(crate::Error::LegacyRoom(format!(
"Room with ID '{}' already exists",
room.id
)));
}
self.rooms.insert(room.id.clone(), room);
Ok(())
}
pub fn add_connection(&mut self, connection: RoomConnection) -> crate::Result<()> {
if !self.rooms.contains_key(&connection.from_room) {
return Err(crate::Error::LegacyRoom(format!(
"Source room '{}' does not exist",
connection.from_room
)));
}
if !self.rooms.contains_key(&connection.to_room) {
return Err(crate::Error::LegacyRoom(format!(
"Target room '{}' does not exist",
connection.to_room
)));
}
self.connections.push(connection);
self.invalidate_propagation_cache();
Ok(())
}
pub async fn process_multi_room_audio(
&mut self,
source_room_id: &str,
source_position: Position3D,
listener_room_id: &str,
listener_position: Position3D,
input_audio: &Array1<f32>,
sample_rate: u32,
) -> crate::Result<(Array1<f32>, Array1<f32>)> {
let mut left_output = Array1::zeros(input_audio.len());
let mut right_output = Array1::zeros(input_audio.len());
if source_room_id == listener_room_id {
let room = self.rooms.get(source_room_id).ok_or_else(|| {
crate::Error::LegacyRoom(format!("Room '{source_room_id}' not found"))
})?;
self.apply_room_acoustics(
&room.simulator,
input_audio,
&mut left_output,
&mut right_output,
source_position,
listener_position,
)
.await?;
} else {
let propagation_paths = self
.find_propagation_paths(source_room_id, listener_room_id)
.await?;
for path in &propagation_paths {
let mut path_left = input_audio.clone();
let mut path_right = input_audio.clone();
self.apply_propagation_path(path, &mut path_left, &mut path_right, sample_rate)
.await?;
for i in 0..left_output.len() {
left_output[i] += path_left[i];
right_output[i] += path_right[i];
}
}
}
Ok((left_output, right_output))
}
async fn apply_room_acoustics(
&self,
room_simulator: &RoomSimulator,
input: &Array1<f32>,
left_output: &mut Array1<f32>,
right_output: &mut Array1<f32>,
source_position: Position3D,
_listener_position: Position3D,
) -> crate::Result<()> {
for i in 0..input.len() {
left_output[i] = input[i];
right_output[i] = input[i];
}
room_simulator
.process_reverb(left_output, right_output, &source_position)
.await?;
Ok(())
}
async fn find_propagation_paths(
&mut self,
source_room: &str,
target_room: &str,
) -> crate::Result<Vec<PropagationPath>> {
let cache_key = (source_room.to_string(), target_room.to_string());
if let Some(cached_path) = self.propagation_cache.get(&cache_key) {
return Ok(vec![cached_path.clone()]);
}
let mut paths = Vec::new();
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back(PropagationPath {
room_sequence: vec![source_room.to_string()],
connections: Vec::new(),
total_attenuation: 1.0,
total_delay_samples: 0,
frequency_response: vec![1.0; 10], });
while let Some(current_path) = queue.pop_front() {
let current_room = current_path.room_sequence.last().ok_or_else(|| {
crate::Error::LegacyProcessing(
"Internal error: room sequence unexpectedly empty in path finding".to_string(),
)
})?;
if current_room == target_room {
paths.push(current_path.clone());
continue;
}
if visited.contains(current_room) || current_path.room_sequence.len() > 5 {
continue; }
visited.insert(current_room.clone());
for connection in &self.connections {
if connection.from_room == *current_room
&& connection.state != ConnectionState::Closed
{
let mut new_path = current_path.clone();
new_path.room_sequence.push(connection.to_room.clone());
new_path.connections.push(connection.id.clone());
let connection_attenuation = self.calculate_connection_attenuation(connection);
new_path.total_attenuation *= connection_attenuation;
let distance = connection
.from_position
.distance_to(&connection.to_position);
let delay_samples =
(distance / self.global_config.speed_of_sound * 44100.0) as usize;
new_path.total_delay_samples += delay_samples;
queue.push_back(new_path);
}
}
}
if !paths.is_empty() {
self.propagation_cache.insert(cache_key, paths[0].clone());
}
Ok(paths)
}
fn calculate_connection_attenuation(&self, connection: &RoomConnection) -> f32 {
let base_attenuation = match connection.state {
ConnectionState::Open => connection.acoustic_properties.transmission_coefficient,
ConnectionState::Closed => 0.01, ConnectionState::PartiallyOpen(ratio) => {
connection.acoustic_properties.transmission_coefficient * ratio
}
};
base_attenuation * 10_f32.powf(-connection.acoustic_properties.attenuation_db / 20.0)
}
async fn apply_propagation_path(
&self,
path: &PropagationPath,
left_audio: &mut Array1<f32>,
right_audio: &mut Array1<f32>,
_sample_rate: u32,
) -> crate::Result<()> {
for sample in left_audio.iter_mut() {
*sample *= path.total_attenuation;
}
for sample in right_audio.iter_mut() {
*sample *= path.total_attenuation;
}
if path.total_delay_samples > 0 && path.total_delay_samples < left_audio.len() {
for i in (path.total_delay_samples..left_audio.len()).rev() {
left_audio[i] = left_audio[i - path.total_delay_samples];
right_audio[i] = right_audio[i - path.total_delay_samples];
}
for i in 0..path.total_delay_samples {
left_audio[i] = 0.0;
right_audio[i] = 0.0;
}
}
Ok(())
}
fn invalidate_propagation_cache(&mut self) {
self.propagation_cache.clear();
}
pub fn get_room(&self, room_id: &str) -> Option<&Room> {
self.rooms.get(room_id)
}
pub fn get_room_mut(&mut self, room_id: &str) -> Option<&mut Room> {
self.rooms.get_mut(room_id)
}
pub fn set_connection_state(
&mut self,
connection_id: &str,
state: ConnectionState,
) -> crate::Result<()> {
if let Some(connection) = self.connections.iter_mut().find(|c| c.id == connection_id) {
connection.state = state;
self.invalidate_propagation_cache();
Ok(())
} else {
Err(crate::Error::LegacyRoom(format!(
"Connection '{connection_id}' not found"
)))
}
}
}
impl Room {
pub fn new(
id: String,
dimensions: (f32, f32, f32),
reverb_time: f32,
position: Position3D,
) -> crate::Result<Self> {
Ok(Self {
id,
simulator: RoomSimulator::new(dimensions, reverb_time)?,
position,
orientation: (0.0, 0.0, 0.0),
volume_adjustment: 1.0,
})
}
pub fn with_config(
id: String,
config: RoomConfig,
position: Position3D,
) -> crate::Result<Self> {
Ok(Self {
id,
simulator: RoomSimulator::with_config(config)?,
position,
orientation: (0.0, 0.0, 0.0),
volume_adjustment: 1.0,
})
}
}
impl Default for GlobalAcousticConfig {
fn default() -> Self {
Self {
speed_of_sound: 343.0,
temperature: 20.0,
humidity: 50.0,
enable_air_absorption: true,
max_propagation_distance: 100.0,
enable_inter_room_delays: true,
}
}
}
impl Default for ConnectionAcousticProperties {
fn default() -> Self {
Self {
transmission_coefficient: 0.3,
frequency_transmission: vec![
FrequencyBandAbsorption {
frequency: 125.0,
coefficient: 0.2,
},
FrequencyBandAbsorption {
frequency: 500.0,
coefficient: 0.3,
},
FrequencyBandAbsorption {
frequency: 2000.0,
coefficient: 0.4,
},
FrequencyBandAbsorption {
frequency: 8000.0,
coefficient: 0.2,
},
],
attenuation_db: 3.0,
reflection_coefficient: 0.1,
diffraction_enabled: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_room_simulator_creation() {
let simulator = RoomSimulator::new((10.0, 8.0, 3.0), 1.2);
assert!(simulator.is_ok());
}
#[test]
fn test_room_config() {
let config = RoomConfig::new((5.0, 4.0, 3.0), 1.0);
assert_eq!(config.dimensions, (5.0, 4.0, 3.0));
assert_eq!(config.volume, 60.0);
assert!(config.average_absorption() > 0.0);
}
#[test]
fn test_material_properties() {
let concrete = Material::concrete();
let carpet = Material::carpet();
assert!(carpet.average_absorption() > concrete.average_absorption());
}
#[test]
fn test_delay_line() {
let mut delay_line =
DelayLine::new(0.001, 44100.0).expect("Should successfully create delay line");
let output1 = delay_line.process(1.0);
assert_eq!(output1, 0.0);
for _ in 0..50 {
delay_line.process(0.0);
}
let _output2 = delay_line.process(0.0);
}
#[test]
fn test_reflection_path_calculation() {
let simulator = RoomSimulator::new((10.0, 8.0, 3.0), 1.2)
.expect("Should successfully create room simulator");
let source = Position3D::new(2.0, 1.0, 1.0);
let listener = Position3D::new(8.0, 1.0, 2.0);
let paths = simulator
.calculate_reflection_paths(source, listener, 1)
.expect("Should successfully calculate reflection paths");
assert!(!paths.is_empty());
assert!(paths.len() >= 7); }
#[test]
fn test_multi_room_environment_creation() {
let mut env = MultiRoomEnvironment::new();
assert_eq!(env.rooms.len(), 0);
assert_eq!(env.connections.len(), 0);
}
#[test]
fn test_room_creation() {
let room = Room::new(
"living_room".to_string(),
(5.0, 4.0, 3.0),
1.2,
Position3D::new(0.0, 0.0, 0.0),
)
.expect("Should successfully create room");
assert_eq!(room.id, "living_room");
assert_eq!(room.position, Position3D::new(0.0, 0.0, 0.0));
}
#[test]
fn test_multi_room_environment_add_room() {
let mut env = MultiRoomEnvironment::new();
let room = Room::new(
"kitchen".to_string(),
(4.0, 3.0, 2.5),
0.8,
Position3D::new(5.0, 0.0, 0.0),
)
.expect("Should successfully create kitchen");
env.add_room(room)
.expect("Should successfully add room to environment");
assert_eq!(env.rooms.len(), 1);
assert!(env.get_room("kitchen").is_some());
}
#[test]
fn test_room_connection() {
let mut env = MultiRoomEnvironment::new();
let living_room = Room::new(
"living_room".to_string(),
(5.0, 4.0, 3.0),
1.2,
Position3D::new(0.0, 0.0, 0.0),
)
.expect("Should successfully create living room");
let kitchen = Room::new(
"kitchen".to_string(),
(4.0, 3.0, 2.5),
0.8,
Position3D::new(5.0, 0.0, 0.0),
)
.expect("Should successfully create kitchen");
env.add_room(living_room)
.expect("Should successfully add living room");
env.add_room(kitchen)
.expect("Should successfully add kitchen");
let connection = RoomConnection {
id: "door_1".to_string(),
from_room: "living_room".to_string(),
to_room: "kitchen".to_string(),
connection_type: ConnectionType::Door,
from_position: Position3D::new(5.0, 2.0, 1.0),
to_position: Position3D::new(0.0, 2.0, 1.0),
dimensions: (0.8, 2.0),
acoustic_properties: ConnectionAcousticProperties::default(),
state: ConnectionState::Open,
};
env.add_connection(connection)
.expect("Should successfully add connection");
assert_eq!(env.connections.len(), 1);
}
#[test]
fn test_connection_state_changes() {
let mut env = MultiRoomEnvironment::new();
let living_room = Room::new(
"living_room".to_string(),
(5.0, 4.0, 3.0),
1.2,
Position3D::new(0.0, 0.0, 0.0),
)
.expect("Should successfully create living room");
let kitchen = Room::new(
"kitchen".to_string(),
(4.0, 3.0, 2.5),
0.8,
Position3D::new(5.0, 0.0, 0.0),
)
.expect("Should successfully create kitchen");
env.add_room(living_room)
.expect("Should successfully add living room");
env.add_room(kitchen)
.expect("Should successfully add kitchen");
let connection = RoomConnection {
id: "door_1".to_string(),
from_room: "living_room".to_string(),
to_room: "kitchen".to_string(),
connection_type: ConnectionType::Door,
from_position: Position3D::new(5.0, 2.0, 1.0),
to_position: Position3D::new(0.0, 2.0, 1.0),
dimensions: (0.8, 2.0),
acoustic_properties: ConnectionAcousticProperties::default(),
state: ConnectionState::Open,
};
env.add_connection(connection)
.expect("Should successfully add connection");
env.set_connection_state("door_1", ConnectionState::Closed)
.expect("Should successfully set connection state to closed");
assert_eq!(env.connections[0].state, ConnectionState::Closed);
env.set_connection_state("door_1", ConnectionState::PartiallyOpen(0.5))
.expect("Should successfully set connection state to partially open");
assert_eq!(
env.connections[0].state,
ConnectionState::PartiallyOpen(0.5)
);
}
#[tokio::test]
async fn test_multi_room_audio_processing() {
let mut env = MultiRoomEnvironment::new();
let living_room = Room::new(
"living_room".to_string(),
(5.0, 4.0, 3.0),
1.2,
Position3D::new(0.0, 0.0, 0.0),
)
.expect("Should successfully create living room");
let kitchen = Room::new(
"kitchen".to_string(),
(4.0, 3.0, 2.5),
0.8,
Position3D::new(5.0, 0.0, 0.0),
)
.expect("Should successfully create kitchen");
env.add_room(living_room)
.expect("Should successfully add living room");
env.add_room(kitchen)
.expect("Should successfully add kitchen");
let connection = RoomConnection {
id: "door_1".to_string(),
from_room: "living_room".to_string(),
to_room: "kitchen".to_string(),
connection_type: ConnectionType::Door,
from_position: Position3D::new(5.0, 2.0, 1.0),
to_position: Position3D::new(0.0, 2.0, 1.0),
dimensions: (0.8, 2.0),
acoustic_properties: ConnectionAcousticProperties::default(),
state: ConnectionState::Open,
};
env.add_connection(connection)
.expect("Should successfully add connection");
let input_audio = Array1::from_vec(vec![0.5; 1000]);
let source_pos = Position3D::new(2.0, 2.0, 1.5);
let listener_pos = Position3D::new(2.0, 1.5, 1.5);
let result = env
.process_multi_room_audio(
"living_room",
source_pos,
"living_room",
listener_pos,
&input_audio,
44100,
)
.await;
assert!(result.is_ok());
let (left, right) = result.expect("Should successfully process multi-room audio");
assert_eq!(left.len(), input_audio.len());
assert_eq!(right.len(), input_audio.len());
}
}