1pub mod bands;
53mod config;
54mod detection;
55pub mod metrics;
56mod mixing_time;
57mod segmentation;
58mod types;
59
60pub use bands::{
61 BandWidth, ISO_OCTAVE_CENTERS_HZ, ISO_THIRD_OCTAVE_CENTERS_HZ, analyze_iso3382_bands,
62 analyze_iso3382_octaves, analyze_iso3382_third_octaves, bandpass,
63};
64pub use config::SsirConfig;
65pub use math_audio_iir_fir::filtfilt;
66pub use metrics::{
67 DecayCurve, Iso3382Metrics, analyze_iso3382, estimate_noise_cutoff, schroeder_curve,
68};
69pub use types::{RirSegment, SsirResult};
70
71use detection::{detect_reflections, find_direct_sound_toa};
72use mixing_time::estimate_mixing_time;
73use rayon::prelude::*;
74use segmentation::build_segments;
75
76pub fn analyze_rir(rir: &[f32], config: &SsirConfig) -> SsirResult {
86 if rir.is_empty() {
87 return SsirResult {
88 segments: Vec::new(),
89 mixing_time_samples: 0,
90 sample_rate: config.sample_rate,
91 };
92 }
93
94 let mixing_time_samples = if config.mixing_time_ms.is_some() {
96 config.mixing_time_samples()
97 } else {
98 estimate_mixing_time(rir, config.sample_rate)
99 };
100
101 let direct_sound_toa = match find_direct_sound_toa(rir, config) {
103 Some(toa) => toa,
104 None => {
105 return SsirResult {
107 segments: Vec::new(),
108 mixing_time_samples,
109 sample_rate: config.sample_rate,
110 };
111 }
112 };
113
114 let reflections = detect_reflections(rir, direct_sound_toa, None, config);
116
117 let segments = build_segments(
119 rir,
120 direct_sound_toa,
121 None,
122 &reflections,
123 mixing_time_samples,
124 config,
125 );
126
127 SsirResult {
128 segments,
129 mixing_time_samples,
130 sample_rate: config.sample_rate,
131 }
132}
133
134pub fn analyze_srir(channels: &[&[f32]], config: &SsirConfig) -> SsirResult {
145 if channels.is_empty() || channels[0].is_empty() {
146 return SsirResult {
147 segments: Vec::new(),
148 mixing_time_samples: 0,
149 sample_rate: config.sample_rate,
150 };
151 }
152
153 let omni = channels[0];
155
156 if channels.len() < 4 {
158 return analyze_rir(omni, config);
159 }
160
161 let len = omni.len();
163 if channels.iter().any(|ch| ch.len() != len) {
164 return analyze_rir(omni, config);
165 }
166
167 let mixing_time_samples = if config.mixing_time_ms.is_some() {
169 config.mixing_time_samples()
170 } else {
171 estimate_mixing_time(omni, config.sample_rate)
172 };
173
174 let direct_sound_toa = match find_direct_sound_toa(omni, config) {
176 Some(toa) => toa,
177 None => {
178 return SsirResult {
179 segments: Vec::new(),
180 mixing_time_samples,
181 sample_rate: config.sample_rate,
182 };
183 }
184 };
185
186 let doa_vectors = compute_bformat_doa(channels, len, config);
189
190 let reflections = detect_reflections(omni, direct_sound_toa, Some(&doa_vectors), config);
192
193 let ds_doa = doa_vectors.get(direct_sound_toa).copied();
195 let segments = build_segments(
196 omni,
197 direct_sound_toa,
198 ds_doa,
199 &reflections,
200 mixing_time_samples,
201 config,
202 );
203
204 SsirResult {
205 segments,
206 mixing_time_samples,
207 sample_rate: config.sample_rate,
208 }
209}
210
211fn compute_bformat_doa(channels: &[&[f32]], len: usize, config: &SsirConfig) -> Vec<[f32; 3]> {
242 let (low_hz, high_hz) = config.doa_bandpass_hz;
243 let order = config.doa_bandpass_order;
244 let nyquist = config.sample_rate / 2.0;
245
246 let needs_filtering = low_hz > 0.0 && high_hz < nyquist && len >= 4 && order >= 1;
249
250 let owned: Option<[Vec<f32>; 4]> = if needs_filtering {
253 let mut sections =
254 filtfilt::peq_to_coefficients(&math_audio_iir_fir::peq_butterworth_highpass(
255 order as usize,
256 low_hz,
257 config.sample_rate,
258 ));
259 sections.extend(filtfilt::peq_to_coefficients(
260 &math_audio_iir_fir::peq_butterworth_lowpass(
261 order as usize,
262 high_hz,
263 config.sample_rate,
264 ),
265 ));
266 let filter_channel = |ch: &[f32]| -> Vec<f32> {
267 let mut scratch: Vec<f64> = Vec::with_capacity(ch.len());
269 scratch.extend(ch.iter().map(|&s| s as f64));
270 let out_f64 = filtfilt::filtfilt(&scratch, §ions);
271 let mut out_f32: Vec<f32> = Vec::with_capacity(out_f64.len());
272 out_f32.extend(out_f64.into_iter().map(|s| s as f32));
273 out_f32
274 };
275 let ((w, x), (y, z)) = rayon::join(
276 || {
277 rayon::join(
278 || filter_channel(channels[0]),
279 || filter_channel(channels[1]),
280 )
281 },
282 || {
283 rayon::join(
284 || filter_channel(channels[2]),
285 || filter_channel(channels[3]),
286 )
287 },
288 );
289 Some([w, x, y, z])
290 } else {
291 None
292 };
293
294 let (w, x, y, z): (&[f32], &[f32], &[f32], &[f32]) = if let Some(o) = owned.as_ref() {
295 (&o[0], &o[1], &o[2], &o[3])
296 } else {
297 (channels[0], channels[1], channels[2], channels[3])
298 };
299
300 (0..len)
301 .into_par_iter()
302 .map(|i| {
303 let p = w[i] as f64;
304 let ix = p * x[i] as f64;
309 let iy = p * y[i] as f64;
310 let iz = p * z[i] as f64;
311
312 let mag = (ix * ix + iy * iy + iz * iz).sqrt();
313 if mag < 1e-12 {
314 [0.0f32, 0.0, 0.0]
315 } else {
316 let inv = 1.0 / mag;
318 [(ix * inv) as f32, (iy * inv) as f32, (iz * inv) as f32]
319 }
320 })
321 .collect()
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 fn make_synthetic_rir(
330 sample_rate: f64,
331 reflection_times_ms: &[f64],
332 reflection_gains: &[f32],
333 ) -> Vec<f32> {
334 let duration_ms = 100.0;
335 let len = (duration_ms * sample_rate / 1000.0) as usize;
336 let mut rir = vec![0.0001f32; len]; let ds_sample = (1.0 * sample_rate / 1000.0) as usize;
340 rir[ds_sample] = 1.0;
341
342 for (&time_ms, &gain) in reflection_times_ms.iter().zip(reflection_gains.iter()) {
344 let sample = (time_ms * sample_rate / 1000.0) as usize;
345 if sample < len {
346 rir[sample] = gain;
347 }
348 }
349
350 rir
351 }
352
353 #[test]
354 fn test_analyze_rir_basic() {
355 let rir = make_synthetic_rir(48000.0, &[6.0, 10.0, 15.0, 22.0], &[0.5, 0.3, 0.25, 0.15]);
356
357 let config = SsirConfig {
358 sample_rate: 48000.0,
359 mixing_time_ms: Some(40.0),
360 ..SsirConfig::default()
361 };
362
363 let result = analyze_rir(&rir, &config);
364
365 assert!(
367 result.num_events() >= 3,
368 "expected >= 3 events, got {}",
369 result.num_events()
370 );
371 assert!(result.segments[0].is_direct_sound);
372
373 for i in 0..result.segments.len() - 1 {
375 assert_eq!(
376 result.segments[i].end_sample,
377 result.segments[i + 1].onset_sample,
378 "segments {} and {} are not consecutive",
379 i,
380 i + 1
381 );
382 }
383
384 for seg in result.reflections() {
386 let toa_ms = seg.toa_ms(48000.0);
387 assert!(
388 toa_ms > 1.0 && toa_ms < 40.0,
389 "reflection TOA {toa_ms:.1}ms outside expected range"
390 );
391 }
392 }
393
394 #[test]
395 fn test_analyze_rir_empty() {
396 let config = SsirConfig::new(48000.0);
397 let result = analyze_rir(&[], &config);
398 assert_eq!(result.num_events(), 0);
399 }
400
401 #[test]
402 fn test_analyze_rir_single_impulse() {
403 let mut rir = vec![0.0001f32; 4800]; rir[48] = 1.0;
406
407 let config = SsirConfig {
408 sample_rate: 48000.0,
409 mixing_time_ms: Some(40.0),
410 ..SsirConfig::default()
411 };
412
413 let result = analyze_rir(&rir, &config);
414
415 assert!(result.num_events() >= 1);
417 assert!(result.segments[0].is_direct_sound);
418 }
419
420 #[test]
421 fn test_analyze_srir_fallback_to_mono() {
422 let rir = make_synthetic_rir(48000.0, &[6.0, 10.0], &[0.5, 0.3]);
423
424 let config = SsirConfig {
425 sample_rate: 48000.0,
426 mixing_time_ms: Some(40.0),
427 ..SsirConfig::default()
428 };
429
430 let result = analyze_srir(&[&rir, &rir], &config);
432 assert!(result.num_events() >= 2);
433 }
434
435 #[test]
436 fn test_compute_bformat_doa_plane_wave_front() {
437 let len = 1024;
440 let mut w = vec![0.0f32; len];
441 let mut x = vec![0.0f32; len];
442 let y = vec![0.0f32; len];
443 let z = vec![0.0f32; len];
444 for i in 100..120 {
445 let s = (-(i as f32 - 110.0).powi(2) / 4.0).exp();
446 w[i] = s;
447 x[i] = s;
448 }
449 let config = SsirConfig {
451 sample_rate: 48000.0,
452 doa_bandpass_hz: (0.0, 96000.0),
453 doa_bandpass_order: 0,
454 ..SsirConfig::default()
455 };
456 let doa = compute_bformat_doa(&[&w, &x, &y, &z], len, &config);
457 let d = doa[110];
458 assert!(d[0] > 0.99, "expected DOA[x] ≈ +1, got {:?}", d);
459 assert!(d[1].abs() < 0.05, "expected DOA[y] ≈ 0, got {:?}", d);
460 assert!(d[2].abs() < 0.05, "expected DOA[z] ≈ 0, got {:?}", d);
461 }
462
463 #[test]
464 fn test_compute_bformat_doa_plane_wave_left() {
465 let len = 1024;
467 let mut w = vec![0.0f32; len];
468 let x = vec![0.0f32; len];
469 let mut y = vec![0.0f32; len];
470 let z = vec![0.0f32; len];
471 for i in 100..120 {
472 let s = (-(i as f32 - 110.0).powi(2) / 4.0).exp();
473 w[i] = s;
474 y[i] = s;
475 }
476 let config = SsirConfig {
477 sample_rate: 48000.0,
478 doa_bandpass_hz: (0.0, 96000.0),
479 doa_bandpass_order: 0,
480 ..SsirConfig::default()
481 };
482 let doa = compute_bformat_doa(&[&w, &x, &y, &z], len, &config);
483 let d = doa[110];
484 assert!(d[0].abs() < 0.05, "expected DOA[x] ≈ 0, got {:?}", d);
485 assert!(d[1] > 0.99, "expected DOA[y] ≈ +1, got {:?}", d);
486 assert!(d[2].abs() < 0.05, "expected DOA[z] ≈ 0, got {:?}", d);
487 }
488
489 #[test]
490 fn test_analyze_srir_bformat() {
491 let len = 4800;
492 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;
499 x[48] = 1.0;
500 y[48] = 0.0;
501
502 w[288] = 0.5;
504 x[288] = 0.0;
505 y[288] = 0.5;
506
507 w[480] = 0.3;
509 x[480] = 0.0;
510 y[480] = -0.3;
511
512 let config = SsirConfig {
513 sample_rate: 48000.0,
514 mixing_time_ms: Some(40.0),
515 ..SsirConfig::default()
516 };
517
518 let result = analyze_srir(&[&w, &x, &y, &z], &config);
519
520 assert!(
521 result.num_events() >= 2,
522 "expected >= 2 events, got {}",
523 result.num_events()
524 );
525
526 for seg in &result.segments {
528 assert!(seg.doa.is_some(), "SRIR segments should have DOA data");
529 }
530
531 let ds_doa = result
532 .direct_sound_doa()
533 .expect("direct sound should carry DOA");
534 assert!(
535 ds_doa[0] > 0.5,
536 "front direct sound should point toward +X, got {:?}",
537 ds_doa
538 );
539 }
540
541 #[test]
542 fn test_segments_cover_early_rir() {
543 let rir = make_synthetic_rir(48000.0, &[6.0, 12.0, 20.0], &[0.5, 0.3, 0.2]);
544
545 let config = SsirConfig {
546 sample_rate: 48000.0,
547 mixing_time_ms: Some(40.0),
548 ..SsirConfig::default()
549 };
550
551 let result = analyze_rir(&rir, &config);
552
553 assert_eq!(result.segments[0].onset_sample, 0);
555
556 for seg in &result.segments {
558 assert!(!seg.is_empty(), "segment should have non-zero length");
559 }
560 }
561
562 #[test]
563 fn test_mixing_time_auto_estimation() {
564 let sample_rate = 48000.0;
566 let len = (0.200 * sample_rate) as usize;
567 let mut rir = vec![0.0f32; len];
568
569 rir[48] = 1.0;
571 rir[240] = 0.5;
573 rir[480] = 0.3;
574
575 let reverb_start = (0.030 * sample_rate) as usize;
577 let mut amp = 0.08f32;
578 let mut rng: u32 = 12345;
579 for sample in rir.iter_mut().take(len).skip(reverb_start) {
580 rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
581 let noise = ((rng >> 16) as f32 / 32768.0) - 1.0;
582 *sample += noise * amp;
583 amp *= 0.9997;
584 }
585
586 let config = SsirConfig {
587 sample_rate,
588 mixing_time_ms: None, ..SsirConfig::default()
590 };
591
592 let result = analyze_rir(&rir, &config);
593
594 let mt_ms = result.mixing_time_ms();
596 assert!(
597 (10.0..=80.0).contains(&mt_ms),
598 "auto mixing time {mt_ms:.1}ms outside expected range"
599 );
600 }
601
602 #[test]
603 fn test_analyze_rir_very_short() {
604 let rir = vec![0.5f32; 10];
606 let config = SsirConfig::new(48000.0);
607 let result = analyze_rir(&rir, &config);
608 assert!(result.num_events() <= 1);
610 }
611
612 #[test]
613 fn test_analyze_rir_all_zeros() {
614 let rir = vec![0.0f32; 4800];
615 let config = SsirConfig {
616 sample_rate: 48000.0,
617 mixing_time_ms: Some(40.0),
618 ..SsirConfig::default()
619 };
620 let result = analyze_rir(&rir, &config);
621 assert_eq!(result.num_events(), 0);
623 }
624
625 #[test]
626 fn test_analyze_rir_dc_offset() {
627 let mut rir = vec![0.1f32; 4800];
629 rir[48] = 1.0;
630 rir[288] = 0.6;
631
632 let config = SsirConfig {
633 sample_rate: 48000.0,
634 mixing_time_ms: Some(40.0),
635 ..SsirConfig::default()
636 };
637 let result = analyze_rir(&rir, &config);
638 assert!(result.num_events() >= 1);
639 }
640
641 #[test]
642 fn test_segment_duration_ms_accuracy() {
643 let seg = RirSegment {
644 onset_sample: 0,
645 end_sample: 480,
646 toa_sample: 48,
647 doa: None,
648 peak_energy: 1.0,
649 is_direct_sound: true,
650 };
651 let dur = seg.duration_ms(48000.0);
652 assert!((dur - 10.0).abs() < 0.01, "expected 10ms, got {dur}ms");
653 }
654
655 #[test]
656 fn test_direct_sound_toa_at_rir_boundary() {
657 let mut rir = vec![0.0001f32; 2400];
659 rir[0] = 1.0;
660 rir[288] = 0.3;
661
662 let config = SsirConfig {
663 sample_rate: 48000.0,
664 mixing_time_ms: Some(40.0),
665 ..SsirConfig::default()
666 };
667 let result = analyze_rir(&rir, &config);
668 assert!(result.num_events() >= 1);
669 assert!(result.segments[0].is_direct_sound);
670 assert_eq!(result.segments[0].toa_sample, 0);
671 }
672}