1use crate::cast;
12use crate::units::Decibels;
13
14use super::track::HOP;
15
16pub const KNEE_SIDE_MIN: usize = 10;
18pub const KNEE_LEFT: usize = 100;
20pub const KNEE_RIGHT: usize = 200;
22pub const KNEE_STEEPEN: f64 = 0.5;
26pub const KNEE_GAIN: f64 = 0.5;
29pub const KNEE_FALL: f64 = 0.5;
32pub const KNEE_FLOOR: f64 = 0.02;
39
40pub const KNEE_BEND: Decibels = Decibels::from_const(0.4);
47pub const KNEE_BEND_LAG: usize = 10;
49pub const KNEE_RISE: f64 = 0.05;
53pub const KNEE_RISE_FRAMES: usize = 20;
56pub const KNEE_BACK: f64 = 1.25;
69
70const KNEE_LEAVE: f64 = 0.85;
72const KNEE_DB_FLOOR: f64 = 1e-4;
74
75#[must_use]
114pub fn find_knee(start: f64, amp: &[f64], clear: usize) -> Option<f64> {
115 find_knee_against(start, amp, amp, clear)
116}
117
118pub(super) fn find_knee_against(
125 start: f64,
126 amp: &[f64],
127 level: &[f64],
128 clear: usize,
129) -> Option<f64> {
130 debug_assert_eq!(amp.len(), level.len());
131 let n = amp.len();
132 if n < 2 * KNEE_SIDE_MIN {
133 return None;
134 }
135 let sums = Sums::of(amp);
136 let mut corners: Vec<Corner> = Vec::new();
137 let mut last: Option<usize> = None;
138 for k in KNEE_SIDE_MIN..=n - KNEE_SIDE_MIN {
139 let lo = k.saturating_sub(KNEE_LEFT);
140 let level = sums.mean(lo, k);
141 let hi = (k..(k + KNEE_RIGHT).min(n))
144 .find(|&j| amp[j] <= KNEE_FLOOR * level)
145 .unwrap_or((k + KNEE_RIGHT).min(n))
146 .max(k + KNEE_SIDE_MIN)
147 .min(n);
148 let (Some(left), Some(right), Some(one)) =
149 (sums.fit(lo, k), sums.fit(k, hi), sums.fit(lo, hi))
150 else {
151 continue;
152 };
153 if one.error <= 0.0 {
154 continue;
155 }
156 let gain = 1.0 - (left.error + right.error) / one.error;
157 let fall = right.at(k) - right.at(hi - 1);
158 if right.slope <= left.slope - KNEE_STEEPEN * level * HOP.get()
159 && fall >= KNEE_FALL * level
160 && gain >= KNEE_GAIN
161 {
162 let found = Corner {
163 first: k,
164 k,
165 gain,
166 left,
167 level,
168 };
169 match corners.last_mut() {
171 Some(corner) if last == Some(k - 1) => {
172 if gain > corner.gain {
173 *corner = Corner {
174 first: corner.first,
175 ..found
176 };
177 }
178 }
179 _ => corners.push(found),
180 }
181 last = Some(k);
182 }
183 }
184 let (first, later) = corners.split_first()?;
185 let db: Vec<f64> = amp
189 .iter()
190 .map(|v| 20.0 * v.max(KNEE_DB_FLOOR).log10())
191 .collect();
192 let mut best = first;
193 let alone = clear.min(n);
194 let mix = Sums::of(level);
195 for corner in later {
196 if alone > 0
199 && corner.k >= alone
200 && (sums.came_back(alone, best.k, corner.k) || mix.came_back(alone, best.k, corner.k))
201 {
202 continue;
203 }
204 if bend(&db, best.k, corner.first, KNEE_BEND_LAG) <= KNEE_BEND.get()
205 && rise(&[corner.k..alone.max(corner.k)]) <= KNEE_RISE * corner.level
206 {
207 best = corner;
208 }
209 }
210 let Corner { k, left, .. } = *best;
211 let held = left.at(k);
214 let reach = (k + KNEE_RIGHT).min(alone.max(k));
215 let leaves = (k..reach).rev().find(|&j| amp[j] >= KNEE_LEAVE * held);
216 if let Some(from) = leaves.filter(|&j| j > k + KNEE_SIDE_MIN) {
217 let to = (from..reach)
218 .find(|&j| amp[j] <= KNEE_FALL * held)
219 .unwrap_or(reach)
220 .max(from + KNEE_SIDE_MIN)
221 .min(n);
222 if let Some(line) = sums.fit(from, to).filter(|l| l.slope < 0.0) {
223 let back = (held - line.intercept) / line.slope;
224 if back > cast::usize_to_f64(k + KNEE_SIDE_MIN) {
225 return Some(start + back.min(cast::usize_to_f64(from)) * HOP.get());
226 }
227 }
228 }
229 let tangent = sums.fit(k, (k + KNEE_SIDE_MIN).min(n))?;
234 let crossing = (tangent.intercept - left.intercept) / (left.slope - tangent.slope);
235 let first = cast::usize_to_f64(k.saturating_sub(KNEE_LEFT));
238 let k = cast::usize_to_f64(k);
239 let x = if (first..=k + cast::usize_to_f64(KNEE_SIDE_MIN)).contains(&crossing) {
240 crossing
241 } else {
242 k
243 };
244 Some(start + x * HOP.get())
245}
246
247#[derive(Clone, Copy, Debug)]
249struct Corner {
250 first: usize,
251 k: usize,
252 gain: f64,
253 left: Line,
254 level: f64,
255}
256
257pub(super) fn bend(db: &[f64], from: usize, to: usize, lag: usize) -> f64 {
260 let (lo, hi) = (from + lag, to.saturating_sub(lag));
261 if hi <= lo {
262 return 0.0;
263 }
264 let mut sizes: Vec<f64> = (lo..hi)
265 .map(|i| (db[i - lag] - 2.0 * db[i] + db[i + lag]).abs())
266 .collect();
267 sizes.sort_by(f64::total_cmp);
268 sizes[(sizes.len() - 1) / 2]
269}
270
271fn rise(amp: &[f64]) -> f64 {
274 let head = amp.iter().take(KNEE_RISE_FRAMES).copied();
275 let means = amp
276 .windows(KNEE_RISE_FRAMES)
277 .map(|w| w.iter().sum::<f64>() / cast::usize_to_f64(KNEE_RISE_FRAMES));
278 climb(head).max(climb(means))
279}
280
281fn climb(values: impl Iterator<Item = f64>) -> f64 {
283 let mut low = f64::INFINITY;
284 let mut climb = 0.0_f64;
285 for v in values {
286 low = low.min(v);
287 climb = climb.max(v - low);
288 }
289 climb
290}
291
292#[derive(Clone, Copy, Debug)]
294struct Line {
295 slope: f64,
296 intercept: f64,
297 error: f64,
298}
299
300impl Line {
301 fn at(self, i: usize) -> f64 {
302 self.intercept + self.slope * cast::usize_to_f64(i)
303 }
304}
305
306struct Sums {
309 x: Vec<f64>,
310 y: Vec<f64>,
311 xx: Vec<f64>,
312 xy: Vec<f64>,
313 yy: Vec<f64>,
314}
315
316impl Sums {
317 fn of(values: &[f64]) -> Self {
318 let mut sums = Self {
319 x: vec![0.0],
320 y: vec![0.0],
321 xx: vec![0.0],
322 xy: vec![0.0],
323 yy: vec![0.0],
324 };
325 for (i, &y) in values.iter().enumerate() {
326 let x = cast::usize_to_f64(i);
327 for (v, add) in [
328 (&mut sums.x, x),
329 (&mut sums.y, y),
330 (&mut sums.xx, x * x),
331 (&mut sums.xy, x * y),
332 (&mut sums.yy, y * y),
333 ] {
334 let last = v.last().copied().unwrap_or(0.0);
335 v.push(last + add);
336 }
337 }
338 sums
339 }
340
341 fn came_back(&self, alone: usize, from: usize, k: usize) -> bool {
346 let stop = self.mean(alone.saturating_sub(KNEE_SIDE_MIN), alone);
347 let held = |k: usize| self.mean(k.saturating_sub(KNEE_LEFT), k);
348 stop < KNEE_FLOOR * held(from)
349 && held(k) >= KNEE_BACK * stop
350 && self
351 .fit(k.saturating_sub(KNEE_LEFT), k)
352 .is_some_and(|left| left.at(k) >= stop)
353 }
354
355 fn mean(&self, lo: usize, hi: usize) -> f64 {
357 if hi > lo {
358 (self.y[hi] - self.y[lo]) / cast::usize_to_f64(hi - lo)
359 } else {
360 0.0
361 }
362 }
363
364 fn fit(&self, lo: usize, hi: usize) -> Option<Line> {
366 if hi < lo + 2 {
367 return None;
368 }
369 let n = cast::usize_to_f64(hi - lo);
370 let d = |v: &[f64]| v[hi] - v[lo];
371 let (sx, sy, sxx, sxy, syy) = (
372 d(&self.x),
373 d(&self.y),
374 d(&self.xx),
375 d(&self.xy),
376 d(&self.yy),
377 );
378 let var = n * sxx - sx * sx;
379 if var <= 0.0 {
380 return None;
381 }
382 let slope = (n * sxy - sx * sy) / var;
383 let intercept = (sy - slope * sx) / n;
384 let error = (syy - 2.0 * intercept * sy - 2.0 * slope * sxy
386 + n * intercept * intercept
387 + 2.0 * intercept * slope * sx
388 + slope * slope * sxx)
389 .max(0.0);
390 Some(Line {
391 slope,
392 intercept,
393 error,
394 })
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 fn track(f: impl Fn(f64) -> f64, seconds: f64) -> Vec<f64> {
404 (0..cast::round_to_usize(seconds / HOP.get()).unwrap())
405 .map(|i| 10f64.powf(f(cast::usize_to_f64(i) * HOP.get()) / 20.0))
406 .collect()
407 }
408
409 fn adsr(sustain: f64, decay: f64, gap: f64, wobble: impl Fn(f64) -> f64) -> Vec<f64> {
412 let off = 0.1 + decay + gap;
413 track(
414 |t| {
415 let held = sustain * ((t - 0.1) / decay).clamp(0.0, 1.0);
416 let fall = 1.0 - ((t - off) / 0.05).clamp(0.0, 1.0);
417 held + 20.0 * fall.max(1e-4).log10() + wobble(t)
418 },
419 off + 0.1,
420 )
421 }
422
423 #[test]
424 fn a_sustain_shorter_than_its_decay_still_has_its_knee_at_the_note_off() {
425 let amp = adsr(-12.0, 0.2, 0.05, |_| 0.0);
427 let knee = find_knee(0.0, &, usize::MAX).unwrap();
428 assert!((knee - 0.355).abs() < 0.002, "{knee}");
429 }
430
431 #[test]
432 fn a_bend_over_the_limit_keeps_the_earlier_corner() {
433 let off = 0.1 + 0.1 + 0.3;
434 let bend_of = |size: f64, hz: f64| {
435 adsr(-18.0, 0.1, 0.3, move |t| {
436 if t > 0.2 && t < off {
437 size * (std::f64::consts::TAU * hz * t).sin()
438 } else {
439 0.0
440 }
441 })
442 };
443 let smooth = find_knee(0.0, &bend_of(0.3, 25.0), usize::MAX).unwrap();
444 assert!((smooth - off).abs() < 0.002, "{smooth}");
445 let wobbling = find_knee(0.0, &bend_of(3.0, 25.0), usize::MAX).unwrap();
446 assert!(wobbling < 0.2, "{wobbling}");
447 }
448
449 #[test]
450 fn the_rise_leaves_out_the_frames_that_read_past_the_track() {
451 let amp: Vec<f64> = (0..800_u32)
454 .map(|i| match i {
455 0..200 => 1.0,
456 200..300 => 1.0 - 0.75 * f64::from(i - 200) / 100.0,
457 300..500 => 0.25,
458 500..550 => 0.25 * (1.0 - f64::from(i - 500) / 50.0),
459 550..790 => 0.0,
460 _ => f64::from(i - 790) / 10.0,
461 })
462 .collect();
463 let at = |clear| find_knee(0.0, &, clear).unwrap();
464 assert!((at(790) - 0.5).abs() < 0.002, "{}", at(790));
465 assert!((at(800) - 0.2).abs() < 0.002, "{}", at(800));
466 }
467
468 fn gap_then_next(gap: f64, next: f64) -> Vec<f64> {
471 (0..900_u32)
472 .map(|i| match i {
473 0..300 => 1.0,
474 300..350 => gap + (1.0 - gap) * (1.0 - f64::from(i - 300) / 50.0),
475 350..600 => gap,
476 600..610 => gap + (next - gap) * f64::from(i - 600) / 10.0,
477 610..800 => next,
478 800..850 => next * (1.0 - f64::from(i - 800) / 50.0),
479 _ => 0.0,
480 })
481 .collect()
482 }
483
484 #[test]
485 fn a_note_that_ends_before_the_next_onset_keeps_its_note_off() {
486 let knee = find_knee(0.0, &gap_then_next(0.0, 1.0), 600).unwrap();
489 assert!((knee - 0.3).abs() < 0.002, "{knee}");
490 }
491
492 #[test]
493 fn a_later_corner_past_the_clear_frames_needs_a_level_of_a_fiftieth_at_their_end() {
494 let at = |gap: f64| find_knee(0.0, &gap_then_next(gap, 1.0), 600).unwrap();
495 assert!((at(0.019) - 0.3).abs() < 0.002, "{}", at(0.019));
496 assert!((at(0.021) - 0.8).abs() < 0.002, "{}", at(0.021));
497 }
498
499 #[test]
500 fn a_later_corner_past_the_clear_frames_needs_the_level_to_come_back_by_a_quarter() {
501 let at = |next: f64| find_knee(0.0, &gap_then_next(0.01, next), 600).unwrap();
503 assert!((at(0.012) - 0.8).abs() < 0.002, "{}", at(0.012));
504 assert!((at(0.013) - 0.3).abs() < 0.002, "{}", at(0.013));
505 }
506
507 #[test]
508 fn the_test_past_the_clear_frames_reads_the_given_level() {
509 let stripped = gap_then_next(0.021, 1.0);
512 let knee = find_knee_against(0.0, &stripped, &gap_then_next(0.019, 1.0), 600).unwrap();
513 assert!((knee - 0.3).abs() < 0.002, "{knee}");
514 let knee = find_knee(0.0, &stripped, 600).unwrap();
515 assert!((knee - 0.8).abs() < 0.002, "{knee}");
516 }
517
518 fn low_sustain(off: u32, level: f64, sustain: impl Fn(f64) -> f64) -> Vec<f64> {
521 (0..off + 200)
522 .map(|i| {
523 let x = f64::from(i);
524 if i < 100 {
525 1.0
526 } else if i < 200 {
527 level.powf((x - 100.0) / 100.0)
528 } else if i < off {
529 sustain(x)
530 } else {
531 (sustain(f64::from(off)) * (1.0 - (x - f64::from(off)) / 50.0)).max(0.0)
532 }
533 })
534 .collect()
535 }
536
537 #[test]
538 fn a_rippled_sustain_under_the_floor_keeps_its_note_off_past_the_clear_frames() {
539 for (depth, period, until) in [(0.01, 37.0, 700.0), (0.25, 10.0, 600.0)] {
543 for p in 0..40 {
544 let phase = std::f64::consts::TAU * f64::from(p) / 40.0;
545 let ripple = |x: f64| {
546 let swing = if x < until { depth } else { 0.0 };
547 0.01 * (1.0 + swing * (std::f64::consts::TAU * x / period + phase).sin())
548 };
549 let knee = find_knee(0.0, &low_sustain(700, 0.01, ripple), 500).unwrap();
550 assert!((knee - 0.7).abs() <= 0.015, "{depth} {p} {knee}");
551 }
552 }
553 }
554
555 #[test]
556 fn a_slowly_rising_sustain_under_the_floor_keeps_its_note_off_past_the_clear_frames() {
557 for level in [0.005, 0.008, 0.01, 0.013, 0.016, 0.019] {
558 let rising = move |x: f64| level + 1e-6 * (x - 200.0);
559 let knee = find_knee(0.0, &low_sustain(500, level, rising), 430).unwrap();
560 assert!((knee - 0.5).abs() < 0.005, "{level} {knee}");
561 }
562 }
563
564 #[test]
565 fn a_short_rise_into_a_note_off_past_the_clear_frames_is_no_next_note() {
566 let amp: Vec<f64> = (0..520_u32)
570 .map(|i| {
571 let x = f64::from(i);
572 match i {
573 0..100 => 1.0,
574 100..300 => 0.01_f64.powf((x - 100.0) / 200.0),
575 300..455 => 0.01,
576 455..465 => 0.01 + 0.07 * (x - 455.0) / 10.0,
577 465..475 => 0.08 * (1.0 - (x - 465.0) / 10.0),
578 _ => 0.0,
579 }
580 })
581 .collect();
582 let knee = find_knee(0.0, &, 427).unwrap();
583 assert!(knee > 0.45, "{knee}");
584 }
585
586 #[test]
587 fn a_note_that_falls_steadily_into_a_note_off_past_the_clear_frames_keeps_it() {
588 let amp: Vec<f64> = (0..477_u32)
592 .map(|i| {
593 let x = f64::from(i);
594 match i {
595 0..100 => 1.0,
596 100..340 => 0.022_f64.powf((x - 100.0) / 240.0),
597 340..440 => 0.022 - 0.014 * (x - 340.0) / 100.0,
598 440..450 => 0.008 * (1.0 - (x - 440.0) / 10.0),
599 _ => 0.0,
600 }
601 })
602 .collect();
603 let knee = find_knee(0.0, &, 427).unwrap();
604 assert!((knee - 0.44).abs() < 0.003, "{knee}");
605 }
606
607 #[test]
608 fn a_corner_at_the_last_clear_frame_counts_as_past_them() {
609 let amp: Vec<f64> = (0..600_u32)
612 .map(|i| match i {
613 0..300 => 1.0,
614 300..350 => 1.0 - f64::from(i - 300) / 50.0,
615 450..500 => 0.2 * (1.0 - f64::from(i - 450) / 50.0),
616 _ => 0.0,
617 })
618 .collect();
619 let knee = find_knee(0.0, &, 450).unwrap();
620 assert!((knee - 0.3).abs() < 0.002, "{knee}");
621 }
622
623 #[test]
624 fn the_stop_level_is_the_mean_over_the_last_10_clear_frames() {
625 let amp: Vec<f64> = (0..1000_u32)
630 .map(|i| {
631 let x = f64::from(i);
632 match i {
633 0..300 => 1.0,
634 300..350 => 0.002 + 0.998 * (1.0 - (x - 300.0) / 50.0),
635 590..595 => 0.014,
636 350..600 => 0.002,
637 600..610 => 0.002 + 0.0075 * (x - 600.0) / 10.0,
638 610..900 => 0.0095,
639 900..950 => 0.0095 * (1.0 - (x - 900.0) / 50.0),
640 _ => 0.0,
641 }
642 })
643 .collect();
644 let knee = find_knee(0.0, &, 600).unwrap();
645 assert!((knee - 0.9).abs() < 0.002, "{knee}");
646 }
647
648 #[test]
649 fn no_clear_frames_keep_the_rule_of_the_rise() {
650 let knee = find_knee(0.0, &two_corners(|_| 0.25, |_| 0.0), 0).unwrap();
652 assert!((knee - 0.5).abs() < 0.002, "{knee}");
653 }
654
655 #[test]
656 fn the_floor_is_read_against_the_corner_that_the_later_one_would_replace() {
657 let read = |gap: f64| {
660 let amp: Vec<f64> = (0..950_u32)
661 .map(|i| {
662 let x = f64::from(i);
663 match i {
664 0..200 | 710..850 => 1.0,
665 200..300 => 1.0 - 0.75 * (x - 200.0) / 100.0,
666 300..500 => 0.25,
667 500..550 => gap + (0.25 - gap) * (1.0 - (x - 500.0) / 50.0),
668 550..700 => gap,
669 700..710 => gap + (1.0 - gap) * (x - 700.0) / 10.0,
670 850..900 => 1.0 - (x - 850.0) / 50.0,
671 _ => 0.0,
672 }
673 })
674 .collect();
675 find_knee(0.0, &, 700).unwrap()
676 };
677 assert!((read(0.004) - 0.5).abs() < 0.002, "{}", read(0.004));
678 assert!((read(0.01) - 0.85).abs() < 0.002, "{}", read(0.01));
679 }
680
681 #[test]
682 fn the_rise_means_span_20_frames() {
683 let read = |period: u32, height: f64| {
687 let after = move |i: u32| {
688 if (i - 550) % period < period / 2 {
689 height
690 } else {
691 0.0
692 }
693 };
694 find_knee(0.0, &two_corners(|_| 0.25, after), usize::MAX).unwrap()
695 };
696 assert!(
698 (read(40, 0.0135) - 0.2).abs() < 0.002,
699 "{}",
700 read(40, 0.0135)
701 );
702 assert!(
703 (read(38, 0.013) - 0.49).abs() < 0.002,
704 "{}",
705 read(38, 0.013)
706 );
707 }
708
709 fn two_corners(shelf: impl Fn(u32) -> f64, after: impl Fn(u32) -> f64) -> Vec<f64> {
711 (0..800_u32)
712 .map(|i| match i {
713 0..200 => 1.0,
714 200..300 => 1.0 - 0.75 * f64::from(i - 200) / 100.0,
715 300..500 => shelf(i),
716 500..550 => 0.25 * (1.0 - f64::from(i - 500) / 50.0),
717 _ => after(i),
718 })
719 .collect()
720 }
721
722 #[test]
723 fn a_later_corner_needs_a_bend_of_at_most_0_4_db() {
724 let read = |peak_db: f64| {
727 let amp: Vec<f64> = (0..800_u32)
728 .map(|i| {
729 let phase = f64::from(i % 20) / 5.0;
730 let shape = if phase < 2.0 {
731 phase - 1.0
732 } else {
733 3.0 - phase
734 };
735 let body = -12.0 * (f64::from(i.max(200) - 200) / 100.0).min(1.0);
736 let level = 10f64.powf((body + peak_db * shape) / 20.0);
737 match i {
738 0..500 => level,
739 500..550 => 0.25 * (1.0 - f64::from(i - 500) / 50.0),
740 _ => 0.0,
741 }
742 })
743 .collect();
744 find_knee(0.0, &, usize::MAX).unwrap()
745 };
746 assert!((read(0.16) - 0.5).abs() < 0.01, "{}", read(0.16));
747 assert!((read(0.17) - 0.19).abs() < 0.01, "{}", read(0.17));
748 }
749
750 #[test]
751 fn a_later_corner_needs_a_rise_of_at_most_a_twentieth_of_the_level_before_it() {
752 let read = |share: f64| {
753 let back = |i: u32| 0.25 * share * (f64::from(i.saturating_sub(600)) / 50.0).min(1.0);
754 find_knee(0.0, &two_corners(|_| 0.25, back), usize::MAX).unwrap()
755 };
756 assert!((read(0.045) - 0.5).abs() < 0.002, "{}", read(0.045));
757 assert!((read(0.055) - 0.2).abs() < 0.002, "{}", read(0.055));
758 }
759
760 #[test]
761 fn the_fall_is_read_from_a_level_of_85_percent_of_the_held_one() {
762 let step: Vec<f64> = (0..600_u32)
764 .map(|i| match i {
765 0..300 => 1.0,
766 300..330 => 0.82,
767 330..430 => 0.82 * (1.0 - f64::from(i - 330) / 100.0),
768 _ => 0.0,
769 })
770 .collect();
771 let knee = find_knee(0.0, &step, usize::MAX).unwrap();
772 assert!((knee - 0.285).abs() < 0.003, "{knee}");
773 let points = [
776 (0.0, 1.0),
777 (312.0, 1.0),
778 (366.0, 0.89),
779 (397.0, 0.37),
780 (447.0, 0.0),
781 (647.0, 0.0),
782 ];
783 let amp: Vec<f64> = (0..647_u32)
784 .map(|i| {
785 let x = f64::from(i);
786 let w = points.windows(2).find(|w| x < w[1].0).unwrap();
787 w[0].1 + (w[1].1 - w[0].1) * (x - w[0].0) / (w[1].0 - w[0].0)
788 })
789 .collect();
790 let knee = find_knee(0.0, &, usize::MAX).unwrap();
791 assert!((knee - 0.362).abs() < 0.002, "{knee}");
792 }
793
794 #[test]
795 fn the_fall_read_stops_where_the_note_stops_being_alone() {
796 let amp: Vec<f64> = (0..600_u32)
798 .map(|i| match i {
799 0..300 | 400..450 => 1.0,
800 300..330 => 0.82,
801 330..400 => 0.82 * (1.0 - f64::from(i - 330) / 70.0),
802 450..500 => 1.0 - f64::from(i - 450) / 50.0,
803 _ => 0.0,
804 })
805 .collect();
806 let at = |clear| find_knee(0.0, &, clear).unwrap();
807 assert!((at(380) - 0.289).abs() < 0.003, "{}", at(380));
808 assert!(at(usize::MAX) > 0.3, "{}", at(usize::MAX));
809 }
810
811 #[test]
812 fn a_fall_read_stops_at_the_frame_it_starts_from() {
813 let points = [
816 (0.0, 1.0),
817 (99.0, 0.91),
818 (129.0, 0.91),
819 (173.0, 0.49),
820 (201.0, 0.49),
821 (239.0, 0.35),
822 (268.0, 0.004),
823 (283.0, 3.85),
824 (298.0, 3.85),
825 (328.0, 0.0),
826 (498.0, 0.0),
827 ];
828 let amp: Vec<f64> = (0..498_u32)
829 .map(|i| {
830 let x = f64::from(i);
831 let w = points.windows(2).find(|w| x < w[1].0).unwrap();
832 w[0].1 + (w[1].1 - w[0].1) * (x - w[0].0) / (w[1].0 - w[0].0)
833 })
834 .collect();
835 let knee = find_knee(0.0, &, usize::MAX).unwrap();
836 assert!((knee - 0.299).abs() < 1e-3, "{knee}");
837 }
838
839 #[test]
840 fn a_noise_floor_under_the_tail_does_not_hide_the_note_off() {
841 let amp: Vec<f64> = (0..800_u32)
844 .map(|i| match i {
845 0..200 => 1.0,
846 200..300 => 1.0 - 0.75 * f64::from(i - 200) / 100.0,
847 300..500 => 0.25,
848 500..550 => 0.25 - 0.235 * f64::from(i - 500) / 50.0,
849 _ => 0.015 + if i % 20 < 10 { 0.0 } else { 0.025 },
850 })
851 .collect();
852 let knee = find_knee(0.0, &, usize::MAX).unwrap();
853 assert!((knee - 0.5).abs() < 0.002, "{knee}");
854 let slow = |i: u32| if i % 40 < 20 { 0.0 } else { 0.05 };
856 let knee = find_knee(0.0, &two_corners(|_| 0.25, slow), usize::MAX).unwrap();
857 assert!((knee - 0.2).abs() < 0.01, "{knee}");
858 }
859
860 #[test]
861 fn a_later_corner_with_a_rise_after_it_keeps_the_earlier_one() {
862 let amp: Vec<f64> = (0..800_u32)
864 .map(|i| match i {
865 0..200 => 1.0,
866 200..300 => 1.0 - 0.75 * f64::from(i - 200) / 100.0,
867 300..500 => 0.25,
868 500..550 => 0.25 * (1.0 - f64::from(i - 500) / 50.0),
869 550..600 => 0.25 * 0.3 * f64::from(i - 550) / 50.0,
870 _ => 0.25 * 0.3,
871 })
872 .collect();
873 let knee = find_knee(0.0, &, usize::MAX).unwrap();
874 assert!((knee - 0.2).abs() < 0.002, "{knee}");
875 }
876
877 #[test]
878 fn the_fall_read_starts_from_the_level_at_the_split() {
879 let amp: Vec<f64> = (0..600_u32)
882 .map(|i| {
883 let x = f64::from(i);
884 match i {
885 0..300 => 0.6 + 0.4 * x / 300.0,
886 300..315 => 1.0 - 0.45 * (x - 300.0) / 15.0,
887 315..340 => 0.55 + 0.45 * (x - 315.0) / 25.0,
888 340..440 => 1.0 - (x - 340.0) / 100.0,
889 _ => 0.0,
890 }
891 })
892 .collect();
893 let knee = find_knee(0.0, &, usize::MAX).unwrap();
894 assert!((knee - 0.3445).abs() < 0.002, "{knee}");
895 }
896
897 #[test]
898 fn a_linear_release_has_its_knee_at_the_note_off() {
899 let amp: Vec<f64> = (0..590)
901 .map(|i| {
902 let t = cast::usize_to_f64(i) * HOP.get();
903 0.5 * (1.0 - (t - 0.4).max(0.0) / 0.2)
904 })
905 .collect();
906 let knee = find_knee(0.0, &, usize::MAX).unwrap();
907 assert!((knee - 0.4).abs() < 0.002, "{knee}");
908 }
909
910 #[test]
911 fn a_held_level_that_breaks_into_a_fall_has_its_knee_at_the_break() {
912 let db = track(
913 |t| {
914 if t < 0.4 {
915 -6.0
916 } else {
917 -6.0 - 200.0 * (t - 0.4)
918 }
919 },
920 0.5,
921 );
922 let knee = find_knee(1.0, &db, 0).unwrap();
923 assert!((knee - 1.4).abs() < 0.002, "{knee}");
924 }
925
926 #[test]
927 fn a_hold_that_breaks_into_a_linear_decay_is_not_the_note_off() {
928 let amp: Vec<f64> = (0..835)
931 .map(|i| {
932 let t = cast::usize_to_f64(i) * HOP.get();
933 let held = if t < 0.03 {
934 1.0
935 } else {
936 (1.0 - 0.937 * (t - 0.03) / 0.3).max(0.063)
937 };
938 held * (1.0 - (t - 0.8).max(0.0) / 0.02).max(0.0)
939 })
940 .collect();
941 let knee = find_knee(0.0, &, usize::MAX).unwrap();
942 assert!((knee - 0.8).abs() < 0.002, "{knee}");
943 }
944
945 #[test]
946 fn a_hold_that_breaks_into_a_curved_fall_has_its_knee_at_the_break() {
947 let amp: Vec<f64> = (0..650)
950 .map(|i| {
951 let t = cast::usize_to_f64(i.max(500) - 500);
952 1.0 - (t / 150.0).powf(1.1)
953 })
954 .collect();
955 let knee = find_knee(0.0, &, usize::MAX).unwrap();
956 assert!((knee - 0.5011).abs() < 5e-4, "{knee}");
957 }
958
959 #[test]
960 fn a_two_stage_release_reads_the_later_corner() {
961 let amp: Vec<f64> = (0..820)
965 .map(|i| {
966 let t = cast::usize_to_f64(i) * HOP.get();
967 let first = 1.0 - 0.5 * ((t - 0.4) / 0.05).clamp(0.0, 1.0);
968 first * (1.0 - ((t - 0.7) / 0.1).clamp(0.0, 1.0))
969 })
970 .collect();
971 let knee = find_knee(0.0, &, usize::MAX).unwrap();
972 assert!((knee - 0.7).abs() < 0.002, "{knee}");
973 }
974
975 #[test]
976 fn a_fall_at_one_rate_has_no_knee() {
977 let db = track(|t| -40.0 * t, 1.0);
978 assert_eq!(find_knee(0.0, &db, usize::MAX), None);
979 }
980
981 #[test]
982 fn a_slope_that_flattens_is_no_knee() {
983 let db = track(|t| if t < 0.3 { -20.0 * t / 0.3 } else { -20.0 }, 0.8);
985 assert_eq!(find_knee(0.0, &db, usize::MAX), None);
986 }
987
988 #[test]
989 fn the_knee_of_a_decay_sustain_release_is_the_release() {
990 let db = track(
991 |t| {
992 if t < 0.2 {
993 -30.0 * t
994 } else if t < 0.7 {
995 -6.0
996 } else {
997 -6.0 - 300.0 * (t - 0.7)
998 }
999 },
1000 0.8,
1001 );
1002 let knee = find_knee(0.0, &db, usize::MAX).unwrap();
1003 assert!((knee - 0.7).abs() < 0.002, "{knee}");
1004 }
1005
1006 #[test]
1007 fn a_crossing_before_the_search_starts_falls_back_to_the_split() {
1008 let amp: Vec<f64> = (0..100)
1012 .map(|i| match i {
1013 0..20 => 1.0,
1014 20..80 => 0.6 * (1.0 - cast::usize_to_f64(i - 20) / 60.0),
1015 _ => 0.0,
1016 })
1017 .collect();
1018 let knee = find_knee(1.0, &, 0).unwrap();
1019 assert!(
1020 (knee - (1.0 + 20.0 * HOP.get())).abs() < 1.5 * HOP.get(),
1021 "{knee}"
1022 );
1023 }
1024
1025 proptest::proptest! {
1026 #[test]
1027 fn a_knee_lies_inside_the_searched_frames(
1028 amp in proptest::collection::vec(0.0..1.0f64, 0..400),
1029 start in 0.0..10.0f64,
1030 ) {
1031 if let Some(knee) = find_knee(start, &, 0) {
1032 let end = start + cast::usize_to_f64(amp.len()) * HOP.get();
1033 proptest::prop_assert!(knee >= start && knee <= end, "{knee}");
1034 }
1035 }
1036 }
1037}