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