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}