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}