Skip to main content

math_rir/
lib.rs

1//! # math-rir: Room Impulse Response Analysis
2//!
3//! Two complementary analysis paths on a Room Impulse Response (RIR):
4//!
5//! 1. **SSIR segmentation** ([`analyze_rir`], [`analyze_srir`]) — Spatial
6//!    Segmentation of the early RIR into consecutive sound events
7//!    (direct sound + reflections), based on Pawlak & Lee, *Spatial
8//!    segmentation of impulse response for room reflection analysis and
9//!    auralization*, Applied Acoustics 249 (2026).
10//! 2. **ISO 3382 room-acoustic metrics** ([`analyze_iso3382`],
11//!    [`analyze_iso3382_octaves`], [`analyze_iso3382_third_octaves`]) —
12//!    EDT, T20, T30, C50, C80, D50, Centre time (Ts) computed from a
13//!    Schroeder backward integration, with optional per-octave or
14//!    per-third-octave filtering using zero-phase Butterworth bandpasses.
15//!
16//! ## Overview
17//!
18//! The SSIR method segments a Room Impulse Response (RIR) into consecutive,
19//! variable-length sound events (direct sound + early reflections), each with
20//! a constant direction of arrival (DOA). This preserves the full temporal
21//! energy profile while enabling per-reflection manipulation.
22//!
23//! The ISO 3382 path treats the whole RIR as one signal and reports the
24//! classical reverberation/clarity parameters that listening rooms and
25//! performance spaces are measured against.
26//!
27//! ## Usage
28//!
29//! ```rust
30//! use math_rir::{analyze_rir, analyze_iso3382, analyze_iso3382_octaves, SsirConfig};
31//!
32//! let rir: Vec<f32> = load_impulse_response(); // your RIR data
33//! let sr = 48000.0;
34//!
35//! // 1) SSIR segmentation — per-reflection geometry.
36//! let result = analyze_rir(&rir, &SsirConfig::new(sr));
37//! println!("Detected {} events ({} reflections)",
38//!     result.num_events(), result.num_reflections());
39//!
40//! // 2) ISO 3382 broadband metrics.
41//! let m = analyze_iso3382(&rir, sr);
42//! println!("T30 = {:.2}s, EDT = {:.2}s, C80 = {:.1} dB, Ts = {:.0} ms",
43//!     m.t30_s, m.edt_s, m.c80_db, m.ts_s * 1000.0);
44//!
45//! // 3) Per-octave-band ISO 3382 metrics (125 Hz … 8 kHz).
46//! for (fc, m) in analyze_iso3382_octaves(&rir, sr) {
47//!     println!("  {:>5.0} Hz: T30={:.2}s C50={:.1}dB", fc, m.t30_s, m.c50_db);
48//! }
49//! # fn load_impulse_response() -> Vec<f32> { vec![0.0; 4800] }
50//! ```
51
52pub 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
76/// Analyze a mono room impulse response using the SSIR method.
77///
78/// Detects the direct sound, identifies early reflections via Local Energy Ratio,
79/// and segments the early RIR into consecutive sound events.
80///
81/// For mono input, DOA validation is not available — only energy-based and
82/// temporal distance criteria are used for reflection detection.
83///
84/// Returns an [`SsirResult`] with the detected segments and mixing time.
85pub 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    // Step 1: Estimate mixing time (or use configured value)
95    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    // Step 2: Find direct sound TOA
102    let direct_sound_toa = match find_direct_sound_toa(rir, config) {
103        Some(toa) => toa,
104        None => {
105            // No direct sound detected — return empty result
106            return SsirResult {
107                segments: Vec::new(),
108                mixing_time_samples,
109                sample_rate: config.sample_rate,
110            };
111        }
112    };
113
114    // Step 3: Detect early reflections (no DOA data for mono)
115    let reflections = detect_reflections(rir, direct_sound_toa, None, config);
116
117    // Step 4: Build segments with onset refinement
118    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
134/// Analyze a multi-channel Spatial Room Impulse Response (SRIR) using the full SSIR method.
135///
136/// Uses the first channel as the omnidirectional pressure signal for energy-based
137/// detection, and derives DOA from all channels using the intensity vector method.
138///
139/// `channels` should contain at least 4 channels (B-format: W, X, Y, Z) for
140/// meaningful DOA estimation. The first channel (W) is used as the omnidirectional
141/// signal for reflection detection.
142///
143/// Falls back to mono analysis if fewer than 4 channels are provided.
144pub 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    // Use first channel as omnidirectional pressure
154    let omni = channels[0];
155
156    // Need at least W, X, Y, Z (4 channels) for DOA estimation
157    if channels.len() < 4 {
158        return analyze_rir(omni, config);
159    }
160
161    // Verify all channels have the same length
162    let len = omni.len();
163    if channels.iter().any(|ch| ch.len() != len) {
164        return analyze_rir(omni, config);
165    }
166
167    // Step 1: Estimate mixing time
168    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    // Step 2: Find direct sound TOA
175    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    // Step 3: Compute DOA vectors from band-limited B-format channels
187    // B-format: W (omni), X (front-back), Y (left-right), Z (up-down)
188    let doa_vectors = compute_bformat_doa(channels, len, config);
189
190    // Step 4: Detect reflections with DOA validation
191    let reflections = detect_reflections(omni, direct_sound_toa, Some(&doa_vectors), config);
192
193    // Step 5: Build segments (pass direct sound DOA from the DOA vector at its TOA)
194    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
211/// Compute per-sample DOA unit vectors from B-format (Ambisonics) channels.
212///
213/// The channels are band-limited with a zero-phase Butterworth bandpass filter
214/// before computing the pseudo-intensity vector. This improves DOA reliability
215/// by excluding low frequencies (poor spatial resolution) and high frequencies
216/// (spatial aliasing).
217///
218/// **DOA sign convention.** Uses the pseudo-intensity vector
219/// `I = P · V`, with `P = W` (omnidirectional pressure) and
220/// `V = [X, Y, Z]` (figure-of-eight channels). For first-order Ambisonics
221/// B-format the V channels are pickup patterns oriented along the
222/// coordinate axes — *not* raw particle-velocity components — so a source
223/// at `+X` produces W and X signals in phase and `I_x = W · X` is positive
224/// for a source at `+X`. The DOA (source direction) is therefore
225/// `+I / |I|`, consistent with the SSIR paper and standard first-order
226/// Ambisonics DOA literature (Pulkki 2007, Merimaa 2002).
227///
228/// The tests `test_compute_bformat_doa_plane_wave_*` verify the sign
229/// against known plane-wave fixtures.
230///
231/// **Allocations.** This used to allocate up to 8 large heap vectors per
232/// call (4 × `Vec<f64>` for the f64 input copy + 4 × `Vec<f32>` for the
233/// filtered output). The no-filter branch additionally cloned all 4 input
234/// channels. We now:
235///   - skip the input clone in the no-filter branch (borrow the caller's
236///     slices directly),
237///   - keep the filtered branch limited to 2 vectors per channel (one f64
238///     scratch input + one f64 filtfilt output that is then materialised
239///     into the owned f32 vector — we cannot eliminate that pair without
240///     changing the `filtfilt` API to operate in-place).
241fn 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    // Band-limit all 4 B-format channels with zero-phase filtering.
247    // Skip filtering if the band covers the full spectrum or the signal is too short.
248    let needs_filtering = low_hz > 0.0 && high_hz < nyquist && len >= 4 && order >= 1;
249
250    // Filtered branch owns four f32 vectors; un-filtered branch borrows
251    // the input slices and allocates nothing extra.
252    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            // Down from 8 vectors per call to 2 (input scratch + output).
268            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, &sections);
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            // Pseudo-intensity vector components I = P · V. For B-format
305            // first-order Ambisonics this points TOWARD the source (the V
306            // channels are figure-of-eight pickup patterns, not raw
307            // particle-velocity components).
308            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                // DOA = +I / |I| (source direction in B-format convention).
317                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    /// Helper: create a synthetic RIR with known reflections
329    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]; // low noise floor
337
338        // Direct sound at 1ms
339        let ds_sample = (1.0 * sample_rate / 1000.0) as usize;
340        rir[ds_sample] = 1.0;
341
342        // Add reflections
343        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        // Should detect direct sound + reflections
366        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        // Segments should be consecutive
374        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        // All reflection TOAs should be within the early RIR
385        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        // Anechoic: only direct sound, no reflections
404        let mut rir = vec![0.0001f32; 4800]; // 100ms
405        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        // Should have at least the direct sound
416        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        // Only 2 channels — should fall back to mono
431        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        // Plane wave from +X (front): W and X in phase, Y = Z = 0.
438        // DOA should point along +X.
439        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        // Bandpass disabled so we test the raw intensity computation.
450        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        // Plane wave from +Y (left): W and Y in phase.
466        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]; // omni
493        let mut x = vec![0.0f32; len]; // front-back
494        let mut y = vec![0.0f32; len]; // left-right
495        let z = vec![0.0f32; len]; // up-down
496
497        // Direct sound from front (positive X)
498        w[48] = 1.0;
499        x[48] = 1.0;
500        y[48] = 0.0;
501
502        // Reflection from left at 6ms (positive Y)
503        w[288] = 0.5;
504        x[288] = 0.0;
505        y[288] = 0.5;
506
507        // Reflection from right at 10ms (negative Y)
508        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        // Check that DOA is present on segments
527        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        // First segment should start at 0
554        assert_eq!(result.segments[0].onset_sample, 0);
555
556        // Segments should be non-empty
557        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        // Create a RIR with sparse reflections then dense reverb
565        let sample_rate = 48000.0;
566        let len = (0.200 * sample_rate) as usize;
567        let mut rir = vec![0.0f32; len];
568
569        // Direct sound
570        rir[48] = 1.0;
571        // Sparse reflections
572        rir[240] = 0.5;
573        rir[480] = 0.3;
574
575        // Dense reverb starting at ~30ms
576        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, // auto-estimate
589            ..SsirConfig::default()
590        };
591
592        let result = analyze_rir(&rir, &config);
593
594        // Mixing time should be in reasonable range
595        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        // RIR shorter than one LER window (48 samples at 48kHz = 1ms)
605        let rir = vec![0.5f32; 10];
606        let config = SsirConfig::new(48000.0);
607        let result = analyze_rir(&rir, &config);
608        // Should not panic, may find 0 or 1 events
609        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        // All-zero RIR: no detectable direct sound
622        assert_eq!(result.num_events(), 0);
623    }
624
625    #[test]
626    fn test_analyze_rir_dc_offset() {
627        // RIR with DC offset — should still detect the impulse
628        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        // Direct sound at the very start
658        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}