1use super::POOL_NOTES_MIN;
4use super::adsr::NoteEnvelope;
5use super::track::HOP;
6use crate::audio::{Band, SampleRate, stereo_screen};
7use crate::cast;
8use crate::dsp::{StftParams, Summary, describe};
9use crate::estimate::pitch::fit::nelder_mead;
10use crate::notes::Note;
11use crate::units::{Decibels, Seconds};
12
13const RT60_SPAN_SHARE: f64 = 0.25;
16pub const TAIL_FLOOR: Decibels = Decibels::from_const(-55.0);
18const TAIL_FRAMES_MIN: usize = 8;
20const RELEASE_STARTS: [f64; 4] = [0.02, 0.08, 0.3, 1.0];
22const RT60_STARTS: [f64; 3] = [0.4, 1.2, 3.0];
24const WEIGHT_START: f64 = 0.1;
26const DENSE_FRAMES: usize = 200;
28const SPARSE_STEP: usize = 5;
30const FIT_ITERATIONS: usize = 400;
32pub const TAIL_RELEASE_RANGE: (f64, f64) = (0.001, 10.0);
35pub const TAIL_WEIGHT_RANGE: (f64, f64) = (1e-4, 10.0);
37pub const TAIL_RT60_RANGE: (f64, f64) = (0.01, 30.0);
39
40#[derive(Clone, Copy, Debug, PartialEq)]
42pub struct TailFit {
43 pub release: Seconds,
45 pub weight: f64,
47 pub rt60: Seconds,
49 pub rt60_observed: bool,
51 pub residual: Decibels,
53 pub rt60_floor: Seconds,
55 pub hold_bend: Decibels,
58}
59
60impl TailFit {
61 #[must_use]
64 pub fn is_smooth(&self) -> bool {
65 self.weight > TAIL_WEIGHT_MAX && self.residual.get() <= TAIL_SMOOTH.get()
66 }
67
68 #[must_use]
71 pub fn reads_rt60(&self) -> bool {
72 self.weight >= TAIL_WEIGHT_MIN
73 && self.rt60_observed
74 && self.rt60.get() >= self.rt60_floor.get()
75 && !self.is_smooth()
76 }
77}
78
79fn model_db(t: f64, release: f64, weight: f64, rt60: f64) -> f64 {
81 let amplitude = (1.0 - t / release).max(0.0) + weight * 10f64.powf(-3.0 * t / rt60);
82 (20.0 * amplitude.max(1e-12).log10()).max(TAIL_FLOOR.get())
83}
84
85#[must_use]
88pub fn fit_tail(db: &[f64]) -> Option<TailFit> {
89 let floor = TAIL_FLOOR.get();
90 let last = db.iter().rposition(|v| *v > floor)?;
91 let above = db.iter().filter(|v| **v > floor).count();
92 if above < TAIL_FRAMES_MIN {
93 return None;
94 }
95 let data: Vec<(f64, f64, f64)> = db[..=last]
98 .iter()
99 .enumerate()
100 .filter(|(i, _)| *i < DENSE_FRAMES || i % SPARSE_STEP == 0)
101 .map(|(i, v)| {
102 let weight = if i < DENSE_FRAMES {
103 1.0
104 } else {
105 cast::usize_to_f64(SPARSE_STEP)
106 };
107 (cast::usize_to_f64(i) * HOP.get(), v.max(floor), weight)
108 })
109 .collect();
110 let frames: f64 = data.iter().map(|d| d.2).sum();
111 let bounded = |p: &[f64; 3]| {
112 (
113 p[0].exp().clamp(TAIL_RELEASE_RANGE.0, TAIL_RELEASE_RANGE.1),
114 p[1].exp().clamp(TAIL_WEIGHT_RANGE.0, TAIL_WEIGHT_RANGE.1),
115 p[2].exp().clamp(TAIL_RT60_RANGE.0, TAIL_RT60_RANGE.1),
116 )
117 };
118 let cost = |p: &[f64; 3]| -> f64 {
119 let (release, weight, rt60) = bounded(p);
120 data.iter()
121 .map(|&(t, v, w)| w * (v - model_db(t, release, weight, rt60)).powi(2))
122 .sum()
123 };
124 let mut best: Option<([f64; 3], f64)> = None;
125 for release in RELEASE_STARTS {
126 for rt60 in RT60_STARTS {
127 let start = [release.ln(), WEIGHT_START.ln(), rt60.ln()];
128 let (p, value) = nelder_mead(cost, start, [0.3, 0.5, 0.3], FIT_ITERATIONS, 1e-6);
129 if best.is_none_or(|(_, b)| value < b) {
130 best = Some((p, value));
131 }
132 }
133 }
134 let (p, value) = best?;
135 let (release, weight, rt60) = bounded(&p);
136 Some(TailFit {
137 release: Seconds::clamped(release),
138 weight,
139 rt60: Seconds::clamped(rt60),
140 rt60_observed: cast::usize_to_f64(above) * HOP.get() >= RT60_SPAN_SHARE * rt60,
141 residual: Decibels::clamped((value / frames).sqrt()),
142 rt60_floor: Seconds::clamped(0.0),
143 hold_bend: Decibels::clamped(0.0),
144 })
145}
146
147pub const TAIL_WEIGHT_MIN: f64 = 0.05;
150pub const TAIL_WEIGHT_MAX: f64 = 0.5;
153pub const TAIL_RESIDUAL_MAX: Decibels = Decibels::from_const(3.0);
156pub const TAIL_WOBBLE_RESIDUAL: Decibels = Decibels::from_const(2.0);
160pub const TAIL_HOLD_BEND: Decibels = Decibels::from_const(0.3);
162pub const TAIL_HOLD_FRAMES: usize = 300;
164pub const TAIL_HOLD_LAG: usize = 25;
167pub const TAIL_SMOOTH: Decibels = Decibels::from_const(1.1);
170pub const TAIL_BAND_PERIODS: f64 = 6.0;
174pub const GAP_LEVEL: Decibels = Decibels::from_const(-40.0);
177pub const SIDE_GROWTH: Decibels = Decibels::from_const(3.0);
179pub const FLATNESS_RISE: f64 = 0.1;
181pub const SAME_RATE_SPREAD: f64 = 0.25;
184pub const HIGH_BAND_FALL: Decibels = Decibels::from_const(3.0);
187const BODY_SPAN: Seconds = Seconds::from_const(0.2);
189const GAP_SPAN: Seconds = Seconds::from_const(0.02);
191const VOTE_FRAME: usize = 1024;
193
194#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
196pub struct TailVotes {
197 pub outlasts_gap: Option<bool>,
199 pub side_grows: Option<bool>,
201 pub flatness_rises: Option<bool>,
203 pub same_rate: Option<bool>,
205 pub high_band_faster: Option<bool>,
207}
208
209impl TailVotes {
210 #[must_use]
212 pub fn count(self) -> usize {
213 [
214 self.outlasts_gap,
215 self.side_grows,
216 self.flatness_rises,
217 self.same_rate,
218 self.high_band_faster,
219 ]
220 .into_iter()
221 .filter(|v| *v == Some(true))
222 .count()
223 }
224}
225
226fn majority(values: &[bool]) -> Option<bool> {
228 (!values.is_empty()).then(|| 2 * values.iter().filter(|v| **v).count() > values.len())
229}
230
231fn rms_db(x: &[f64]) -> Option<f64> {
232 let energy = x.iter().map(|v| v * v).sum::<f64>();
233 (energy > 0.0).then(|| 10.0 * (energy / cast::usize_to_f64(x.len())).log10())
234}
235
236fn sample_index(t: f64, fs: f64, len: usize) -> usize {
239 cast::round_to_usize(t.max(0.0) * fs)
240 .unwrap_or(len)
241 .min(len)
242}
243
244pub(super) fn read_votes(
246 mono: &[f64],
247 stereo: Option<(&[f64], &[f64])>,
248 rate: SampleRate,
249 notes: &[Note],
250 pool: &[&NoteEnvelope],
251) -> TailVotes {
252 let index = |t: f64| sample_index(t, rate.hz().get(), mono.len());
253 let mut gaps = Vec::new();
254 let mut sides = Vec::new();
255 let mut flats = Vec::new();
256 let mut highs = Vec::new();
257 let mut rates = Vec::new();
258 for note in pool {
259 let (Some(knee), Some(fit)) = (note.knee.map(Seconds::get), note.tail) else {
260 continue;
261 };
262 let next = notes.iter().map(|n| n.onset().get()).find(|t| *t > knee);
263 let stop = next.map_or(note.end.get(), |n| n.min(note.end.get()));
264 let body = index((knee - BODY_SPAN.get()).max(note.onset.get()))..index(knee);
265 let tail = index(knee + fit.release.get())..index(stop);
266 if fit.reads_rt60() {
267 rates.push(fit.rt60.get());
268 }
269 let own = notes
272 .iter()
273 .position(|n| n.onset() == note.onset && n.pitch() == note.pitch);
274 let silent_gap = |next: f64| {
275 !notes.iter().enumerate().any(|(i, n)| {
276 Some(i) != own && n.onset().get() < next && n.offset().get() > next - GAP_SPAN.get()
277 })
278 };
279 if let Some(next) = next
280 .filter(|n| *n - knee > fit.release.get() + GAP_SPAN.get())
281 .filter(|n| silent_gap(*n))
282 {
283 let before = index(next - GAP_SPAN.get())..index(next);
284 if let (Some(gap), Some(level)) = (rms_db(&mono[before]), rms_db(&mono[body.clone()])) {
285 gaps.push(gap - level > GAP_LEVEL.get());
286 }
287 }
288 if tail.is_empty() || body.is_empty() {
289 continue;
290 }
291 if let Some((left, right)) = stereo {
292 let sm = |r: std::ops::Range<usize>| {
293 stereo_screen(left.get(r.clone())?, right.get(r)?, rate, None)
294 .ok()
295 .and_then(|s| s.band(Band::Full).sm_db)
296 .map(Decibels::get)
297 };
298 if let (Some(a), Some(b)) = (sm(body.clone()), sm(tail.clone())) {
299 sides.push(b - a >= SIDE_GROWTH.get());
300 }
301 }
302 let spectra = |r: std::ops::Range<usize>| {
303 describe(
304 &mono[r],
305 rate,
306 StftParams::new(VOTE_FRAME).ok()?,
307 None,
308 false,
309 )
310 .ok()
311 };
312 if let (Some(a), Some(b)) = (spectra(body), spectra(tail.clone()))
313 && let (Some(a), Some(b)) = (a.flatness.value(), b.flatness.value())
314 {
315 flats.push(b.median().get() - a.median().get() >= FLATNESS_RISE);
316 }
317 let middle = tail.start + tail.len() / 2;
318 if let (Some(a), Some(b)) = (spectra(tail.start..middle), spectra(middle..tail.end)) {
319 let high = |d: &crate::dsp::Descriptors| {
320 d.band_share.value().map(|bands| {
321 bands
322 .iter()
323 .filter(|p| p.lo.get() >= 1000.0)
324 .map(|p| p.share.get())
325 .sum::<f64>()
326 })
327 };
328 if let (Some(a), Some(b)) = (high(&a), high(&b))
329 && a > 0.0
330 && b > 0.0
331 {
332 highs.push(10.0 * (b / a).log10() <= -HIGH_BAND_FALL.get());
333 }
334 }
335 }
336 let same_rate = (rates.len() >= POOL_NOTES_MIN)
337 .then(|| Summary::of(&rates).is_some_and(|s| s.iqr() <= SAME_RATE_SPREAD * s.median()));
338 TailVotes {
339 outlasts_gap: majority(&gaps),
340 side_grows: majority(&sides),
341 flatness_rises: majority(&flats),
342 same_rate,
343 high_band_faster: majority(&highs),
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 fn curve(release: f64, weight: f64, rt60: f64) -> Vec<f64> {
352 (0..4000)
353 .map(|i| model_db(cast::usize_to_f64(i) * HOP.get(), release, weight, rt60))
354 .collect()
355 }
356
357 #[test]
358 fn the_fit_recovers_the_model_it_reads() {
359 for (release, weight, rt60) in [(0.05, 0.15, 0.6), (0.2, 0.4, 1.5), (0.02, 0.05, 3.0)] {
360 let fit = fit_tail(&curve(release, weight, rt60)).unwrap();
361 assert!((fit.release.get() / release - 1.0).abs() < 0.03, "{fit:?}");
362 assert!((fit.weight / weight - 1.0).abs() < 0.03, "{fit:?}");
363 assert!((fit.rt60.get() / rt60 - 1.0).abs() < 0.03, "{fit:?}");
364 assert!(fit.residual.get() < 0.1, "{fit:?}");
365 }
366 }
367
368 #[test]
369 fn a_level_that_never_falls_stays_inside_the_ranges() {
370 let fit = fit_tail(&[0.0; 200]).unwrap();
371 let inside = |v: f64, (lo, hi): (f64, f64)| (lo..=hi).contains(&v);
372 assert!(inside(fit.release.get(), TAIL_RELEASE_RANGE), "{fit:?}");
373 assert!(inside(fit.weight, TAIL_WEIGHT_RANGE), "{fit:?}");
374 assert!(inside(fit.rt60.get(), TAIL_RT60_RANGE), "{fit:?}");
375 assert!(fit.residual.get() < 0.1, "{fit:?}");
376 let loud = fit_tail(&[40.0; 200]).unwrap();
377 assert!(inside(loud.weight, TAIL_WEIGHT_RANGE), "{loud:?}");
378 }
379
380 #[test]
381 fn a_time_past_the_last_index_maps_to_the_end() {
382 assert_eq!(sample_index(1e30, 44_100.0, 100), 100);
383 assert_eq!(sample_index(1.0, 50.0, 100), 50);
384 assert_eq!(sample_index(-1.0, 50.0, 100), 0);
385 }
386
387 #[test]
388 fn a_tail_under_the_floor_or_too_short_has_no_fit() {
389 assert_eq!(fit_tail(&[-60.0; 100]), None);
390 let mut one_frame = [-60.0; 100];
391 one_frame[50] = -1.0;
392 assert_eq!(fit_tail(&one_frame), None);
393 assert_eq!(fit_tail(&[-1.0; 7]), None);
394 assert!(fit_tail(&[-1.0; 8]).is_some());
395 }
396
397 #[test]
398 fn an_rt60_counts_over_a_fall_of_15_db_of_it() {
399 let short: Vec<f64> = curve(0.05, 0.3, 3.0)[..500].to_vec();
400 assert!(!fit_tail(&short).unwrap().rt60_observed);
401 assert!(fit_tail(&curve(0.05, 0.3, 3.0)).unwrap().rt60_observed);
402 assert!(
404 fit_tail(&curve(0.05, 0.3, 3.0)[..900])
405 .unwrap()
406 .rt60_observed
407 );
408 let mut spike = vec![-5.0; 300];
410 spike.resize(1500, -60.0);
411 spike[1499] = -5.0;
412 assert!(!fit_tail(&spike).unwrap().rt60_observed);
413 }
414
415 #[test]
416 fn the_votes_count_only_those_in_favour() {
417 let votes = TailVotes {
418 outlasts_gap: Some(true),
419 side_grows: Some(false),
420 flatness_rises: None,
421 same_rate: Some(true),
422 high_band_faster: None,
423 };
424 assert_eq!(votes.count(), 2);
425 assert_eq!(TailVotes::default().count(), 0);
426 }
427
428 #[test]
429 fn a_majority_needs_more_than_half() {
430 assert_eq!(majority(&[]), None);
431 assert_eq!(majority(&[true, false]), Some(false));
432 assert_eq!(majority(&[true, true, false]), Some(true));
433 }
434}