1use std::error::Error;
10use std::fmt;
11
12use crate::audio::SampleRate;
13use crate::cast;
14use crate::units::{Decibels, Seconds};
15
16const BLOCK_SECONDS: f64 = 0.01;
18const FLOOR_SHARE: f64 = 0.1;
20const FLOOR_MARGIN_DB: f64 = 10.0;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub enum FitRange {
27 T20,
29 T30,
31}
32
33impl FitRange {
34 #[must_use]
36 pub fn limits(self) -> (Decibels, Decibels) {
37 let (top, bottom) = match self {
38 Self::T20 => (-5.0, -25.0),
39 Self::T30 => (-5.0, -35.0),
40 };
41 (Decibels::from_const(top), Decibels::from_const(bottom))
42 }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq)]
47pub enum DecayFailure {
48 Silent,
50 TooShort,
52 FloorTooHigh {
54 floor: Decibels,
56 limit: Decibels,
58 },
59 RangeNotReached,
62}
63
64impl fmt::Display for DecayFailure {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 Self::Silent => write!(f, "the tail is silent"),
68 Self::TooShort => write!(f, "the tail is too short for a decay fit"),
69 Self::FloorTooHigh { floor, limit } => write!(
70 f,
71 "the noise floor at {floor} is above the {limit} that the fit range needs"
72 ),
73 Self::RangeNotReached => {
74 write!(f, "the decay does not reach the bottom of the fit range")
75 }
76 }
77 }
78}
79
80impl Error for DecayFailure {}
81
82#[derive(Clone, Debug, PartialEq)]
84pub struct Decay {
85 floor: Option<Decibels>,
86 truncation: Seconds,
87 t20: Result<Seconds, DecayFailure>,
88 t30: Result<Seconds, DecayFailure>,
89}
90
91impl Decay {
92 #[must_use]
110 pub fn of(tail: &[f64], rate: SampleRate) -> Self {
111 match Self::analyse(tail, rate) {
112 Ok(decay) => decay,
113 Err(failure) => Self {
114 floor: None,
115 truncation: rate.seconds(tail.len()),
116 t20: Err(failure),
117 t30: Err(failure),
118 },
119 }
120 }
121
122 fn analyse(tail: &[f64], rate: SampleRate) -> Result<Self, DecayFailure> {
123 let fs = rate.hz().get();
124 let block = cast::round_to_usize(BLOCK_SECONDS * fs).unwrap_or(1).max(1);
125 if tail.iter().all(|&x| x == 0.0) {
126 return Err(DecayFailure::Silent);
127 }
128 let loudest = tail
129 .chunks_exact(block)
130 .map(|chunk| chunk.iter().map(|x| x * x).sum::<f64>())
131 .enumerate()
132 .fold(
133 (0, 0.0),
134 |best, (index, sum)| if sum > best.1 { (index, sum) } else { best },
135 )
136 .0;
137 let offset = loudest * block;
138 Self::analyse_from(&tail[offset..], rate, block, offset)
139 }
140
141 fn analyse_from(
144 tail: &[f64],
145 rate: SampleRate,
146 block: usize,
147 offset: usize,
148 ) -> Result<Self, DecayFailure> {
149 let fs = rate.hz().get();
150 let energy: Vec<f64> = tail.iter().map(|x| x * x).collect();
151 if energy.len() < 3 * block {
152 return Err(DecayFailure::TooShort);
153 }
154 let levels: Vec<f64> = energy
155 .chunks_exact(block)
156 .map(|chunk| chunk.iter().sum::<f64>() / cast::usize_to_f64(block))
157 .collect();
158 let reference = levels[0];
159 if reference <= 0.0 {
160 return Err(DecayFailure::Silent);
161 }
162 let floor_start = energy.len()
163 - cast::ceil_to_usize(FLOOR_SHARE * cast::usize_to_f64(energy.len()))
164 .unwrap_or(block)
165 .max(block);
166 let noise = energy[floor_start..].iter().sum::<f64>()
167 / cast::usize_to_f64(energy.len() - floor_start);
168 let floor = (noise > 0.0).then(|| power_db(noise / reference));
169
170 let floor_db = floor.unwrap_or(f64::NEG_INFINITY);
172 let points: Vec<(f64, f64)> = levels
173 .iter()
174 .enumerate()
175 .map(|(index, &level)| {
176 let centre = (cast::usize_to_f64(index) + 0.5) * cast::usize_to_f64(block) / fs;
177 (
178 centre,
179 if level > 0.0 {
180 power_db(level / reference)
181 } else {
182 f64::NEG_INFINITY
183 },
184 )
185 })
186 .take_while(|&(_, level)| level >= floor_db + FLOOR_MARGIN_DB)
187 .collect();
188 if points.len() < 2 {
189 return Err(DecayFailure::FloorTooHigh {
190 floor: Decibels::clamped(floor_db),
191 limit: Decibels::clamped(-FLOOR_MARGIN_DB),
192 });
193 }
194 let (slope, intercept) = fit_line(&points).ok_or(DecayFailure::TooShort)?;
195 let end = cast::usize_to_f64(energy.len()) / fs;
196 let (cut, compensation) = if slope < 0.0 && floor.is_some() {
197 let crossing = ((floor_db - intercept) / slope).clamp(0.0, end);
198 let tau = 10.0 / (std::f64::consts::LN_10 * slope.abs());
200 (crossing, noise * tau * fs)
201 } else {
202 (end, 0.0)
203 };
204 let cut_index = cast::floor_to_usize(cut * fs)
205 .unwrap_or(energy.len())
206 .clamp(1, energy.len());
207 let mut curve = backward_integral(&energy[..cut_index]);
208 for value in &mut curve {
209 *value += compensation;
210 }
211 Ok(Self {
212 floor: floor.map(Decibels::clamped),
213 truncation: rate.seconds(offset + cut_index),
214 t20: fit_range(&curve, fs, floor, FitRange::T20),
215 t30: fit_range(&curve, fs, floor, FitRange::T30),
216 })
217 }
218
219 #[must_use]
222 pub fn floor(&self) -> Option<Decibels> {
223 self.floor
224 }
225
226 #[must_use]
228 pub fn truncation(&self) -> Seconds {
229 self.truncation
230 }
231
232 pub fn t20(&self) -> Result<Seconds, DecayFailure> {
238 self.t20
239 }
240
241 pub fn t30(&self) -> Result<Seconds, DecayFailure> {
247 self.t30
248 }
249}
250
251fn power_db(ratio: f64) -> f64 {
252 10.0 * ratio.log10()
253}
254
255#[must_use]
258pub fn backward_integral(energy: &[f64]) -> Vec<f64> {
259 let mut curve = vec![0.0; energy.len()];
260 let mut sum = 0.0;
261 for (slot, &value) in curve.iter_mut().zip(energy).rev() {
262 sum += value;
263 *slot = sum;
264 }
265 curve
266}
267
268fn fit_range(
269 curve: &[f64],
270 fs: f64,
271 floor: Option<f64>,
272 range: FitRange,
273) -> Result<Seconds, DecayFailure> {
274 let (top, bottom) = range.limits();
275 let (top, bottom) = (top.get(), bottom.get());
276 if let Some(floor) = floor
277 && floor > bottom - FLOOR_MARGIN_DB
278 {
279 return Err(DecayFailure::FloorTooHigh {
280 floor: Decibels::clamped(floor),
281 limit: Decibels::clamped(bottom - FLOOR_MARGIN_DB),
282 });
283 }
284 let start = curve
285 .first()
286 .copied()
287 .filter(|&v| v > 0.0)
288 .ok_or(DecayFailure::Silent)?;
289 let levels: Vec<f64> = curve.iter().map(|&v| power_db(v / start)).collect();
290 if levels.last().is_none_or(|&last| last > bottom) {
291 return Err(DecayFailure::RangeNotReached);
292 }
293 let points: Vec<(f64, f64)> = levels
294 .iter()
295 .enumerate()
296 .filter(|&(_, &level)| level <= top && level >= bottom)
297 .map(|(n, &level)| (cast::usize_to_f64(n) / fs, level))
298 .collect();
299 let (slope, _) = fit_line(&points).ok_or(DecayFailure::TooShort)?;
300 if slope >= 0.0 {
301 return Err(DecayFailure::RangeNotReached);
302 }
303 Ok(Seconds::clamped(-60.0 / slope))
304}
305
306pub(crate) fn fit_line(points: &[(f64, f64)]) -> Option<(f64, f64)> {
308 if points.len() < 2 {
309 return None;
310 }
311 let n = cast::usize_to_f64(points.len());
312 let mean_x = points.iter().map(|p| p.0).sum::<f64>() / n;
313 let mean_y = points.iter().map(|p| p.1).sum::<f64>() / n;
314 let sxx: f64 = points.iter().map(|p| (p.0 - mean_x).powi(2)).sum();
315 let sxy: f64 = points.iter().map(|p| (p.0 - mean_x) * (p.1 - mean_y)).sum();
316 (sxx > 0.0).then(|| {
317 let slope = sxy / sxx;
318 (slope, mean_y - slope * mean_x)
319 })
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn the_fit_limits_pass_the_unit_check() {
328 let db = |v| Decibels::new(v).unwrap();
329 assert_eq!(FitRange::T20.limits(), (db(-5.0), db(-25.0)));
330 assert_eq!(FitRange::T30.limits(), (db(-5.0), db(-35.0)));
331 }
332
333 fn rate() -> SampleRate {
334 SampleRate::new(16_000).unwrap()
335 }
336
337 fn exponential(rt60: f64, seconds: f64) -> Vec<f64> {
338 let fs = 16_000.0;
339 (0..cast::floor_to_usize(seconds * fs).unwrap())
340 .map(|n| (-6.907_755_278_982_137 * cast::usize_to_f64(n) / fs / rt60).exp())
341 .collect()
342 }
343
344 #[test]
345 fn the_backward_integral_sums_to_the_end() {
346 assert_eq!(backward_integral(&[1.0, 2.0, 3.0]), vec![6.0, 5.0, 3.0]);
347 }
348
349 #[test]
350 fn a_pure_exponential_decays_60_db_per_rt60() {
351 let decay = Decay::of(&exponential(0.4, 2.0), rate());
353 for time in [decay.t20().unwrap(), decay.t30().unwrap()] {
354 assert!((time.get() - 0.4).abs() < 0.4e-3, "{time:?}");
355 }
356 }
357
358 #[test]
359 fn silence_and_short_tails_fail() {
360 assert_eq!(
361 Decay::of(&vec![0.0; 16_000], rate()).t20(),
362 Err(DecayFailure::Silent)
363 );
364 assert_eq!(
365 Decay::of(&[1.0; 100], rate()).t30(),
366 Err(DecayFailure::TooShort)
367 );
368 }
369
370 #[test]
371 fn a_flat_tail_never_reaches_the_range() {
372 let decay = Decay::of(&vec![0.5; 16_000], rate());
373 assert!(
374 matches!(decay.t20(), Err(DecayFailure::FloorTooHigh { .. })),
375 "{decay:?}"
376 );
377 }
378}