pub mod bands;
mod config;
mod detection;
pub mod metrics;
mod mixing_time;
mod segmentation;
mod types;
pub use bands::{
BandWidth, ISO_OCTAVE_CENTERS_HZ, ISO_THIRD_OCTAVE_CENTERS_HZ, analyze_iso3382_bands,
analyze_iso3382_octaves, analyze_iso3382_third_octaves, bandpass,
};
pub use config::SsirConfig;
pub use math_audio_iir_fir::filtfilt;
pub use metrics::{
DecayCurve, Iso3382Metrics, analyze_iso3382, estimate_noise_cutoff, schroeder_curve,
};
pub use types::{RirSegment, SsirResult};
use detection::{detect_reflections, find_direct_sound_toa};
use mixing_time::estimate_mixing_time;
use rayon::prelude::*;
use segmentation::build_segments;
pub fn analyze_rir(rir: &[f32], config: &SsirConfig) -> SsirResult {
if rir.is_empty() {
return SsirResult {
segments: Vec::new(),
mixing_time_samples: 0,
sample_rate: config.sample_rate,
};
}
let mixing_time_samples = if config.mixing_time_ms.is_some() {
config.mixing_time_samples()
} else {
estimate_mixing_time(rir, config.sample_rate)
};
let direct_sound_toa = match find_direct_sound_toa(rir, config) {
Some(toa) => toa,
None => {
return SsirResult {
segments: Vec::new(),
mixing_time_samples,
sample_rate: config.sample_rate,
};
}
};
let reflections = detect_reflections(rir, direct_sound_toa, None, config);
let segments = build_segments(
rir,
direct_sound_toa,
None,
&reflections,
mixing_time_samples,
config,
);
SsirResult {
segments,
mixing_time_samples,
sample_rate: config.sample_rate,
}
}
pub fn analyze_srir(channels: &[&[f32]], config: &SsirConfig) -> SsirResult {
if channels.is_empty() || channels[0].is_empty() {
return SsirResult {
segments: Vec::new(),
mixing_time_samples: 0,
sample_rate: config.sample_rate,
};
}
let omni = channels[0];
if channels.len() < 4 {
return analyze_rir(omni, config);
}
let len = omni.len();
if channels.iter().any(|ch| ch.len() != len) {
return analyze_rir(omni, config);
}
let mixing_time_samples = if config.mixing_time_ms.is_some() {
config.mixing_time_samples()
} else {
estimate_mixing_time(omni, config.sample_rate)
};
let direct_sound_toa = match find_direct_sound_toa(omni, config) {
Some(toa) => toa,
None => {
return SsirResult {
segments: Vec::new(),
mixing_time_samples,
sample_rate: config.sample_rate,
};
}
};
let doa_vectors = compute_bformat_doa(channels, len, config);
let reflections = detect_reflections(omni, direct_sound_toa, Some(&doa_vectors), config);
let ds_doa = doa_vectors.get(direct_sound_toa).copied();
let segments = build_segments(
omni,
direct_sound_toa,
ds_doa,
&reflections,
mixing_time_samples,
config,
);
SsirResult {
segments,
mixing_time_samples,
sample_rate: config.sample_rate,
}
}
fn compute_bformat_doa(channels: &[&[f32]], len: usize, config: &SsirConfig) -> Vec<[f32; 3]> {
let (low_hz, high_hz) = config.doa_bandpass_hz;
let order = config.doa_bandpass_order;
let nyquist = config.sample_rate / 2.0;
let needs_filtering = low_hz > 0.0 && high_hz < nyquist && len >= 4 && order >= 1;
let owned: Option<[Vec<f32>; 4]> = if needs_filtering {
let mut sections =
filtfilt::peq_to_coefficients(&math_audio_iir_fir::peq_butterworth_highpass(
order as usize,
low_hz,
config.sample_rate,
));
sections.extend(filtfilt::peq_to_coefficients(
&math_audio_iir_fir::peq_butterworth_lowpass(
order as usize,
high_hz,
config.sample_rate,
),
));
let filter_channel = |ch: &[f32]| -> Vec<f32> {
let mut scratch: Vec<f64> = Vec::with_capacity(ch.len());
scratch.extend(ch.iter().map(|&s| s as f64));
let out_f64 = filtfilt::filtfilt(&scratch, §ions);
let mut out_f32: Vec<f32> = Vec::with_capacity(out_f64.len());
out_f32.extend(out_f64.into_iter().map(|s| s as f32));
out_f32
};
let ((w, x), (y, z)) = rayon::join(
|| {
rayon::join(
|| filter_channel(channels[0]),
|| filter_channel(channels[1]),
)
},
|| {
rayon::join(
|| filter_channel(channels[2]),
|| filter_channel(channels[3]),
)
},
);
Some([w, x, y, z])
} else {
None
};
let (w, x, y, z): (&[f32], &[f32], &[f32], &[f32]) = if let Some(o) = owned.as_ref() {
(&o[0], &o[1], &o[2], &o[3])
} else {
(channels[0], channels[1], channels[2], channels[3])
};
(0..len)
.into_par_iter()
.map(|i| {
let p = w[i] as f64;
let ix = p * x[i] as f64;
let iy = p * y[i] as f64;
let iz = p * z[i] as f64;
let mag = (ix * ix + iy * iy + iz * iz).sqrt();
if mag < 1e-12 {
[0.0f32, 0.0, 0.0]
} else {
let inv = 1.0 / mag;
[(ix * inv) as f32, (iy * inv) as f32, (iz * inv) as f32]
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_synthetic_rir(
sample_rate: f64,
reflection_times_ms: &[f64],
reflection_gains: &[f32],
) -> Vec<f32> {
let duration_ms = 100.0;
let len = (duration_ms * sample_rate / 1000.0) as usize;
let mut rir = vec![0.0001f32; len];
let ds_sample = (1.0 * sample_rate / 1000.0) as usize;
rir[ds_sample] = 1.0;
for (&time_ms, &gain) in reflection_times_ms.iter().zip(reflection_gains.iter()) {
let sample = (time_ms * sample_rate / 1000.0) as usize;
if sample < len {
rir[sample] = gain;
}
}
rir
}
#[test]
fn test_analyze_rir_basic() {
let rir = make_synthetic_rir(48000.0, &[6.0, 10.0, 15.0, 22.0], &[0.5, 0.3, 0.25, 0.15]);
let config = SsirConfig {
sample_rate: 48000.0,
mixing_time_ms: Some(40.0),
..SsirConfig::default()
};
let result = analyze_rir(&rir, &config);
assert!(
result.num_events() >= 3,
"expected >= 3 events, got {}",
result.num_events()
);
assert!(result.segments[0].is_direct_sound);
for i in 0..result.segments.len() - 1 {
assert_eq!(
result.segments[i].end_sample,
result.segments[i + 1].onset_sample,
"segments {} and {} are not consecutive",
i,
i + 1
);
}
for seg in result.reflections() {
let toa_ms = seg.toa_ms(48000.0);
assert!(
toa_ms > 1.0 && toa_ms < 40.0,
"reflection TOA {toa_ms:.1}ms outside expected range"
);
}
}
#[test]
fn test_analyze_rir_empty() {
let config = SsirConfig::new(48000.0);
let result = analyze_rir(&[], &config);
assert_eq!(result.num_events(), 0);
}
#[test]
fn test_analyze_rir_single_impulse() {
let mut rir = vec![0.0001f32; 4800]; rir[48] = 1.0;
let config = SsirConfig {
sample_rate: 48000.0,
mixing_time_ms: Some(40.0),
..SsirConfig::default()
};
let result = analyze_rir(&rir, &config);
assert!(result.num_events() >= 1);
assert!(result.segments[0].is_direct_sound);
}
#[test]
fn test_analyze_srir_fallback_to_mono() {
let rir = make_synthetic_rir(48000.0, &[6.0, 10.0], &[0.5, 0.3]);
let config = SsirConfig {
sample_rate: 48000.0,
mixing_time_ms: Some(40.0),
..SsirConfig::default()
};
let result = analyze_srir(&[&rir, &rir], &config);
assert!(result.num_events() >= 2);
}
#[test]
fn test_compute_bformat_doa_plane_wave_front() {
let len = 1024;
let mut w = vec![0.0f32; len];
let mut x = vec![0.0f32; len];
let y = vec![0.0f32; len];
let z = vec![0.0f32; len];
for i in 100..120 {
let s = (-(i as f32 - 110.0).powi(2) / 4.0).exp();
w[i] = s;
x[i] = s;
}
let config = SsirConfig {
sample_rate: 48000.0,
doa_bandpass_hz: (0.0, 96000.0),
doa_bandpass_order: 0,
..SsirConfig::default()
};
let doa = compute_bformat_doa(&[&w, &x, &y, &z], len, &config);
let d = doa[110];
assert!(d[0] > 0.99, "expected DOA[x] ≈ +1, got {:?}", d);
assert!(d[1].abs() < 0.05, "expected DOA[y] ≈ 0, got {:?}", d);
assert!(d[2].abs() < 0.05, "expected DOA[z] ≈ 0, got {:?}", d);
}
#[test]
fn test_compute_bformat_doa_plane_wave_left() {
let len = 1024;
let mut w = vec![0.0f32; len];
let x = vec![0.0f32; len];
let mut y = vec![0.0f32; len];
let z = vec![0.0f32; len];
for i in 100..120 {
let s = (-(i as f32 - 110.0).powi(2) / 4.0).exp();
w[i] = s;
y[i] = s;
}
let config = SsirConfig {
sample_rate: 48000.0,
doa_bandpass_hz: (0.0, 96000.0),
doa_bandpass_order: 0,
..SsirConfig::default()
};
let doa = compute_bformat_doa(&[&w, &x, &y, &z], len, &config);
let d = doa[110];
assert!(d[0].abs() < 0.05, "expected DOA[x] ≈ 0, got {:?}", d);
assert!(d[1] > 0.99, "expected DOA[y] ≈ +1, got {:?}", d);
assert!(d[2].abs() < 0.05, "expected DOA[z] ≈ 0, got {:?}", d);
}
#[test]
fn test_analyze_srir_bformat() {
let len = 4800;
let mut w = vec![0.0001f32; len]; let mut x = vec![0.0f32; len]; let mut y = vec![0.0f32; len]; let z = vec![0.0f32; len];
w[48] = 1.0;
x[48] = 1.0;
y[48] = 0.0;
w[288] = 0.5;
x[288] = 0.0;
y[288] = 0.5;
w[480] = 0.3;
x[480] = 0.0;
y[480] = -0.3;
let config = SsirConfig {
sample_rate: 48000.0,
mixing_time_ms: Some(40.0),
..SsirConfig::default()
};
let result = analyze_srir(&[&w, &x, &y, &z], &config);
assert!(
result.num_events() >= 2,
"expected >= 2 events, got {}",
result.num_events()
);
for seg in &result.segments {
assert!(seg.doa.is_some(), "SRIR segments should have DOA data");
}
let ds_doa = result
.direct_sound_doa()
.expect("direct sound should carry DOA");
assert!(
ds_doa[0] > 0.5,
"front direct sound should point toward +X, got {:?}",
ds_doa
);
}
#[test]
fn test_segments_cover_early_rir() {
let rir = make_synthetic_rir(48000.0, &[6.0, 12.0, 20.0], &[0.5, 0.3, 0.2]);
let config = SsirConfig {
sample_rate: 48000.0,
mixing_time_ms: Some(40.0),
..SsirConfig::default()
};
let result = analyze_rir(&rir, &config);
assert_eq!(result.segments[0].onset_sample, 0);
for seg in &result.segments {
assert!(!seg.is_empty(), "segment should have non-zero length");
}
}
#[test]
fn test_mixing_time_auto_estimation() {
let sample_rate = 48000.0;
let len = (0.200 * sample_rate) as usize;
let mut rir = vec![0.0f32; len];
rir[48] = 1.0;
rir[240] = 0.5;
rir[480] = 0.3;
let reverb_start = (0.030 * sample_rate) as usize;
let mut amp = 0.08f32;
let mut rng: u32 = 12345;
for sample in rir.iter_mut().take(len).skip(reverb_start) {
rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
let noise = ((rng >> 16) as f32 / 32768.0) - 1.0;
*sample += noise * amp;
amp *= 0.9997;
}
let config = SsirConfig {
sample_rate,
mixing_time_ms: None, ..SsirConfig::default()
};
let result = analyze_rir(&rir, &config);
let mt_ms = result.mixing_time_ms();
assert!(
(10.0..=80.0).contains(&mt_ms),
"auto mixing time {mt_ms:.1}ms outside expected range"
);
}
#[test]
fn test_analyze_rir_very_short() {
let rir = vec![0.5f32; 10];
let config = SsirConfig::new(48000.0);
let result = analyze_rir(&rir, &config);
assert!(result.num_events() <= 1);
}
#[test]
fn test_analyze_rir_all_zeros() {
let rir = vec![0.0f32; 4800];
let config = SsirConfig {
sample_rate: 48000.0,
mixing_time_ms: Some(40.0),
..SsirConfig::default()
};
let result = analyze_rir(&rir, &config);
assert_eq!(result.num_events(), 0);
}
#[test]
fn test_analyze_rir_dc_offset() {
let mut rir = vec![0.1f32; 4800];
rir[48] = 1.0;
rir[288] = 0.6;
let config = SsirConfig {
sample_rate: 48000.0,
mixing_time_ms: Some(40.0),
..SsirConfig::default()
};
let result = analyze_rir(&rir, &config);
assert!(result.num_events() >= 1);
}
#[test]
fn test_segment_duration_ms_accuracy() {
let seg = RirSegment {
onset_sample: 0,
end_sample: 480,
toa_sample: 48,
doa: None,
peak_energy: 1.0,
is_direct_sound: true,
};
let dur = seg.duration_ms(48000.0);
assert!((dur - 10.0).abs() < 0.01, "expected 10ms, got {dur}ms");
}
#[test]
fn test_direct_sound_toa_at_rir_boundary() {
let mut rir = vec![0.0001f32; 2400];
rir[0] = 1.0;
rir[288] = 0.3;
let config = SsirConfig {
sample_rate: 48000.0,
mixing_time_ms: Some(40.0),
..SsirConfig::default()
};
let result = analyze_rir(&rir, &config);
assert!(result.num_events() >= 1);
assert!(result.segments[0].is_direct_sound);
assert_eq!(result.segments[0].toa_sample, 0);
}
}