pub fn find_knee(start: f64, amp: &[f64], clear: usize) -> Option<f64>Expand description
The knee of a level track: the split where two lines fit best against one, at the crossing of the left line with the release’s first frames.
The left line spans up to KNEE_LEFT frames before the split, the
right one up to KNEE_RIGHT after it, and the crossing takes the line
over the first KNEE_SIDE_MIN frames after the split.
amp holds the linear amplitude, one value per HOP from start. Only its
first clear frames show this note alone, so the rise and the read of the fall
stop there. A later corner past them can still replace an earlier one, since a
note can be held past the onset of the next, unless the level has fallen under
KNEE_FLOOR of the earlier corner’s level over their last frames and comes back
before the later corner, by KNEE_BACK: that note has ended, and the later
corner belongs to the next note.
Every split leaves at least KNEE_SIDE_MIN frames on each side. A
split counts when the right line is steeper by KNEE_STEEPEN times
the left side’s mean per second, falls by KNEE_FALL times that
mean over its side, and the two lines leave at most 1 - KNEE_GAIN of
the squared error of one line. Splits that count in a row form a
corner, and within a corner the split with the largest gain wins. A later
corner replaces an earlier one, by KNEE_BEND and KNEE_RISE.
The lines are local, so a decay earlier in the note does not hide its
note-off. None when no split counts: a pluck that decays
at one rate has no note-off to see.
A level held at 1 for 0.5 s that then falls linearly to 0 over 0.2 s has its knee where the fall starts:
use sound_to_patch::estimate::envelope::find_knee;
let amp: Vec<f64> = (0..800)
.map(|i| (1.0 - (f64::from(i) - 500.0).max(0.0) / 200.0).max(0.0))
.collect();
let knee = find_knee(0.0, &, usize::MAX);
assert!(knee.is_some_and(|t| (t - 0.5).abs() < 0.002));