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}