use crate::diffraction::{edge_diffraction_loss, is_occluded};
use crate::material::FREQUENCY_BANDS;
use crate::room::{AcceleratedRoom, AcousticRoom};
use hisab::Vec3;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OcclusionResult {
pub is_occluded: bool,
pub attenuation_db: f32,
pub frequency_dependent: [f32; crate::material::NUM_BANDS],
}
#[derive(Debug, Clone)]
pub struct OcclusionEngine {
accel: AcceleratedRoom,
}
impl OcclusionEngine {
#[must_use]
pub fn new(room: AcousticRoom) -> Self {
Self {
accel: AcceleratedRoom::new(room),
}
}
#[must_use]
#[tracing::instrument(skip(self))]
pub fn query(&self, source: Vec3, listener: Vec3) -> OcclusionResult {
let walls = &self.accel.room.geometry.walls;
let occluded = is_occluded(source, listener, walls);
if !occluded {
return OcclusionResult {
is_occluded: false,
attenuation_db: 0.0,
frequency_dependent: [0.0; crate::material::NUM_BANDS],
};
}
let edge_angle = std::f32::consts::FRAC_PI_4;
let frequency_dependent = std::array::from_fn(|band| {
edge_diffraction_loss(
FREQUENCY_BANDS[band],
edge_angle,
self.accel.room.temperature_celsius,
)
});
let avg_atten = frequency_dependent.iter().sum::<f32>() / frequency_dependent.len() as f32;
OcclusionResult {
is_occluded: true,
attenuation_db: avg_atten,
frequency_dependent,
}
}
#[must_use]
pub fn room(&self) -> &AcousticRoom {
&self.accel.room
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::material::AcousticMaterial;
use crate::room::Wall;
#[test]
fn unoccluded_path_zero_attenuation() {
let room = AcousticRoom::shoebox(10.0, 8.0, 3.0, AcousticMaterial::concrete());
let engine = OcclusionEngine::new(room);
let result = engine.query(Vec3::new(3.0, 1.5, 4.0), Vec3::new(7.0, 1.5, 4.0));
assert!(!result.is_occluded);
assert!((result.attenuation_db).abs() < f32::EPSILON);
}
#[test]
fn occluded_path_has_attenuation() {
let mut room = AcousticRoom::shoebox(20.0, 20.0, 3.0, AcousticMaterial::concrete());
room.geometry.walls.push(Wall {
vertices: vec![
Vec3::new(10.0, -5.0, 5.0),
Vec3::new(10.0, 5.0, 5.0),
Vec3::new(10.0, 5.0, -5.0),
Vec3::new(10.0, -5.0, -5.0),
],
material: AcousticMaterial::concrete(),
normal: Vec3::new(-1.0, 0.0, 0.0),
});
let engine = OcclusionEngine::new(room);
let result = engine.query(Vec3::new(5.0, 0.0, 0.0), Vec3::new(15.0, 0.0, 0.0));
assert!(result.is_occluded);
assert!(
result.attenuation_db < 0.0,
"occlusion should produce negative attenuation"
);
}
#[test]
fn frequency_dependent_attenuation() {
let mut room = AcousticRoom::shoebox(20.0, 20.0, 3.0, AcousticMaterial::concrete());
room.geometry.walls.push(Wall {
vertices: vec![
Vec3::new(10.0, -5.0, 5.0),
Vec3::new(10.0, 5.0, 5.0),
Vec3::new(10.0, 5.0, -5.0),
Vec3::new(10.0, -5.0, -5.0),
],
material: AcousticMaterial::concrete(),
normal: Vec3::new(-1.0, 0.0, 0.0),
});
let engine = OcclusionEngine::new(room);
let result = engine.query(Vec3::new(5.0, 0.0, 0.0), Vec3::new(15.0, 0.0, 0.0));
assert!(
result.frequency_dependent[5] < result.frequency_dependent[0],
"high freq ({}) should be more attenuated than low ({})",
result.frequency_dependent[5],
result.frequency_dependent[0]
);
}
#[test]
fn occlusion_result_serializes() {
let result = OcclusionResult {
is_occluded: true,
attenuation_db: -6.0,
frequency_dependent: [-2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0, -9.0],
};
let json = serde_json::to_string(&result);
assert!(json.is_ok());
}
#[test]
fn occlusion_engine_room_accessor() {
let room = AcousticRoom::shoebox(10.0, 8.0, 3.0, AcousticMaterial::concrete());
let engine = OcclusionEngine::new(room);
assert_eq!(engine.room().geometry.walls.len(), 6);
}
}