1use crate::audio::SampleRate;
5use crate::cast;
6use crate::dsp::{FilterError, fit_line, quantile};
7use crate::notes::Note;
8use crate::units::{Decibels, Hz, MidiPitch, Seconds};
9
10use super::knee::{bend, find_knee_against};
11use super::median;
12use super::pump::Pump;
13use super::tail::{TAIL_BAND_PERIODS, TAIL_HOLD_FRAMES, TAIL_HOLD_LAG, TailFit, fit_tail};
14use super::track::{
15 HARMONIC_CUTOFF_MAX, HARMONIC_CUTOFF_MIN, HOP, LevelTrack, RMS_PERIODS, rms_window,
16};
17
18pub const ATTACK_TOP: Decibels = Decibels::from_const(1.0);
29pub const RAMP_FACTOR: f64 = 1.25;
32pub const ATTACK_PERIODS_MIN: f64 = 1.5;
41pub const BAND_STEP_RISE: f64 = 0.49;
45pub const BAND_STEPS_MIN: f64 = 2.0;
49pub const HOLD_MIN: Seconds = Seconds::from_const(0.01);
52pub const SUSTAIN_SHARE: f64 = 0.3;
54pub const DECAY_SHARES: (f64, f64) = (0.1, 0.9);
64pub const DECAY_FIRST_DB: Decibels = Decibels::from_const(0.5);
70pub const OBSERVABLE_DECAYS: f64 = 3.0;
73pub const FIT_FLOOR: Decibels = Decibels::from_const(60.0);
76pub const SILENCE_DB: Decibels = Decibels::from_const(-120.0);
80pub const TRACK_LEAD: Seconds = Seconds::from_const(0.03);
83const SPAN_MARGIN: Seconds = Seconds::from_const(0.3);
85pub const KNEE_END_GAP: Seconds = Seconds::from_const(0.025);
91pub const FREE_GAP: Hz = Hz::from_const(30.0);
94pub const PROXY_HARMONICS: u32 = 3;
102const LEVEL_FRAMES: usize = 50;
104
105#[derive(Clone, Debug, PartialEq)]
107pub struct NoteEnvelope {
108 pub onset: Seconds,
110 pub end: Seconds,
113 pub pitch: MidiPitch,
115 pub peak_db: Decibels,
117 pub attack: Option<Seconds>,
120 pub attack_upper_bound: bool,
124 pub hold: Seconds,
126 pub decay: Option<Seconds>,
130 pub sustain: Option<Decibels>,
132 pub knee: Option<Seconds>,
134 pub ripple: Option<Decibels>,
138 pub residual: Option<Decibels>,
141 pub overlap: bool,
144 pub tail: Option<TailFit>,
146}
147
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub enum NoteSkip {
151 NoFreeHarmonic,
155 TooShort,
157 Silent,
159}
160
161pub fn read_note(
170 mono: &[f64],
171 rate: SampleRate,
172 note: &Note,
173 others: &[&Note],
174 pump: Option<&Pump>,
175) -> Result<Result<NoteEnvelope, NoteSkip>, FilterError> {
176 let f0 = Hz::from(note.pitch());
177 let (from, to) = span(mono, rate, note);
178 if to.get() - note.onset().get() < 2.0 * HOLD_MIN.get() {
179 return Ok(Err(NoteSkip::TooShort));
180 }
181 let partials = proxy_partials(f0, others, rate);
182 if partials.is_empty() {
183 return Ok(Err(NoteSkip::NoFreeHarmonic));
184 }
185 let overlap = !others.is_empty();
186 let (proxy, timing, mix) = tracks(mono, rate, (from, to), f0, &partials, overlap, pump)?;
187 let onset = proxy.index(note.onset().get());
188 let peak_db = proxy.db()[onset..]
189 .iter()
190 .copied()
191 .fold(f64::NEG_INFINITY, f64::max);
192 if peak_db <= SILENCE_DB.get() {
193 return Ok(Err(NoteSkip::Silent));
194 }
195 let resolution = if overlap {
196 BAND_STEPS_MIN * BAND_STEP_RISE / partials[0].1.get()
197 } else {
198 ATTACK_PERIODS_MIN * rms_window(f0, RMS_PERIODS).get()
199 };
200 let (attack, attack_upper_bound) = match rise(&timing, onset, resolution) {
201 Some(rise) => (
202 Some(Seconds::clamped(RAMP_FACTOR * rise)),
203 rise < resolution,
204 ),
205 None => (None, false),
206 };
207 let rel: Vec<f64> = proxy
208 .db()
209 .iter()
210 .map(|db| (db - peak_db).max(-FIT_FLOOR.get()))
211 .collect();
212 let top = (onset..rel.len())
213 .find(|&i| rel[i] >= -ATTACK_TOP.get())
214 .unwrap_or(onset);
215 let knee_end = proxy.index(to.get() - KNEE_END_GAP.get());
216 let settle = settle_frames(overlap, f0, &partials);
217 let frames = knee_end.saturating_sub(top);
218 let clear = clear_frames(&proxy, top, frames, others, to, settle);
219 let knee = note_off(&proxy, mix.as_ref(), (onset, top, knee_end), clear);
220 let held_end = knee.map_or(knee_end, |k| proxy.index(k)).max(top + 1);
221 let window = cast::floor_to_usize(SUSTAIN_SHARE * cast::usize_to_f64(held_end - top))
222 .unwrap_or(0)
223 .max(1);
224 let sustain_window = &rel[held_end - window..held_end];
225 let sustain = median(sustain_window).unwrap_or(0.0);
226 let peak = (top..held_end)
229 .max_by(|&a, &b| rel[a].total_cmp(&rel[b]))
230 .unwrap_or(top);
231 let fall = (sustain < -ATTACK_TOP.get())
232 .then(|| decay_fall(&rel, peak, held_end, sustain))
233 .flatten();
234 let plateau = fall.map_or(0.0, |(start, _)| {
237 start - cast::usize_to_f64(top) * HOP.get()
238 });
239 let hold = if plateau > HOLD_MIN.get() {
240 plateau
241 } else {
242 0.0
243 };
244 let decay = fall.map(|(_, decay)| decay);
245 let tail = knee
246 .and_then(|k| read_tail(&timing, onset, k, to, others, f0))
247 .map(|fit| TailFit {
248 rt60_floor: rt60_floor(overlap, &partials),
249 ..fit
250 });
251 let held_time = cast::usize_to_f64(held_end - onset) * HOP.get();
252 let observable = knee.is_none()
255 || held_time
256 >= attack.map_or(0.0, Seconds::get) + hold + OBSERVABLE_DECAYS * decay.unwrap_or(0.0);
257 Ok(Ok(NoteEnvelope {
258 onset: note.onset(),
259 end: to,
260 pitch: note.pitch(),
261 peak_db: Decibels::clamped(peak_db),
262 attack,
263 attack_upper_bound,
264 hold: Seconds::clamped(hold),
265 decay: decay.filter(|_| observable).map(Seconds::clamped),
266 sustain: observable.then(|| Decibels::clamped(sustain)),
267 knee: knee.map(Seconds::clamped),
268 ripple: observable
269 .then(|| ripple(sustain_window))
270 .flatten()
271 .map(Decibels::clamped),
272 residual: observable
273 .then(|| residual(&rel[top..held_end], hold, decay, sustain))
274 .flatten()
275 .map(Decibels::clamped),
276 overlap,
277 tail,
278 }))
279}
280
281fn clear_frames(
284 proxy: &LevelTrack,
285 top: usize,
286 frames: usize,
287 others: &[&Note],
288 to: Seconds,
289 settle: usize,
290) -> usize {
291 let next = others
292 .iter()
293 .map(|n| n.onset().get())
294 .filter(|&t| t > proxy.time(top))
295 .fold(to.get(), f64::min);
296 proxy
297 .index(next)
298 .min(top + frames)
299 .saturating_sub(settle + top)
300}
301
302fn settle_frames(overlap: bool, f0: Hz, partials: &[(Hz, Hz)]) -> usize {
306 let reach = if overlap {
307 let narrowest = partials
308 .iter()
309 .map(|p| p.1.get())
310 .fold(f64::INFINITY, f64::min);
311 BAND_STEPS_MIN * BAND_STEP_RISE / narrowest
312 } else {
313 0.5 * rms_window(f0, RMS_PERIODS).get()
314 };
315 cast::round_to_usize(reach / HOP.get()).unwrap_or(0)
316}
317
318fn rt60_floor(overlap: bool, partials: &[(Hz, Hz)]) -> Seconds {
321 let narrowest = partials
322 .iter()
323 .map(|p| p.1.get())
324 .fold(f64::INFINITY, f64::min);
325 Seconds::clamped(if overlap {
326 TAIL_BAND_PERIODS / narrowest
327 } else {
328 0.0
329 })
330}
331
332fn read_tail(
335 timing: &LevelTrack,
336 onset: usize,
337 k: f64,
338 to: Seconds,
339 others: &[&Note],
340 f0: Hz,
341) -> Option<TailFit> {
342 let stop = others
343 .iter()
344 .map(|n| n.onset().get())
345 .filter(|&t| t > k)
346 .fold(to.get(), f64::min);
347 let stop = stop - 0.5 * rms_window(f0, RMS_PERIODS).get();
349 let (a, b) = (timing.index(k), timing.index(stop));
350 let from = a.saturating_sub(LEVEL_FRAMES).max(onset.min(a));
351 let level = median(&timing.db()[from..a]).unwrap_or(0.0);
352 let db: Vec<f64> = timing.db()[a.min(b)..b].iter().map(|v| v - level).collect();
353 let held = a.saturating_sub(TAIL_HOLD_FRAMES).max(onset.min(a));
354 let hold_bend = Decibels::clamped(bend(timing.db(), held, a, TAIL_HOLD_LAG));
355 fit_tail(&db).map(|fit| TailFit { hold_bend, ..fit })
356}
357
358fn span(mono: &[f64], rate: SampleRate, note: &Note) -> (Seconds, Seconds) {
361 let stem_end = cast::usize_to_f64(mono.len()) / rate.hz().get();
362 (
363 Seconds::clamped(note.onset().get() - TRACK_LEAD.get()),
364 Seconds::clamped(note.offset().get().min(stem_end)),
365 )
366}
367
368fn residual(part: &[f64], hold: f64, decay: Option<f64>, sustain: f64) -> Option<f64> {
372 if part.is_empty() {
373 return None;
374 }
375 let decay = decay.unwrap_or(0.0);
376 let model = |t: f64| {
377 if t < hold {
378 0.0
379 } else if t < hold + decay {
380 sustain * (t - hold) / decay
381 } else {
382 sustain
383 }
384 };
385 let sum: f64 = part
386 .iter()
387 .enumerate()
388 .map(|(i, db)| (db - model(cast::usize_to_f64(i) * HOP.get())).powi(2))
389 .sum();
390 Some((sum / cast::usize_to_f64(part.len())).sqrt())
391}
392
393pub fn amp_proxy(
402 mono: &[f64],
403 rate: SampleRate,
404 note: &Note,
405 others: &[&Note],
406) -> Result<Option<LevelTrack>, FilterError> {
407 let f0 = Hz::from(note.pitch());
408 let (from, to) = span(mono, rate, note);
409 let partials = proxy_partials(f0, others, rate);
410 if partials.is_empty() {
411 return Ok(None);
412 }
413 let (proxy, _, _) = tracks(
414 mono,
415 rate,
416 (from, to),
417 f0,
418 &partials,
419 !others.is_empty(),
420 None,
421 )?;
422 Ok(Some(proxy))
423}
424
425fn note_off(
430 proxy: &LevelTrack,
431 mix: Option<&LevelTrack>,
432 (onset, top, end): (usize, usize, usize),
433 clear: usize,
434) -> Option<f64> {
435 let amp = knee_track(proxy, onset, top, end);
436 let level = mix.map(|mix| knee_track(mix, onset, top, end));
437 find_knee_against(
438 proxy.time(top),
439 &,
440 level.as_deref().unwrap_or(&),
441 clear,
442 )
443}
444
445fn knee_track(track: &LevelTrack, onset: usize, top: usize, end: usize) -> Vec<f64> {
448 let db = track.db();
449 let peak = db[onset..]
450 .iter()
451 .copied()
452 .fold(f64::NEG_INFINITY, f64::max);
453 db[top..end.max(top)]
454 .iter()
455 .map(|v| 10f64.powf((v - peak).max(-FIT_FLOOR.get()) / 20.0))
456 .collect()
457}
458
459fn tracks(
467 mono: &[f64],
468 rate: SampleRate,
469 (from, to): (Seconds, Seconds),
470 f0: Hz,
471 partials: &[(Hz, Hz)],
472 overlap: bool,
473 pump: Option<&Pump>,
474) -> Result<(LevelTrack, LevelTrack, Option<LevelTrack>), FilterError> {
475 let fs = rate.hz().get();
480 let lo = cast::floor_to_usize((from.get() - SPAN_MARGIN.get()).max(0.0) * fs).unwrap_or(0);
481 let hi = cast::ceil_to_usize((to.get() + SPAN_MARGIN.get()) * fs)
482 .unwrap_or(mono.len())
483 .min(mono.len());
484 let span = &mono[lo.min(hi)..hi];
485 let mut proxy = if overlap {
486 let offset = Seconds::clamped(cast::usize_to_f64(lo) / fs);
487 LevelTrack::partials(span, offset, rate, (from, to), partials)?
488 } else {
489 LevelTrack::harmonic(span, lo, rate, (from, to), partials[0].0, f0)
490 };
491 let mut timing = if overlap {
492 proxy.clone()
493 } else {
494 LevelTrack::rms(span, lo, rate, (from, to), f0, RMS_PERIODS)
495 };
496 let mix = pump
497 .filter(|p| p.strips() && p.carries(from.get(), to.get()))
498 .map(|pump| {
499 let mix = proxy.clone();
500 pump.strip(&mut proxy);
501 pump.strip(&mut timing);
502 mix
503 });
504 Ok((proxy, timing, mix))
505}
506
507fn proxy_partials(f0: Hz, others: &[&Note], rate: SampleRate) -> Vec<(Hz, Hz)> {
515 let nyquist = rate.nyquist().get();
516 (1..=PROXY_HARMONICS)
517 .filter_map(|k| {
518 let f = f64::from(k) * f0.get();
519 let foreign = others
520 .iter()
521 .map(|n| {
522 let g = Hz::from(n.pitch()).get();
523 let nearest = (f / g).round().max(1.0) * g;
524 (f - nearest).abs()
525 })
526 .fold(f64::INFINITY, f64::min);
527 if foreign < FREE_GAP.get() {
528 return None;
529 }
530 let cutoff = (0.5 * foreign.min(f0.get()))
531 .clamp(HARMONIC_CUTOFF_MIN.get(), HARMONIC_CUTOFF_MAX.get());
532 (f > cutoff && f + cutoff < nyquist).then(|| (Hz::clamped(f), Hz::clamped(cutoff)))
533 })
534 .take(1)
535 .collect()
536}
537
538fn rise(track: &LevelTrack, onset: usize, resolution: f64) -> Option<f64> {
551 let db = track.db();
552 let peak = attack_top(db, onset, resolution)?;
553 let amp: Vec<f64> = db.iter().map(|d| 10f64.powf((d - peak) / 20.0)).collect();
554 let up = (onset.max(1)..amp.len()).find(|&i| amp[i] >= 0.9)?;
555 let t90 = between(&, up, 0.9);
556 let low = (0..up).rev().find(|&i| amp[i] < 0.1)?;
557 let t10 = between(&, low + 1, 0.1);
558 Some((t90 - t10) * HOP.get())
559}
560
561fn attack_top(db: &[f64], onset: usize, resolution: f64) -> Option<f64> {
563 let tenth = 20.0;
565 let least = cast::ceil_to_usize(resolution / HOP.get()).unwrap_or(0);
566 let first = onset.max(1);
567 let mut lowest = db[..first.min(db.len())]
568 .iter()
569 .copied()
570 .fold(f64::INFINITY, f64::min);
571 (first..db.len()).find_map(|i| {
572 let low = lowest;
575 lowest = lowest.min(db[i]);
576 if low >= db[i] - tenth {
577 return None;
578 }
579 let start = (0..i).rev().find(|&j| db[j] < db[i] - tenth)?;
580 let ahead = db[i..=(i + (i - start).max(least)).min(db.len() - 1)]
581 .iter()
582 .copied()
583 .fold(f64::NEG_INFINITY, f64::max);
584 (ahead < db[i] + ATTACK_TOP.get()).then_some(ahead)
585 })
586}
587
588fn between(amp: &[f64], i: usize, level: f64) -> f64 {
591 let (a, b) = (amp[i - 1], amp[i]);
592 let w = if b > a { (level - a) / (b - a) } else { 1.0 };
593 cast::usize_to_f64(i - 1) + w.clamp(0.0, 1.0)
594}
595
596fn decay_fall(rel: &[f64], from: usize, to: usize, sustain: f64) -> Option<(f64, f64)> {
604 let (lo, hi) = DECAY_SHARES;
605 let lo = lo.max(DECAY_FIRST_DB.get() / -sustain);
606 let first = falls_through(rel, from, to, lo * sustain)?;
607 let second = falls_through(rel, cast::floor_to_usize(first)?, to, hi * sustain)?;
608 let decay = (second - first) * HOP.get() / (hi - lo);
609 Some((first * HOP.get() - lo * decay, decay))
610}
611
612fn falls_through(rel: &[f64], from: usize, to: usize, level: f64) -> Option<f64> {
615 let i = (from.max(1)..to.min(rel.len())).find(|&i| rel[i] <= level)?;
616 let (a, b) = (rel[i - 1], rel[i]);
617 let w = if a > b { (a - level) / (a - b) } else { 1.0 };
618 Some(cast::usize_to_f64(i - 1) + w.clamp(0.0, 1.0))
619}
620
621fn ripple(values: &[f64]) -> Option<f64> {
624 if values.len() < 10 {
625 return None;
626 }
627 let points: Vec<(f64, f64)> = values
628 .iter()
629 .enumerate()
630 .map(|(i, &v)| (cast::usize_to_f64(i), v))
631 .collect();
632 let b = fit_line(&points).map_or(0.0, |(slope, _)| slope);
633 let mut rest: Vec<f64> = points.iter().map(|&(x, y)| y - b * x).collect();
634 rest.sort_by(f64::total_cmp);
635 Some(quantile(&rest, 0.9)? - quantile(&rest, 0.1)?)
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[test]
643 fn the_tail_level_is_the_median_of_the_50_frames_before_the_knee() {
644 let read = |last: usize| {
647 let fall: Vec<f64> = (0..300).map(|i| -0.1 * f64::from(i)).collect();
648 let mut db = vec![-6.0; 1000 - last];
649 db.extend(vec![0.0; last]);
650 db.extend(&fall);
651 let track = LevelTrack::new(Seconds::clamped(0.0), db);
652 let f0 = Hz::clamped(440.0);
653 let knee = 1000.0 * HOP.get();
654 read_tail(&track, 0, knee, Seconds::clamped(1.3), &[], f0).unwrap()
655 };
656 let weight_at = |level: f64| {
657 let f0_frames = cast::round_to_usize(
658 0.5 * rms_window(Hz::clamped(440.0), RMS_PERIODS).get() / HOP.get(),
659 )
660 .unwrap();
661 let db: Vec<f64> = (0..300 - f0_frames)
662 .map(|i| -0.1 * cast::usize_to_f64(i) - level)
663 .collect();
664 fit_tail(&db).unwrap().weight
665 };
666 assert!((read(20).weight - weight_at(-6.0)).abs() < 1e-9);
668 assert!((read(30).weight - weight_at(0.0)).abs() < 1e-9);
670 }
671
672 #[test]
673 fn the_shortest_rt60_is_6_periods_of_the_narrowest_cutoff() {
674 let partials = [
675 (Hz::clamped(220.0), Hz::clamped(40.0)),
676 (Hz::clamped(440.0), Hz::clamped(20.0)),
677 ];
678 assert_eq!(rt60_floor(true, &partials).get(), 6.0 / 20.0);
679 assert_eq!(rt60_floor(false, &partials).get(), 0.0);
680 }
681
682 #[test]
683 fn the_settle_is_half_a_level_window_alone_and_two_band_rises_in_an_overlap() {
684 let partials = [
685 (Hz::clamped(100.0), Hz::clamped(40.0)),
686 (Hz::clamped(200.0), Hz::clamped(20.0)),
687 ];
688 assert_eq!(settle_frames(false, Hz::clamped(50.0), &partials), 10);
690 assert_eq!(settle_frames(true, Hz::clamped(50.0), &partials), 49);
691 }
692
693 #[test]
694 fn the_hold_bend_reads_300_frames_before_the_knee_25_each_way() {
695 let triangle = |i: usize, peak: f64| {
698 let phase = cast::usize_to_f64(i % 50) / 12.5;
699 peak * if phase < 2.0 {
700 phase - 1.0
701 } else {
702 3.0 - phase
703 }
704 };
705 let read = |flat_frames: usize, bent: usize| {
706 let mut db = vec![0.0; flat_frames];
707 db.extend((0..bent).map(|i| triangle(i, 1.0)));
708 let knee = db.len();
709 db.extend((0..300).map(|i| -0.1 * f64::from(i)));
710 let track = LevelTrack::new(Seconds::clamped(0.0), db);
711 let end = cast::usize_to_f64(knee + 300) * HOP.get();
712 read_tail(
713 &track,
714 0,
715 cast::usize_to_f64(knee) * HOP.get(),
716 Seconds::clamped(end),
717 &[],
718 Hz::clamped(440.0),
719 )
720 .unwrap()
721 .hold_bend
722 .get()
723 };
724 assert!((read(1000, 300) - 2.0).abs() < 0.2, "{}", read(1000, 300));
725 assert!(read(1000, 100) < 0.01, "{}", read(1000, 100));
728 }
729
730 #[test]
731 fn the_residual_is_the_rms_against_hold_decay_and_sustain() {
732 let model: Vec<f64> = (0..100)
734 .map(|i| {
735 let t = cast::usize_to_f64(i) * HOP.get();
736 if t < 0.01 {
737 0.0
738 } else if t < 0.06 {
739 -6.0 * (t - 0.01) / 0.05
740 } else {
741 -6.0
742 }
743 })
744 .collect();
745 assert!(residual(&model, 0.01, Some(0.05), -6.0).unwrap() < 1e-9);
746 let off: Vec<f64> = model.iter().map(|v| v + 1.0).collect();
747 assert!((residual(&off, 0.01, Some(0.05), -6.0).unwrap() - 1.0).abs() < 1e-9);
748 let step: Vec<f64> = (0..20).map(|i| if i < 10 { 0.0 } else { -3.0 }).collect();
750 assert!(residual(&step, 0.0095, None, -3.0).unwrap() < 1e-9);
751 assert!(residual(&[], 0.0, None, 0.0).is_none());
752 }
753
754 #[test]
755 fn a_fall_through_a_level_interpolates_between_frames() {
756 let ramp = [0.0, -1.0, -2.0, -3.0, -4.0];
757 assert!((falls_through(&ramp, 0, 5, -2.5).unwrap() - 2.5).abs() < 1e-12);
758 assert!((falls_through(&ramp, 4, 5, -0.5).unwrap() - 3.0).abs() < 1e-12);
760 assert!(falls_through(&ramp, 0, 3, -2.5).is_none());
761 assert!((falls_through(&[-3.0, -3.0], 0, 2, -3.0).unwrap() - 1.0).abs() < 1e-12);
763 }
764
765 #[test]
766 fn the_decay_reads_a_fall_linear_in_db_exactly() {
767 for sustain in [-10.0, -4.0] {
770 let rel: Vec<f64> = (0..200)
771 .map(|i| sustain * (cast::usize_to_f64(i) / 100.0).min(1.0))
772 .collect();
773 let (start, decay) = decay_fall(&rel, 0, 200, sustain).unwrap();
774 assert!(
775 start.abs() < 1e-9 && (decay - 0.1).abs() < 1e-9,
776 "{sustain}: {start} {decay}"
777 );
778 }
779 let rel: Vec<f64> = (0..200)
783 .map(|i| {
784 let i = cast::usize_to_f64(i);
785 if i < 20.0 {
786 -0.1 * i
787 } else {
788 (-2.0 - 2.0 * (i - 20.0) / 80.0).max(-4.0)
789 }
790 })
791 .collect();
792 let (start, decay) = decay_fall(&rel, 0, 200, -4.0).unwrap();
793 let want = 0.079 / 0.775;
794 assert!((decay - want).abs() < 1e-9, "{decay}");
795 assert!((start - (0.005 - 0.125 * want)).abs() < 1e-9, "{start}");
796 let rel: Vec<f64> = (0..200)
798 .map(|i| -10.0 * (cast::usize_to_f64(i) / 100.0).min(1.0))
799 .collect();
800 assert!(decay_fall(&rel, 0, 80, -10.0).is_none());
801 }
802
803 #[test]
804 fn the_attack_top_looks_ahead_at_least_over_the_resolution() {
805 let mut db: Vec<f64> = vec![-60.0; 5];
808 db.extend([-40.0, -20.0, -20.0, -20.0, -20.0, -10.0, -5.0, 0.0]);
809 db.extend([0.0; 20]);
810 assert_eq!(attack_top(&db, 0, 0.0), Some(-20.0));
811 assert_eq!(attack_top(&db, 0, 0.01), Some(0.0));
812 }
813
814 #[test]
815 fn the_ripple_is_the_spread_after_the_line() {
816 let values: Vec<f64> = (0..200)
818 .map(|i| 0.5 * cast::usize_to_f64(i) + if i % 2 == 0 { 1.0 } else { -1.0 })
819 .collect();
820 assert!((ripple(&values).unwrap() - 2.0).abs() < 0.05);
821 let flat: Vec<f64> = (0..20)
822 .map(|i| -3.0 - 0.2 * cast::usize_to_f64(i))
823 .collect();
824 assert!(ripple(&flat).unwrap().abs() < 1e-9);
825 assert!(ripple(&[0.0; 9]).is_none());
826 assert_eq!(ripple(&[0.0; 10]), Some(0.0));
827 }
828
829 #[test]
830 fn the_constants_pass_the_unit_check() {
831 assert_eq!(ATTACK_TOP, Decibels::new(1.0).unwrap());
832 assert_eq!(HOLD_MIN, Seconds::new(0.01).unwrap());
833 assert_eq!(FIT_FLOOR, Decibels::new(60.0).unwrap());
834 assert_eq!(DECAY_FIRST_DB, Decibels::new(0.5).unwrap());
835 assert_eq!(TRACK_LEAD, Seconds::new(0.03).unwrap());
836 assert_eq!(KNEE_END_GAP, Seconds::new(0.025).unwrap());
837 assert_eq!(FREE_GAP, Hz::new(30.0).unwrap());
838 }
839
840 #[test]
841 fn a_linear_ramp_rises_from_10_to_90_percent_in_80_percent_of_its_time() {
842 let db: Vec<f64> = (0..300)
844 .map(|i| {
845 let a = (cast::usize_to_f64(i) - 50.0).clamp(0.0, 100.0) / 100.0;
846 20.0 * a.max(1e-9).log10()
847 })
848 .collect();
849 let track = LevelTrack::new(Seconds::clamped(0.0), db);
850 let rise = rise(&track, 0, 0.0).unwrap();
851 assert!((rise - 0.08).abs() < 1e-3, "{rise}");
852 }
853
854 #[test]
855 fn a_harmonic_near_another_notes_harmonic_is_not_free() {
856 let rate = SampleRate::new(44_100).unwrap();
857 let fifth = Note::new(
858 Seconds::clamped(0.0),
859 Seconds::clamped(1.0),
860 crate::units::MidiPitch::new(52.0).unwrap(),
861 )
862 .unwrap();
863 let kept: Vec<f64> = proxy_partials(Hz::new(110.0).unwrap(), &[&fifth], rate)
866 .iter()
867 .map(|p| p.0.get())
868 .collect();
869 assert_eq!(kept, vec![110.0]);
870 let near = Note::new(
873 Seconds::clamped(0.0),
874 Seconds::clamped(1.0),
875 crate::units::MidiPitch::try_from(Hz::new(120.0).unwrap()).unwrap(),
876 )
877 .unwrap();
878 let kept = proxy_partials(Hz::new(110.0).unwrap(), &[&near], rate);
879 assert_eq!(kept.len(), 1);
880 assert!((kept[0].0.get() - 330.0).abs() < 1e-9);
881 assert!((kept[0].1.get() - 15.0).abs() < 1e-6, "{:?}", kept[0].1);
882 let octave = Note::new(
883 Seconds::clamped(0.0),
884 Seconds::clamped(1.0),
885 crate::units::MidiPitch::new(57.0).unwrap(),
886 )
887 .unwrap();
888 assert!(
889 proxy_partials(Hz::new(440.0).unwrap(), &[&octave], rate).is_empty(),
890 "an octave above has no free harmonic"
891 );
892 }
893}