Skip to main content

math_rir/
config.rs

1/// Configuration for SSIR (Spatial Segmentation of Impulse Response) analysis.
2///
3/// Default values correspond to the SSIR-Mk2 configuration from
4/// Pawlak & Lee (Applied Acoustics 249, 2026), Table 1.
5#[derive(Debug, Clone)]
6pub struct SsirConfig {
7    /// Sample rate in Hz
8    pub sample_rate: f64,
9
10    /// Direct sound window: (pre, post) in ms relative to detected onset.
11    /// Reflections within this window are excluded from detection.
12    /// Default: (0.5, 3.5) — the direct sound typically occupies ~4ms.
13    pub direct_sound_window_ms: (f64, f64),
14
15    /// Local Energy Ratio analysis window length in ms.
16    /// The RIR is divided into consecutive windows of this length.
17    /// Default: 1.0 ms (48 samples @ 48kHz).
18    pub ler_window_ms: f64,
19
20    /// Energy threshold as a multiple of the per-window median energy.
21    /// A sample is considered a reflection candidate if its energy exceeds
22    /// this multiple of the window's median energy.
23    /// Default: 3.0
24    pub energy_threshold: f64,
25
26    /// Minimum angular distance (degrees) between consecutive reflections
27    /// for them to be considered distinct events.
28    /// Pairs below this threshold are merged.
29    /// Default: 9.0 degrees. Only used with multi-channel (SRIR) input.
30    pub doa_threshold_deg: f64,
31
32    /// Minimum time-of-arrival difference (ms) between consecutive reflections.
33    /// Pairs closer than this are merged regardless of DOA.
34    /// Default: 0.5 ms.
35    pub toa_threshold_ms: f64,
36
37    /// Minimum segment duration (ms) for early reflections.
38    /// Segments shorter than this are merged with the preceding segment.
39    /// Default: 0.5 ms.
40    pub min_segment_ms: f64,
41
42    /// Mixing time in ms (boundary between early reflections and reverberant tail).
43    /// If None, estimated automatically from the Schroeder decay curve.
44    /// Default: None (auto-estimate, typical values: 30-50ms for small rooms).
45    pub mixing_time_ms: Option<f64>,
46
47    /// Pre-onset window length (ms) for refining segment boundaries.
48    /// For each detected reflection, the onset is searched within
49    /// [TOA - onset_window_ms, TOA].
50    /// Default: 0.5 ms.
51    pub onset_window_ms: f64,
52
53    /// Duration (ms) of the optional final segment after the last detected event.
54    /// Default: 2.0 ms.
55    pub final_segment_ms: f64,
56
57    /// Minimum peak distance (ms) for direct sound onset detection.
58    /// Peaks closer than this are suppressed when searching for the direct sound.
59    /// Default: 0.1 ms (5 samples @ 48kHz).
60    pub min_peak_distance_ms: f64,
61
62    /// Band-limiting frequency range (Hz) for DOA estimation from B-format channels.
63    ///
64    /// The pseudo-intensity vector method is most reliable within a frequency band
65    /// where spatial aliasing is low and wavelengths are short enough for directional
66    /// resolution. Low frequencies have poor spatial resolution; high frequencies
67    /// may alias depending on the microphone array.
68    ///
69    /// Default: (500.0, 4000.0) — a commonly used range for first-order Ambisonics.
70    pub doa_bandpass_hz: (f64, f64),
71
72    /// Butterworth filter order for DOA band-limiting.
73    ///
74    /// Applied as a zero-phase (filtfilt) bandpass, so the effective order is doubled.
75    /// Default: 4 (effective 8th-order after forward-reverse filtering).
76    pub doa_bandpass_order: u32,
77}
78
79impl SsirConfig {
80    /// Create a config with the given sample rate and all other values at defaults.
81    pub fn new(sample_rate: f64) -> Self {
82        Self {
83            sample_rate,
84            ..Self::default_at(sample_rate)
85        }
86    }
87
88    /// Create default config at a specific sample rate.
89    fn default_at(sample_rate: f64) -> Self {
90        Self {
91            sample_rate,
92            direct_sound_window_ms: (0.5, 3.5),
93            ler_window_ms: 1.0,
94            energy_threshold: 3.0,
95            doa_threshold_deg: 9.0,
96            toa_threshold_ms: 0.5,
97            min_segment_ms: 0.5,
98            mixing_time_ms: None,
99            onset_window_ms: 0.5,
100            final_segment_ms: 2.0,
101            min_peak_distance_ms: 0.1,
102            doa_bandpass_hz: (500.0, 4000.0),
103            doa_bandpass_order: 4,
104        }
105    }
106
107    // -- helper conversions --
108
109    /// Convert milliseconds to samples at the configured sample rate.
110    pub(crate) fn ms_to_samples(&self, ms: f64) -> usize {
111        (ms * self.sample_rate / 1000.0).round() as usize
112    }
113
114    /// LER window length in samples.
115    pub(crate) fn ler_window_samples(&self) -> usize {
116        self.ms_to_samples(self.ler_window_ms)
117    }
118
119    /// Direct sound window as (pre_samples, post_samples) relative to onset.
120    pub(crate) fn direct_sound_window_samples(&self) -> (usize, usize) {
121        (
122            self.ms_to_samples(self.direct_sound_window_ms.0),
123            self.ms_to_samples(self.direct_sound_window_ms.1),
124        )
125    }
126
127    /// TOA threshold in samples.
128    pub(crate) fn toa_threshold_samples(&self) -> usize {
129        self.ms_to_samples(self.toa_threshold_ms)
130    }
131
132    /// Minimum segment duration in samples.
133    pub(crate) fn min_segment_samples(&self) -> usize {
134        self.ms_to_samples(self.min_segment_ms)
135    }
136
137    /// Onset window in samples.
138    pub(crate) fn onset_window_samples(&self) -> usize {
139        self.ms_to_samples(self.onset_window_ms)
140    }
141
142    /// Mixing time in samples (using configured or default fallback of 38ms).
143    pub(crate) fn mixing_time_samples(&self) -> usize {
144        self.ms_to_samples(self.mixing_time_ms.unwrap_or(38.0))
145    }
146
147    /// Final segment duration in samples.
148    pub(crate) fn final_segment_samples(&self) -> usize {
149        self.ms_to_samples(self.final_segment_ms)
150    }
151
152    /// Minimum peak distance in samples for onset detection.
153    pub(crate) fn min_peak_distance_samples(&self) -> usize {
154        self.ms_to_samples(self.min_peak_distance_ms).max(1)
155    }
156}
157
158impl Default for SsirConfig {
159    fn default() -> Self {
160        Self::default_at(48000.0)
161    }
162}