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