use serde::{Deserialize, Serialize};
use goonj::analysis::{clarity_c50, clarity_c80, definition_d50, sti_estimate};
use goonj::impulse::ImpulseResponse;
use crate::error::{NaadError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoomMetrics {
pub c50: f32,
pub c80: f32,
pub d50: f32,
pub sti: f32,
pub rt60: f32,
}
pub fn analyze_impulse_response(ir: &[f32], sample_rate: u32) -> Result<RoomMetrics> {
if ir.is_empty() {
return Err(NaadError::ComputationError {
message: "impulse response is empty".into(),
});
}
if sample_rate == 0 {
return Err(NaadError::ComputationError {
message: "sample rate must be > 0".into(),
});
}
let goonj_ir = ImpulseResponse {
samples: ir.to_vec(),
sample_rate,
rt60: 0.0, };
let c50 = clarity_c50(&goonj_ir);
let c80 = clarity_c80(&goonj_ir);
let d50 = definition_d50(&goonj_ir);
let sti = sti_estimate(&goonj_ir);
let rt60 = estimate_rt60(ir, sample_rate);
Ok(RoomMetrics {
c50,
c80,
d50,
sti,
rt60,
})
}
#[must_use]
#[inline]
pub fn estimate_rt60(ir: &[f32], sample_rate: u32) -> f32 {
if ir.is_empty() || sample_rate == 0 {
return 0.0;
}
let total_energy: f32 = ir.iter().map(|&s| s * s).sum();
if total_energy < f32::EPSILON {
return 0.0;
}
let mut cumulative = 0.0_f32;
let threshold = total_energy * 0.001;
for (i, &s) in ir.iter().enumerate() {
cumulative += s * s;
if cumulative >= total_energy - threshold {
let t30 = i as f32 / sample_rate as f32;
return t30 * 2.0;
}
}
ir.len() as f32 / sample_rate as f32
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_ir(sample_rate: u32, duration_secs: f32, decay_rate: f32) -> Vec<f32> {
let len = (sample_rate as f32 * duration_secs) as usize;
(0..len)
.map(|i| {
let t = i as f32 / sample_rate as f32;
(-decay_rate * t).exp()
})
.collect()
}
#[test]
fn test_analyze_synthetic_ir() {
let ir = synthetic_ir(48000, 1.0, 5.0);
let metrics = analyze_impulse_response(&ir, 48000).unwrap();
assert!(metrics.c50.is_finite(), "C50 should be finite");
assert!(metrics.c80.is_finite(), "C80 should be finite");
assert!(metrics.d50.is_finite(), "D50 should be finite");
assert!(metrics.sti.is_finite(), "STI should be finite");
assert!(metrics.rt60.is_finite(), "RT60 should be finite");
assert!(metrics.rt60 > 0.0, "RT60 should be positive");
}
#[test]
fn test_empty_ir_errors() {
assert!(analyze_impulse_response(&[], 48000).is_err());
}
#[test]
fn test_zero_sample_rate_errors() {
assert!(analyze_impulse_response(&[1.0, 0.5], 0).is_err());
}
#[test]
fn test_estimate_rt60_basic() {
let ir = synthetic_ir(48000, 2.0, 3.0);
let rt60 = estimate_rt60(&ir, 48000);
assert!(rt60 > 0.0, "RT60 should be positive");
assert!(rt60.is_finite(), "RT60 should be finite");
}
#[test]
fn test_estimate_rt60_empty() {
assert_eq!(estimate_rt60(&[], 48000), 0.0);
}
#[test]
fn test_serde_roundtrip() {
let ir = synthetic_ir(48000, 0.5, 5.0);
let metrics = analyze_impulse_response(&ir, 48000).unwrap();
let json = serde_json::to_string(&metrics).unwrap();
let back: RoomMetrics = serde_json::from_str(&json).unwrap();
assert!((metrics.c50 - back.c50).abs() < f32::EPSILON);
assert!((metrics.sti - back.sti).abs() < f32::EPSILON);
}
}