1mod config;
37mod detection;
38mod mixing_time;
39mod segmentation;
40mod types;
41
42pub use config::SsirConfig;
43pub use math_audio_iir_fir::filtfilt;
44pub use types::{RirSegment, SsirResult};
45
46use detection::{detect_reflections, find_direct_sound_toa};
47use mixing_time::estimate_mixing_time;
48use segmentation::build_segments;
49
50pub fn analyze_rir(rir: &[f32], config: &SsirConfig) -> SsirResult {
60 if rir.is_empty() {
61 return SsirResult {
62 segments: Vec::new(),
63 mixing_time_samples: 0,
64 sample_rate: config.sample_rate,
65 };
66 }
67
68 let mixing_time_samples = if config.mixing_time_ms.is_some() {
70 config.mixing_time_samples()
71 } else {
72 estimate_mixing_time(rir, config.sample_rate)
73 };
74
75 let direct_sound_toa = match find_direct_sound_toa(rir, config) {
77 Some(toa) => toa,
78 None => {
79 return SsirResult {
81 segments: Vec::new(),
82 mixing_time_samples,
83 sample_rate: config.sample_rate,
84 };
85 }
86 };
87
88 let reflections = detect_reflections(rir, direct_sound_toa, None, config);
90
91 let segments = build_segments(
93 rir,
94 direct_sound_toa,
95 None,
96 &reflections,
97 mixing_time_samples,
98 config,
99 );
100
101 SsirResult {
102 segments,
103 mixing_time_samples,
104 sample_rate: config.sample_rate,
105 }
106}
107
108pub fn analyze_srir(channels: &[&[f32]], config: &SsirConfig) -> SsirResult {
119 if channels.is_empty() || channels[0].is_empty() {
120 return SsirResult {
121 segments: Vec::new(),
122 mixing_time_samples: 0,
123 sample_rate: config.sample_rate,
124 };
125 }
126
127 let omni = channels[0];
129
130 if channels.len() < 4 {
132 return analyze_rir(omni, config);
133 }
134
135 let len = omni.len();
137 if channels.iter().any(|ch| ch.len() != len) {
138 return analyze_rir(omni, config);
139 }
140
141 let mixing_time_samples = if config.mixing_time_ms.is_some() {
143 config.mixing_time_samples()
144 } else {
145 estimate_mixing_time(omni, config.sample_rate)
146 };
147
148 let direct_sound_toa = match find_direct_sound_toa(omni, config) {
150 Some(toa) => toa,
151 None => {
152 return SsirResult {
153 segments: Vec::new(),
154 mixing_time_samples,
155 sample_rate: config.sample_rate,
156 };
157 }
158 };
159
160 let doa_vectors = compute_bformat_doa(channels, len, config);
163
164 let reflections = detect_reflections(omni, direct_sound_toa, Some(&doa_vectors), config);
166
167 let ds_doa = doa_vectors.get(direct_sound_toa).copied();
169 let segments = build_segments(
170 omni,
171 direct_sound_toa,
172 ds_doa,
173 &reflections,
174 mixing_time_samples,
175 config,
176 );
177
178 SsirResult {
179 segments,
180 mixing_time_samples,
181 sample_rate: config.sample_rate,
182 }
183}
184
185fn compute_bformat_doa(channels: &[&[f32]], len: usize, config: &SsirConfig) -> Vec<[f32; 3]> {
195 let (low_hz, high_hz) = config.doa_bandpass_hz;
196 let order = config.doa_bandpass_order;
197 let nyquist = config.sample_rate / 2.0;
198
199 let needs_filtering = low_hz > 0.0 && high_hz < nyquist && len >= 4 && order >= 1;
202
203 let (w, x, y, z): (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>) = if needs_filtering {
204 let mut sections =
205 filtfilt::peq_to_coefficients(&math_audio_iir_fir::peq_butterworth_highpass(
206 order as usize,
207 low_hz,
208 config.sample_rate,
209 ));
210 sections.extend(filtfilt::peq_to_coefficients(
211 &math_audio_iir_fir::peq_butterworth_lowpass(
212 order as usize,
213 high_hz,
214 config.sample_rate,
215 ),
216 ));
217 let filter_channel = |ch: &[f32]| -> Vec<f32> {
219 let ch_f64: Vec<f64> = ch.iter().map(|&s| s as f64).collect();
220 filtfilt::filtfilt(&ch_f64, §ions)
221 .into_iter()
222 .map(|s| s as f32)
223 .collect()
224 };
225 (
226 filter_channel(channels[0]),
227 filter_channel(channels[1]),
228 filter_channel(channels[2]),
229 filter_channel(channels[3]),
230 )
231 } else {
232 (
233 channels[0].to_vec(),
234 channels[1].to_vec(),
235 channels[2].to_vec(),
236 channels[3].to_vec(),
237 )
238 };
239
240 (0..len)
241 .map(|i| {
242 let p = w[i] as f64;
243 let ix = p * x[i] as f64;
245 let iy = p * y[i] as f64;
246 let iz = p * z[i] as f64;
247
248 let mag = (ix * ix + iy * iy + iz * iz).sqrt();
249 if mag < 1e-12 {
250 [0.0f32, 0.0, 0.0]
251 } else {
252 [(ix / mag) as f32, (iy / mag) as f32, (iz / mag) as f32]
253 }
254 })
255 .collect()
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn make_synthetic_rir(
264 sample_rate: f64,
265 reflection_times_ms: &[f64],
266 reflection_gains: &[f32],
267 ) -> Vec<f32> {
268 let duration_ms = 100.0;
269 let len = (duration_ms * sample_rate / 1000.0) as usize;
270 let mut rir = vec![0.0001f32; len]; let ds_sample = (1.0 * sample_rate / 1000.0) as usize;
274 rir[ds_sample] = 1.0;
275
276 for (&time_ms, &gain) in reflection_times_ms.iter().zip(reflection_gains.iter()) {
278 let sample = (time_ms * sample_rate / 1000.0) as usize;
279 if sample < len {
280 rir[sample] = gain;
281 }
282 }
283
284 rir
285 }
286
287 #[test]
288 fn test_analyze_rir_basic() {
289 let rir = make_synthetic_rir(48000.0, &[6.0, 10.0, 15.0, 22.0], &[0.5, 0.3, 0.25, 0.15]);
290
291 let config = SsirConfig {
292 sample_rate: 48000.0,
293 mixing_time_ms: Some(40.0),
294 ..SsirConfig::default()
295 };
296
297 let result = analyze_rir(&rir, &config);
298
299 assert!(
301 result.num_events() >= 3,
302 "expected >= 3 events, got {}",
303 result.num_events()
304 );
305 assert!(result.segments[0].is_direct_sound);
306
307 for i in 0..result.segments.len() - 1 {
309 assert_eq!(
310 result.segments[i].end_sample,
311 result.segments[i + 1].onset_sample,
312 "segments {} and {} are not consecutive",
313 i,
314 i + 1
315 );
316 }
317
318 for seg in result.reflections() {
320 let toa_ms = seg.toa_ms(48000.0);
321 assert!(
322 toa_ms > 1.0 && toa_ms < 40.0,
323 "reflection TOA {toa_ms:.1}ms outside expected range"
324 );
325 }
326 }
327
328 #[test]
329 fn test_analyze_rir_empty() {
330 let config = SsirConfig::new(48000.0);
331 let result = analyze_rir(&[], &config);
332 assert_eq!(result.num_events(), 0);
333 }
334
335 #[test]
336 fn test_analyze_rir_single_impulse() {
337 let mut rir = vec![0.0001f32; 4800]; rir[48] = 1.0;
340
341 let config = SsirConfig {
342 sample_rate: 48000.0,
343 mixing_time_ms: Some(40.0),
344 ..SsirConfig::default()
345 };
346
347 let result = analyze_rir(&rir, &config);
348
349 assert!(result.num_events() >= 1);
351 assert!(result.segments[0].is_direct_sound);
352 }
353
354 #[test]
355 fn test_analyze_srir_fallback_to_mono() {
356 let rir = make_synthetic_rir(48000.0, &[6.0, 10.0], &[0.5, 0.3]);
357
358 let config = SsirConfig {
359 sample_rate: 48000.0,
360 mixing_time_ms: Some(40.0),
361 ..SsirConfig::default()
362 };
363
364 let result = analyze_srir(&[&rir, &rir], &config);
366 assert!(result.num_events() >= 2);
367 }
368
369 #[test]
370 fn test_analyze_srir_bformat() {
371 let len = 4800;
372 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;
379 x[48] = 1.0;
380 y[48] = 0.0;
381
382 w[288] = 0.5;
384 x[288] = 0.0;
385 y[288] = 0.5;
386
387 w[480] = 0.3;
389 x[480] = 0.0;
390 y[480] = -0.3;
391
392 let config = SsirConfig {
393 sample_rate: 48000.0,
394 mixing_time_ms: Some(40.0),
395 ..SsirConfig::default()
396 };
397
398 let result = analyze_srir(&[&w, &x, &y, &z], &config);
399
400 assert!(
401 result.num_events() >= 2,
402 "expected >= 2 events, got {}",
403 result.num_events()
404 );
405
406 for seg in &result.segments {
408 assert!(seg.doa.is_some(), "SRIR segments should have DOA data");
409 }
410 }
411
412 #[test]
413 fn test_segments_cover_early_rir() {
414 let rir = make_synthetic_rir(48000.0, &[6.0, 12.0, 20.0], &[0.5, 0.3, 0.2]);
415
416 let config = SsirConfig {
417 sample_rate: 48000.0,
418 mixing_time_ms: Some(40.0),
419 ..SsirConfig::default()
420 };
421
422 let result = analyze_rir(&rir, &config);
423
424 assert_eq!(result.segments[0].onset_sample, 0);
426
427 for seg in &result.segments {
429 assert!(!seg.is_empty(), "segment should have non-zero length");
430 }
431 }
432
433 #[test]
434 fn test_mixing_time_auto_estimation() {
435 let sample_rate = 48000.0;
437 let len = (0.200 * sample_rate) as usize;
438 let mut rir = vec![0.0f32; len];
439
440 rir[48] = 1.0;
442 rir[240] = 0.5;
444 rir[480] = 0.3;
445
446 let reverb_start = (0.030 * sample_rate) as usize;
448 let mut amp = 0.08f32;
449 let mut rng: u32 = 12345;
450 for sample in rir.iter_mut().take(len).skip(reverb_start) {
451 rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
452 let noise = ((rng >> 16) as f32 / 32768.0) - 1.0;
453 *sample += noise * amp;
454 amp *= 0.9997;
455 }
456
457 let config = SsirConfig {
458 sample_rate,
459 mixing_time_ms: None, ..SsirConfig::default()
461 };
462
463 let result = analyze_rir(&rir, &config);
464
465 let mt_ms = result.mixing_time_ms();
467 assert!(
468 (10.0..=80.0).contains(&mt_ms),
469 "auto mixing time {mt_ms:.1}ms outside expected range"
470 );
471 }
472
473 #[test]
474 fn test_analyze_rir_very_short() {
475 let rir = vec![0.5f32; 10];
477 let config = SsirConfig::new(48000.0);
478 let result = analyze_rir(&rir, &config);
479 assert!(result.num_events() <= 1);
481 }
482
483 #[test]
484 fn test_analyze_rir_all_zeros() {
485 let rir = vec![0.0f32; 4800];
486 let config = SsirConfig {
487 sample_rate: 48000.0,
488 mixing_time_ms: Some(40.0),
489 ..SsirConfig::default()
490 };
491 let result = analyze_rir(&rir, &config);
492 assert_eq!(result.num_events(), 0);
494 }
495
496 #[test]
497 fn test_analyze_rir_dc_offset() {
498 let mut rir = vec![0.1f32; 4800];
500 rir[48] = 1.0;
501 rir[288] = 0.6;
502
503 let config = SsirConfig {
504 sample_rate: 48000.0,
505 mixing_time_ms: Some(40.0),
506 ..SsirConfig::default()
507 };
508 let result = analyze_rir(&rir, &config);
509 assert!(result.num_events() >= 1);
510 }
511
512 #[test]
513 fn test_segment_duration_ms_accuracy() {
514 let seg = RirSegment {
515 onset_sample: 0,
516 end_sample: 480,
517 toa_sample: 48,
518 doa: None,
519 peak_energy: 1.0,
520 is_direct_sound: true,
521 };
522 let dur = seg.duration_ms(48000.0);
523 assert!((dur - 10.0).abs() < 0.01, "expected 10ms, got {dur}ms");
524 }
525
526 #[test]
527 fn test_direct_sound_toa_at_rir_boundary() {
528 let mut rir = vec![0.0001f32; 2400];
530 rir[0] = 1.0;
531 rir[288] = 0.3;
532
533 let config = SsirConfig {
534 sample_rate: 48000.0,
535 mixing_time_ms: Some(40.0),
536 ..SsirConfig::default()
537 };
538 let result = analyze_rir(&rir, &config);
539 assert!(result.num_events() >= 1);
540 assert!(result.segments[0].is_direct_sound);
541 }
542}