use serde::{Deserialize, Serialize};
use goonj::coupled::{CoupledRooms, coupled_room_decay};
use goonj::portal::Portal;
use goonj::room::AcousticRoom;
use hisab::Vec3;
use crate::error::{NaadError, Result};
use super::room::RoomReverbConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoupledPortal {
pub position: [f32; 3],
pub normal: [f32; 3],
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoupledRoomConfig {
pub room_a: RoomReverbConfig,
pub room_b: RoomReverbConfig,
pub portal: CoupledPortal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoupledDecayResult {
pub rt60_early: f32,
pub rt60_late: f32,
pub early_amplitude: f32,
pub coupling_strength: f32,
}
pub fn analyze_coupled_decay(config: &CoupledRoomConfig) -> Result<CoupledDecayResult> {
let room_a = build_room(&config.room_a)?;
let room_b = build_room(&config.room_b)?;
let portal = Portal {
position: Vec3::new(
config.portal.position[0],
config.portal.position[1],
config.portal.position[2],
),
normal: Vec3::new(
config.portal.normal[0],
config.portal.normal[1],
config.portal.normal[2],
),
width: config.portal.width,
height: config.portal.height,
};
let coupled = CoupledRooms {
room_a,
room_b,
portal,
};
let decay = coupled_room_decay(&coupled);
Ok(CoupledDecayResult {
rt60_early: decay.rt60_early,
rt60_late: decay.rt60_late,
early_amplitude: decay.early_amplitude,
coupling_strength: decay.coupling_strength,
})
}
fn build_room(config: &RoomReverbConfig) -> Result<AcousticRoom> {
let material = super::material_by_name(&config.wall_material_name).ok_or_else(|| {
NaadError::ComputationError {
message: format!("unknown wall material: {}", config.wall_material_name),
}
})?;
if config.length <= 0.0 || config.width <= 0.0 || config.height <= 0.0 {
return Err(NaadError::ComputationError {
message: "room dimensions must be positive".into(),
});
}
Ok(AcousticRoom::shoebox(
config.length,
config.width,
config.height,
material,
))
}
#[cfg(test)]
mod tests {
use super::*;
fn small_room() -> RoomReverbConfig {
RoomReverbConfig {
length: 5.0,
width: 4.0,
height: 3.0,
wall_material_name: "carpet".to_string(),
source_position: [1.0, 1.5, 1.0],
listener_position: [4.0, 1.5, 3.0],
sample_rate: 48000,
}
}
fn large_room() -> RoomReverbConfig {
RoomReverbConfig {
length: 20.0,
width: 15.0,
height: 8.0,
wall_material_name: "concrete".to_string(),
source_position: [10.0, 4.0, 7.0],
listener_position: [5.0, 4.0, 7.0],
sample_rate: 48000,
}
}
fn portal() -> CoupledPortal {
CoupledPortal {
position: [5.0, 1.5, 2.0],
normal: [1.0, 0.0, 0.0],
width: 1.0,
height: 2.0,
}
}
#[test]
fn test_coupled_decay_two_rooms() {
let cfg = CoupledRoomConfig {
room_a: small_room(),
room_b: large_room(),
portal: portal(),
};
let decay = analyze_coupled_decay(&cfg).unwrap();
assert!(decay.rt60_early.is_finite() && decay.rt60_early > 0.0);
assert!(decay.rt60_late.is_finite() && decay.rt60_late > 0.0);
assert!((0.0..=1.0).contains(&decay.early_amplitude));
assert!((0.0..=1.0).contains(&decay.coupling_strength));
}
#[test]
fn test_coupled_decay_late_is_longer_for_reverberant_room_b() {
let cfg = CoupledRoomConfig {
room_a: small_room(),
room_b: large_room(),
portal: portal(),
};
let decay = analyze_coupled_decay(&cfg).unwrap();
assert!(
decay.rt60_late >= decay.rt60_early,
"concrete coupled room should sustain late decay: early={}, late={}",
decay.rt60_early,
decay.rt60_late
);
}
#[test]
fn test_invalid_material_errors() {
let mut a = small_room();
a.wall_material_name = "kryptonite".to_string();
let cfg = CoupledRoomConfig {
room_a: a,
room_b: large_room(),
portal: portal(),
};
assert!(analyze_coupled_decay(&cfg).is_err());
}
#[test]
fn test_serde_roundtrip() {
let cfg = CoupledRoomConfig {
room_a: small_room(),
room_b: large_room(),
portal: portal(),
};
let json = serde_json::to_string(&cfg).unwrap();
let back: CoupledRoomConfig = serde_json::from_str(&json).unwrap();
assert!((back.portal.width - 1.0).abs() < f32::EPSILON);
}
}