Skip to main content

math_rir/
types.rs

1/// A single segment of the RIR identified by SSIR analysis.
2///
3/// Each segment represents a discrete acoustic event (direct sound or early reflection)
4/// with a constant direction of arrival (DOA). Segments are consecutive — the end of
5/// one segment is the onset of the next, preserving the full temporal energy profile.
6#[derive(Debug, Clone)]
7pub struct RirSegment {
8    /// Start sample of this segment (onset)
9    pub onset_sample: usize,
10    /// End sample (exclusive) — equals the next segment's onset, or mixing time for the last segment
11    pub end_sample: usize,
12    /// Sample index of the peak arrival (TOA) within this segment
13    pub toa_sample: usize,
14    /// Direction of arrival as a unit vector [x, y, z], if available from multi-channel input
15    pub doa: Option<[f32; 3]>,
16    /// Peak energy (squared amplitude) at the TOA sample
17    pub peak_energy: f64,
18    /// Whether this segment contains the direct sound
19    pub is_direct_sound: bool,
20}
21
22impl RirSegment {
23    /// Duration of this segment in samples
24    pub fn len(&self) -> usize {
25        self.end_sample.saturating_sub(self.onset_sample)
26    }
27
28    /// Whether this segment has zero length
29    pub fn is_empty(&self) -> bool {
30        self.len() == 0
31    }
32
33    /// Duration of this segment in seconds
34    pub fn duration_secs(&self, sample_rate: f64) -> f64 {
35        self.len() as f64 / sample_rate
36    }
37
38    /// Duration of this segment in milliseconds
39    pub fn duration_ms(&self, sample_rate: f64) -> f64 {
40        self.duration_secs(sample_rate) * 1000.0
41    }
42
43    /// Time of arrival relative to the RIR start, in milliseconds
44    pub fn toa_ms(&self, sample_rate: f64) -> f64 {
45        self.toa_sample as f64 / sample_rate * 1000.0
46    }
47
48    /// DOA azimuth in degrees (0 = front, positive = left), if DOA is available.
49    /// Computed from the x,y components of the DOA unit vector.
50    pub fn azimuth_deg(&self) -> Option<f32> {
51        self.doa.map(|d| d[1].atan2(d[0]).to_degrees())
52    }
53
54    /// DOA elevation in degrees, if DOA is available.
55    /// Computed from the z component of the DOA unit vector.
56    pub fn elevation_deg(&self) -> Option<f32> {
57        self.doa
58            .map(|d| d[2].atan2((d[0] * d[0] + d[1] * d[1]).sqrt()).to_degrees())
59    }
60}
61
62/// Result of SSIR analysis on a room impulse response.
63#[derive(Debug, Clone)]
64pub struct SsirResult {
65    /// Ordered sequence of segments covering the early RIR.
66    /// First segment is always the direct sound.
67    /// Segments are consecutive: segment[i].end_sample == segment[i+1].onset_sample
68    pub segments: Vec<RirSegment>,
69    /// Estimated mixing time in samples (boundary between early reflections and reverberant tail)
70    pub mixing_time_samples: usize,
71    /// Sample rate used for analysis
72    pub sample_rate: f64,
73}
74
75impl SsirResult {
76    /// Number of detected sound events (direct sound + early reflections)
77    pub fn num_events(&self) -> usize {
78        self.segments.len()
79    }
80
81    /// Number of early reflections (excludes direct sound)
82    pub fn num_reflections(&self) -> usize {
83        self.segments.len().saturating_sub(1)
84    }
85
86    /// Mixing time in milliseconds
87    pub fn mixing_time_ms(&self) -> f64 {
88        self.mixing_time_samples as f64 / self.sample_rate * 1000.0
89    }
90
91    /// Iterator over only the early reflection segments (excludes direct sound)
92    pub fn reflections(&self) -> impl Iterator<Item = &RirSegment> {
93        self.segments.iter().filter(|s| !s.is_direct_sound)
94    }
95
96    /// The direct sound segment, if detected
97    pub fn direct_sound(&self) -> Option<&RirSegment> {
98        self.segments.first().filter(|s| s.is_direct_sound)
99    }
100}