Skip to main content

sound_to_patch/notes/
cleanup.rs

1//! The cleanup of decoded notes: the octave check against the f0 track,
2//! the merge of octave ghosts into their notes, the split of notes at
3//! attacks inside them, and the join of split notes.
4//!
5//! The decoder adds a short copy an octave above a low note, and starts a
6//! second note at the same key when the onset activation peaks again
7//! inside a held note. A note an octave above the f0 track's reading, over
8//! a note of the lower key, is such a copy: it moves down, overlaps its
9//! true note and merges into it. The track alone moves nothing, because
10//! it can read a true note an octave low. A note that starts where an earlier
11//! one of the same key ends, on a weak onset and with no attack of its own
12//! in the stem, is the rest of that note. A note that holds an attack of
13//! its own in the stem holds a repeat the decoder missed, and splits there.
14
15use super::{MODEL_RATE, ModelRun, Note, NoteList, PitchTrack, QUIET_FRAME, StemLevels};
16use crate::cast;
17use crate::units::Hz;
18use crate::units::{Decibels, MidiPitch, Seconds, Semitones};
19
20/// How far a note may lie from the f0 track's median and still agree.
21pub const OCTAVE_AGREEMENT: Semitones = Semitones::from_const(1.0);
22/// How close to an octave a gap must lie to count as one.
23pub const OCTAVE_TOLERANCE: Semitones = Semitones::from_const(0.5);
24/// The f0 median, A2 (110 Hz), at or below which, within
25/// [`OCTAVE_TOLERANCE`], a note an octave above it moves down. The ghosts
26/// are measured there; above it a gap of an octave only sets the flag.
27pub const OCTAVE_FIX_CEILING: MidiPitch = MidiPitch::from_const(45.0);
28/// How far a note may reach past the note one octave below it and still
29/// lie inside it: two model frames.
30///
31/// The decoder's times sit up to 12 ms off its frame grid. A note moves
32/// down only inside such a note,
33/// because the track can read the lower octave of a true note, and a late
34/// end of the decoder makes a true note's neighbour overlap it.
35pub const COPY_SLACK: Seconds = Seconds::from_const(2.0 * 256.0 / 22_050.0);
36/// The fewest voiced frames, 17 ms of track, that check a note.
37pub const OCTAVE_MIN_FRAMES: usize = 3;
38/// The onset activation below which a note that starts where a note of
39/// the same key ends joins it.
40///
41/// On the generated set every piece of a held note starts at 0.66 or
42/// less. The weakest repeated note, a sixteenth at 175 BPM, starts at 0.70.
43pub const SPLIT_ONSET: f64 = 0.68;
44/// The widest gap a split may leave: two model frames.
45pub const SPLIT_GAP: Seconds = Seconds::from_const(2.0 * 256.0 / 22_050.0);
46/// The lowest key whose notes [`split_at_attacks`] splits: A1, 55 Hz.
47///
48/// Below A1 the envelope window of one period lasts 19 ms or more, as long
49/// as the recovery of a fast sidechain duck. On the generated sets ducks to
50/// -40 dB that recover in 30 to 45 ms split on every key from D1 to G#1,
51/// and a duck at E1 reads the same envelope and onset activation as a gate
52/// with a 30 ms gap and a 5 ms tail. Full-depth sine tremolos of 10 Hz
53/// split at A0 and B0. From A1 up no duck to -40 or -48 dB that recovers
54/// in 25 to 45 ms splits.
55pub const SPLIT_LOWEST: MidiPitch = MidiPitch::from_const(33.0);
56/// How far before a note's onset the quiet frames of its attack lie:
57/// frames centred from 75 ms before the onset to the onset, and not
58/// before the earlier note starts.
59///
60/// The decoder starts a 55 Hz note up to 37 ms after its attack, so the
61/// gap before the attack lies further back.
62pub const ATTACK_BEFORE: Seconds = Seconds::from_const(0.075);
63/// How far after a note's onset its loudest frame may lie: frames centred
64/// from the onset to 50 ms after it, and not past its end. The attack
65/// split's piece after a cut lasts at least this long.
66pub const ATTACK_AFTER: Seconds = Seconds::from_const(0.05);
67/// How far the stem's level, with any DC offset removed, must dip below
68/// the loudest frame in the quiet frames of an attack.
69///
70/// On the generated sets 17 to 19 dB give the same notes on every stem
71/// but square tremolos: at 20 dB eighths on E1 and A1 with a 40 ms tail
72/// keep fewer repeats, and at 21 dB a 55 Hz repeat after a 30 ms gap with
73/// a 5 ms tail joins.
74pub const ATTACK_DIP: Decibels = Decibels::from_const(19.0);
75/// How far the level at the note's fundamental alone must dip across a cut
76/// of the attack split.
77///
78/// Below about A2 the harmonics' window cannot tell a note from a key
79/// nearby: a held A1 20 dB under a louder gated A2 dips there as the gate
80/// closes. Its fundamental does not, and the split cuts it no further than
81/// the split join alone. The bin of [`StemLevels::fundamental_level`]
82/// rejects the note's harmonics alone, fully only where a period is a
83/// whole number of samples. From about A6 up a harmonic leaks within
84/// 23 dB at the worst phase.
85///
86/// On the generated sets 21 to 25 dB give the same notes, and 23 dB lies
87/// in the middle. At 19 and 20 dB squares loud for 60 to 80 % of the
88/// cycle split a held note further in 72 runs where 23 dB gives 52, and
89/// at 30 dB gated runs hit 8 fewer gates.
90pub const FUNDAMENTAL_DIP: Decibels = Decibels::from_const(23.0);
91/// How far the level at the note's own harmonics must dip in the same
92/// frames.
93///
94/// The tail of a note under the attack of another key dips 18.1 dB or
95/// less. On the generated sets 18, 19, 22, 24 and 25 dB differ only on
96/// square tremolos and on eighths on A1 with a 40 ms tail: 18 and 19 dB
97/// keep 10 of their 16 repeats, 22 dB 6, and 24 and 25 dB 5.
98pub const HARMONIC_DIP: Decibels = Decibels::from_const(22.0);
99/// The shortest envelope window of the rise test; it spans one period of
100/// the note when that is longer.
101///
102/// One period reads a periodic tone steady. Two periods smooth the dip of
103/// a tremolo at A1 away, and a window between the two ripples.
104pub const RISE_WINDOW: Seconds = Seconds::from_const(0.004);
105/// The step between envelope readings of the rise test.
106pub const RISE_STEP: Seconds = Seconds::from_const(0.001);
107/// The share of the loudest envelope reading a rise starts below, and the
108/// share it ends above.
109pub const RISE_SPAN: (f64, f64) = (0.1, 0.9);
110/// How far after the onset a rise may start: the decoder starts a note up
111/// to 15 ms before its attack.
112pub const RISE_LATEST: Seconds = Seconds::from_const(0.02);
113/// How many rise limits past [`RISE_LATEST`] the rise test reads.
114///
115/// A tremolo that rises from a tenth to nine tenths within the limit peaks
116/// about 1.7 limits after its trough, and the loudest reading must hold
117/// that peak: with readings that end before it, a slow rise to the level
118/// at their end reads fast. On the generated sets 2.5 to 4 limits give
119/// the same notes.
120pub const RISE_READ: f64 = 3.0;
121/// How much longer than a step read through the envelope window a rise
122/// may take: about the attack of a gate.
123///
124/// A full-depth tremolo to silence at `r` Hz rises in about 0.3 / `r` s.
125/// On the first generated set 6 to 8 ms give the same held notes; at 9 ms
126/// a duck to -40 dB on E1 splits, and each ms less keeps fewer repeats
127/// with a slow attack. On the second, 8 ms splits a duck to -40 dB on A1.
128pub const RISE_SLACK: Seconds = Seconds::from_const(0.007);
129/// How long the stem must hold above the first share of [`HOLD_SPAN`]
130/// before it last fell below the second share ahead of a rise.
131///
132/// Below A1 the window of one period is as long as the rise of a tremolo
133/// of 14 to 20 Hz, so the rise alone cannot tell the two apart. A gated
134/// note holds its level until its gap, and a fast tremolo never holds. On
135/// the generated sets 50 to 70 ms keep every repeat and split no sine
136/// tremolo; at 45 ms a tremolo of 14 Hz on E1 splits, and at 80 ms
137/// sixteenths on E1 with attacks of 19 and 23 ms join.
138pub const HOLD_BEFORE: Seconds = Seconds::from_const(0.06);
139/// The share of the loudest reading the hold must stay above, and the
140/// share at which it ends.
141///
142/// The level ripples by a few hundredths at the fall of a gate, so a hold
143/// that ended at the share it must stay above could end on a ripple.
144pub const HOLD_SPAN: (f64, f64) = (0.5, 0.6);
145/// How far back from the start of a rise the hold may end.
146///
147/// A sine tremolo that reads as fast runs faster than about 8 Hz, and at
148/// 8 Hz and full depth it stays below six tenths of its peak for about
149/// 70 ms. A square tremolo off half its cycle stays low for 167 ms at
150/// 3 Hz, and the search must reach its fall for [`HOLD_RATIO`] to weigh
151/// it. A longer dip is no tremolo's trough, and the bound keeps the
152/// search from reading a whole quiet passage before a weak repeat. On the
153/// generated sets 200 to 500 ms give the same notes; at 150 ms an E1
154/// under a 3 Hz square off half its cycle splits into 12 notes where the
155/// onset rule gives 7.
156pub const HOLD_SEARCH: Seconds = Seconds::from_const(0.4);
157/// How many times longer than its dip the stem must hold before it.
158///
159/// A gated note holds longer than its gap, and a tremolo holds about as
160/// long as it dips: a square tremolo off half its cycle holds exactly as
161/// long. On the generated sets 1.5 splits no square tremolo off half its
162/// cycle. At 1.25 a square at 9.1 Hz on C2 splits a held note into 10
163/// where the onset rule gives 5. At 2 eighths on E1 with a 10 ms attack
164/// and a 10 ms tail over a -40 dB floor keep 3 of 16 repeats in the bass
165/// profile, as the onset rule does, where 1.5 keeps 12. The cost of 1.5:
166/// a gap longer than about two thirds of the note before it joins, and
167/// A1 sixteenths with 44 ms gaps and a 10 ms attack keep 3 of 32 repeats,
168/// as the onset rule does, where a hold of 60 ms alone kept 7.
169pub const HOLD_RATIO: f64 = 1.5;
170
171/// What the octave check did.
172#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
173pub struct OctaveCheck {
174    /// Notes moved down an octave.
175    pub moved: usize,
176    /// Notes that disagree with the track and keep their pitch, flagged.
177    pub flagged: usize,
178    /// Notes with too few voiced frames to check.
179    pub unchecked: usize,
180}
181
182/// Checks every note against the median of the voiced f0 frames inside
183/// it, and returns the notes with their octave flag set.
184///
185/// A note within [`OCTAVE_AGREEMENT`] of the median is fine. A note an
186/// octave above it, within [`OCTAVE_TOLERANCE`], moves down 12 semitones
187/// when the median lies within [`OCTAVE_TOLERANCE`] of
188/// [`OCTAVE_FIX_CEILING`] or below, and a note of that lower key with a
189/// higher velocity holds it within [`COPY_SLACK`] at each end. Any other gap keeps
190/// the pitch and sets the flag. A note with fewer than
191/// [`OCTAVE_MIN_FRAMES`] voiced frames keeps its flag unset.
192#[must_use]
193pub fn check_octaves(notes: &NoteList, track: &PitchTrack) -> (NoteList, OctaveCheck) {
194    let mut check = OctaveCheck::default();
195    let checked = notes
196        .notes()
197        .iter()
198        .map(|note| {
199            let Some(median) = median_pitch(track, note) else {
200                check.unchecked += 1;
201                return note.clone();
202            };
203            let gap = note.pitch().get() - median;
204            if gap.abs() <= OCTAVE_AGREEMENT.get() {
205                return note.clone().with_octave_uncertain(false);
206            }
207            if (gap - 12.0).abs() <= OCTAVE_TOLERANCE.get()
208                && median <= OCTAVE_FIX_CEILING.get() + OCTAVE_TOLERANCE.get()
209                && let Some(lower) = lower_octave(notes, note)
210                && let Some(moved) = moved_down(note, lower)
211            {
212                check.moved += 1;
213                return moved;
214            }
215            check.flagged += 1;
216            note.clone().with_octave_uncertain(true)
217        })
218        .collect();
219    (NoteList::new(checked), check)
220}
221
222/// The note one octave below `note` that holds it, within [`COPY_SLACK`]
223/// at each end, with a higher velocity: the model heard the lower note
224/// around this one, and more surely. The decoder gives a note an octave
225/// twin with nearly its span, so only the velocity, the mean activation,
226/// tells the copy from the true note.
227fn lower_octave<'a>(notes: &'a NoteList, note: &Note) -> Option<&'a Note> {
228    let slack = COPY_SLACK.get();
229    notes.notes().iter().find(|other| {
230        (note.pitch().get() - other.pitch().get() - 12.0).abs() < 0.5
231            && other.onset().get() <= note.onset().get() + slack
232            && note.offset().get() <= other.offset().get() + slack
233            && note
234                .velocity()
235                .zip(other.velocity())
236                .is_some_and(|(copy, lower)| copy < lower)
237    })
238}
239
240/// The median pitch of the voiced frames inside a note, if enough are
241/// voiced.
242fn median_pitch(track: &PitchTrack, note: &Note) -> Option<f64> {
243    let mut pitches: Vec<f64> = track
244        .voiced_between(note.onset(), note.offset())
245        .filter_map(|(_, f0)| MidiPitch::try_from(f0).ok())
246        .map(MidiPitch::get)
247        .collect();
248    if pitches.len() < OCTAVE_MIN_FRAMES {
249        return None;
250    }
251    pitches.sort_by(f64::total_cmp);
252    let middle = pitches.len() / 2;
253    Some(if pitches.len().is_multiple_of(2) {
254        f64::midpoint(pitches[middle - 1], pitches[middle])
255    } else {
256        pitches[middle]
257    })
258}
259
260/// The copy moved onto its lower note: that note's key and span, so it
261/// merges into that note alone and never bridges two repeats. Its
262/// velocity stays and its flag is cleared.
263fn moved_down(note: &Note, lower: &Note) -> Option<Note> {
264    let mut moved = Note::new(lower.onset(), lower.offset(), lower.pitch())
265        .ok()?
266        .with_octave_uncertain(false);
267    if let Some(velocity) = note.velocity() {
268        moved = moved.with_velocity(velocity);
269    }
270    Some(moved)
271}
272
273/// Merges every pair of notes of one key that sound at the same time into
274/// one note from the earlier onset to the later offset.
275///
276/// A ghost moved down by [`check_octaves`] lands on its true note and
277/// disappears into it. The decoder itself never gives two notes of one
278/// key that overlap, so a repeat it decodes never merges. The merged note
279/// keeps the onset, pitch and octave flag of the note that starts first,
280/// and the higher velocity. It has no contour and no overlap flag:
281/// [`PitchTrack::with_contours`] and [`NoteList::with_overlaps`] fill
282/// those after the cleanup.
283#[must_use]
284pub fn merge_overlaps(notes: &NoteList) -> NoteList {
285    let mut merged: Vec<Note> = Vec::with_capacity(notes.notes().len());
286    for note in notes.notes() {
287        let same_key = merged
288            .iter_mut()
289            .rev()
290            .find(|kept| same_key(kept, note) && note.onset() < kept.offset());
291        match same_key {
292            Some(kept) => *kept = joined(kept, note),
293            None => merged.push(note.clone()),
294        }
295    }
296    NoteList::new(merged)
297}
298
299/// Joins the pieces of a held note that the decoder split, on the onset
300/// rule alone: [`join_splits_with`] with a stem of silence, which never
301/// gives an attack.
302#[must_use]
303pub fn join_splits(notes: &NoteList, run: &ModelRun) -> NoteList {
304    join_splits_with(notes, run, &StemLevels::new(&[], MODEL_RATE))
305}
306
307/// Joins the pieces of a held note that the decoder split.
308///
309/// A note joins a note of the same key that ends within [`SPLIT_GAP`]
310/// before it, on an onset activation below [`SPLIT_ONSET`], when its
311/// velocity is no higher than that note's and `levels` show no attack of
312/// its own.
313///
314/// A repeated note starts on a strong onset or on an attack in the stem,
315/// and stays apart. The decoder can fill the gap before a low
316/// repeat and read its onset weak, so the onset alone misses it. A split
317/// piece is the weaker rest of a held note; a weaker piece before a
318/// stronger note is an early fragment of the attack, and joining it would
319/// move the onset.
320#[must_use]
321pub fn join_splits_with(notes: &NoteList, run: &ModelRun, levels: &StemLevels<'_>) -> NoteList {
322    let mut joined_notes: Vec<Note> = Vec::with_capacity(notes.notes().len());
323    for note in notes.notes() {
324        let weak = run
325            .onset_strength(note.onset(), note.pitch())
326            .is_some_and(|strength| strength < SPLIT_ONSET);
327        let earlier = joined_notes.iter_mut().rev().find(|kept| {
328            same_key(kept, note)
329                && kept.offset() <= note.onset()
330                && note.onset().get() - kept.offset().get() <= SPLIT_GAP.get()
331                && note.velocity() <= kept.velocity()
332        });
333        match earlier {
334            Some(kept) if weak && !attacks(levels, kept, note) => {
335                *kept = joined(kept, note);
336            }
337            _ => joined_notes.push(note.clone()),
338        }
339    }
340    NoteList::new(joined_notes)
341}
342
343/// Splits each note where the stem has an attack of its own inside it.
344///
345/// The decoder can read a low repeat with a short gap as part of one long
346/// note, so no later step sees two notes. A frame inside a note marks a
347/// dip when it is no louder than the frame before it, quieter than the
348/// frame after it, and [`ATTACK_DIP`] or more below the loudest frame in
349/// the [`ATTACK_AFTER`] after it. The split falls on the last envelope
350/// reading at or below the first share of [`RISE_SPAN`] of the loudest
351/// reading in that span, before the first reading at the second share
352/// after the quietest of the dip frame's readings, and the note's
353/// fundamental must dip by [`FUNDAMENTAL_DIP`] across it. It holds when the piece from
354/// there has an attack of its own after the piece before it, by the rule
355/// of the split join, which then keeps the two apart. Each piece keeps the
356/// note's key, octave flag and velocity.
357///
358/// A last piece of at most twice [`ATTACK_AFTER`] that runs into the
359/// decoder's next note of its key, within [`SPLIT_GAP`], starts that note
360/// instead, unless that note has an attack of its own after the piece.
361/// The decoder can start a note 58 ms after its attack, which leaves such
362/// a piece. A repeat after a gap of 20 ms or less can show no attack by
363/// the rule of the split join, so the limit keeps a longer piece before
364/// one a note of its own. A gated repeat of 60 to 90 ms after such a gap
365/// still loses its onset to the piece before it.
366#[must_use]
367pub fn split_at_attacks(notes: &NoteList, levels: &StemLevels<'_>) -> NoteList {
368    let decoded = notes.notes();
369    let mut starts: Vec<Option<Seconds>> = vec![None; decoded.len()];
370    let mut pieces: Vec<Note> = Vec::with_capacity(decoded.len());
371    for (index, found) in decoded.iter().enumerate() {
372        let note = starts[index]
373            .and_then(|onset| piece(found, onset, found.offset()))
374            .unwrap_or_else(|| found.clone());
375        if note.pitch() < SPLIT_LOWEST {
376            pieces.push(note);
377            continue;
378        }
379        let frames = levels.frames_between(note.onset(), note.offset());
380        let reach = levels.frames_between(
381            note.onset(),
382            Seconds::clamped(note.offset().get() + ATTACK_AFTER.get()),
383        );
384        // Each frame's level once: the loudest earlier frame is kept as the
385        // frames advance, which keeps a long note linear in its frames.
386        let first = frames.start.saturating_sub(1);
387        let cached: Vec<f64> = (first..=reach.end).map(|m| levels.ac_rms(m)).collect();
388        let level = |m: usize| cached[m - first];
389        let mut rest = note.clone();
390        let (mut seen, mut before) = (frames.start, 0.0_f64);
391        for frame in frames {
392            let hold_end = Seconds::clamped(levels.frame_time(frame).get() - HOLD_BEFORE.get());
393            while seen < levels.frames_between(rest.onset(), hold_end).end {
394                before = before.max(level(seen));
395                seen += 1;
396            }
397            let Some(onset) = split_point(levels, frame, note.pitch(), &level, before)
398                .filter(|onset| rest.offset().get() - onset.get() >= ATTACK_AFTER.get())
399            else {
400                continue;
401            };
402            if let (Some(earlier), Some(after)) = (
403                piece(&rest, rest.onset(), onset),
404                piece(&rest, onset, rest.offset()),
405            ) && attacks(levels, &earlier, &after)
406            {
407                pieces.push(earlier);
408                rest = after;
409                seen = levels.frames_between(rest.onset(), rest.offset()).start;
410                before = 0.0;
411            }
412        }
413        let next = (rest.onset() > note.onset())
414            .then(|| {
415                decoded[index + 1..]
416                    .iter()
417                    .position(|later| same_key(later, &rest) && later.onset() >= rest.offset())
418            })
419            .flatten()
420            .map(|offset| index + 1 + offset)
421            .filter(|&later| {
422                rest.offset().get() - rest.onset().get() <= 2.0 * ATTACK_AFTER.get()
423                    && decoded[later].onset().get() - rest.offset().get() <= SPLIT_GAP.get()
424                    && !attacks(levels, &rest, &decoded[later])
425            });
426        match next {
427            Some(later) => starts[later] = Some(rest.onset()),
428            None => pieces.push(rest),
429        }
430    }
431    NoteList::new(pieces)
432}
433
434/// Where a dip at `frame` ends, by the rule of [`split_at_attacks`] for a
435/// note of `pitch`, or `None` when the frame marks no dip. `level` reads a
436/// frame's level, and `before` is the loudest frame of the note centred
437/// [`HOLD_BEFORE`] or more before this one.
438fn split_point(
439    levels: &StemLevels<'_>,
440    frame: usize,
441    pitch: MidiPitch,
442    level: &dyn Fn(usize) -> f64,
443    before: f64,
444) -> Option<Seconds> {
445    let centre = levels.frame_time(frame);
446    let after = Seconds::clamped(centre.get() + ATTACK_AFTER.get());
447    let loud = levels
448        .frames_between(centre, after)
449        .map(level)
450        .fold(0.0, f64::max);
451    // The dip test repeats the attack rule's, and keeps the readings to
452    // frames that can split.
453    if frame == 0
454        || level(frame - 1) < level(frame)
455        || level(frame + 1) <= level(frame)
456        || 20.0 * (loud / level(frame)).log10() < ATTACK_DIP.get()
457        || before < HOLD_SPAN.1 * loud
458    {
459        return None;
460    }
461    let (from, to) = RISE_SPAN;
462    let step = RISE_STEP.get();
463    let start = centre.get() - QUIET_FRAME.get() / 2.0;
464    let last = cast::floor_to_usize((after.get() - start) / step)?;
465    let readings = readings(
466        levels,
467        (0..=last).map(|k| start + cast::usize_to_f64(k) * step),
468        rise_window(pitch),
469    );
470    let peak = peak_from(&readings, centre.get());
471    // The climb starts at the quietest of the dip frame's 26 readings, from
472    // its start to its end: the note before the dip can read louder than
473    // the one after it, and a deeper gap later in the window belongs to a
474    // dip of its own. A count, not a time, so float steps cannot move it.
475    let dip_readings = cast::round_to_usize(QUIET_FRAME.get() / step)? + 1;
476    let trough = readings
477        .iter()
478        .take(dip_readings)
479        .enumerate()
480        .min_by(|a, b| a.1.1.total_cmp(&b.1.1))
481        .map(|(k, _)| k)?;
482    let rise = trough
483        + readings[trough..]
484            .iter()
485            .position(|&(_, level)| level >= to * peak)?;
486    let onset = readings[..rise]
487        .iter()
488        .rev()
489        .find(|&&(_, level)| level <= from * peak)
490        .map(|&(time, _)| time)?;
491    fundamental_dips(levels, &readings, onset, pitch).then(|| Seconds::clamped(onset))
492}
493
494/// Whether the level at the fundamental of `pitch`, at the times of
495/// `readings`, dips by [`FUNDAMENTAL_DIP`] from the loudest reading from
496/// `onset` on to the quietest before it.
497fn fundamental_dips(
498    levels: &StemLevels<'_>,
499    readings: &[(f64, f64)],
500    onset: f64,
501    pitch: MidiPitch,
502) -> bool {
503    let level = |time: f64| levels.fundamental_level(Seconds::clamped(time), pitch);
504    let (low, high) = readings
505        .iter()
506        .fold((f64::INFINITY, 0.0_f64), |(low, high), &(time, _)| {
507            if time < onset {
508                (low.min(level(time)), high)
509            } else {
510                (low, high.max(level(time)))
511            }
512        });
513    20.0 * (high / low).log10() >= FUNDAMENTAL_DIP.get()
514}
515
516/// The envelope window of the rise test for a note of `pitch`: one period,
517/// and at least [`RISE_WINDOW`].
518fn rise_window(pitch: MidiPitch) -> f64 {
519    (1.0 / Hz::from(pitch).get()).max(RISE_WINDOW.get())
520}
521
522/// The envelope through `window` at each of `times`, with its time.
523fn readings(
524    levels: &StemLevels<'_>,
525    times: impl Iterator<Item = f64>,
526    window: f64,
527) -> Vec<(f64, f64)> {
528    times
529        .map(|time| {
530            (
531                time,
532                levels.envelope(Seconds::clamped(time), Seconds::clamped(window)),
533            )
534        })
535        .collect()
536}
537
538/// The loudest of `readings` at `time` or later.
539fn peak_from(readings: &[(f64, f64)], time: f64) -> f64 {
540    readings
541        .iter()
542        .filter(|(at, _)| *at >= time)
543        .map(|&(_, level)| level)
544        .fold(0.0, f64::max)
545}
546
547/// `note` cut to the span from `onset` to `offset`, with its key, octave
548/// flag and velocity, or `None` when the span is empty.
549fn piece(note: &Note, onset: Seconds, offset: Seconds) -> Option<Note> {
550    let mut piece = Note::new(onset, offset, note.pitch()).ok()?;
551    if let Some(flag) = note.octave_uncertain() {
552        piece = piece.with_octave_uncertain(flag);
553    }
554    if let Some(velocity) = note.velocity() {
555        piece = piece.with_velocity(velocity);
556    }
557    Some(piece)
558}
559
560/// Whether `note` starts on an attack of its own after `kept`.
561///
562/// The quiet frames are those centred from [`ATTACK_BEFORE`] the onset to
563/// the onset, and not before `kept` starts; the loud frames those centred
564/// from the onset to [`ATTACK_AFTER`] it, and not past the note's end. An
565/// attack needs the stem to dip by [`ATTACK_DIP`] and the note's harmonics
566/// by [`HARMONIC_DIP`] from the loudest frame to the quietest, and no slow
567/// rise. The stem's dip keeps the pieces of a held note under a line of
568/// other keys together, the harmonics' dip a tail under the attack of
569/// another key, and the rise test a tremolo.
570fn attacks(levels: &StemLevels<'_>, kept: &Note, note: &Note) -> bool {
571    let start =
572        Seconds::clamped((note.onset().get() - ATTACK_BEFORE.get()).max(kept.onset().get()));
573    let end = Seconds::clamped((note.onset().get() + ATTACK_AFTER.get()).min(note.offset().get()));
574    let quiet = levels.frames_between(start, note.onset());
575    let loud = levels.frames_between(note.onset(), end);
576    let dips = |level: &dyn Fn(usize) -> f64, dip: Decibels| {
577        let low = quiet.clone().map(level).fold(f64::INFINITY, f64::min);
578        let high = loud.clone().map(level).fold(0.0, f64::max);
579        // A dip from silence is endless; silence on both sides is none.
580        20.0 * (high / low).log10() >= dip.get()
581    };
582    dips(&|m| levels.ac_rms(m), ATTACK_DIP)
583        && dips(&|m| levels.harmonic_level(m, note.pitch()), HARMONIC_DIP)
584        && !rises_slowly(levels, note, start)
585}
586
587/// Whether the stem's envelope around `note`, read every [`RISE_STEP`]
588/// from `start` to [`RISE_READ`] limits past [`RISE_LATEST`] after the
589/// onset, rises slowly: from below the first share of [`RISE_SPAN`] of its
590/// loudest reading after the onset, at the latest [`RISE_LATEST`] after
591/// the onset, to above the second share, its fastest rise takes longer
592/// than the limit, a step read through the window plus [`RISE_SLACK`].
593///
594/// The window spans one period of the note, at least [`RISE_WINDOW`], so a
595/// periodic tone reads steady. The RMS of a step reads in the window as the
596/// square root of the share of the window past it, which climbs from the
597/// first share to the second over the difference of their squares, 0.8 of
598/// the window. An envelope that never falls below the first share rises
599/// from no silence, and counts as slow. A rise counts only when the stem
600/// [`held`] before its dip.
601fn rises_slowly(levels: &StemLevels<'_>, note: &Note, start: Seconds) -> bool {
602    let onset = note.onset().get();
603    let window = rise_window(note.pitch());
604    let (from, to) = RISE_SPAN;
605    let limit = (to * to - from * from) * window + RISE_SLACK.get();
606    let end = onset + RISE_LATEST.get() + RISE_READ * limit;
607    let step = RISE_STEP.get();
608    let first = ((start.get() - onset) / step).ceil();
609    let Some(last) = cast::floor_to_usize((end - onset) / step - first) else {
610        return true;
611    };
612    let readings = readings(
613        levels,
614        (0..=last).map(|k| onset + (first + cast::usize_to_f64(k)) * step),
615        window,
616    );
617    let peak = peak_from(&readings, onset);
618    let mut low = None;
619    let mut fastest: Option<f64> = None;
620    for &(time, level) in &readings {
621        if level <= from * peak && time <= onset + RISE_LATEST.get() {
622            low = Some(time);
623        }
624        if let Some(since) = low
625            && level >= to * peak
626        {
627            let rose = readings
628                .iter()
629                .find(|&&(after, level)| after > since && level >= HOLD_SPAN.1 * peak)
630                .map_or(time, |&(after, _)| after);
631            if held(levels, since, rose, peak, window) {
632                fastest = Some(fastest.map_or(time - since, |rise| rise.min(time - since)));
633            }
634            low = None;
635        }
636    }
637    fastest.is_none_or(|rise| rise > limit)
638}
639
640/// Whether the envelope, read every [`RISE_STEP`], held before the dip
641/// that ends at `low`. The dip runs from the last reading at or above the
642/// second share of [`HOLD_SPAN`] of `peak`, at most [`HOLD_SEARCH`] before
643/// `low`, to `rose`, the first reading back above it. Before the dip the
644/// envelope must stay above the first share for [`HOLD_BEFORE`] and for
645/// [`HOLD_RATIO`] times the dip. A dip that lasts the whole search is too
646/// long for a tremolo, and holds. The level before the stem starts is
647/// unknown, so a dip or a hold that reaches the start is too short.
648fn held(levels: &StemLevels<'_>, low: f64, rose: f64, peak: f64, window: f64) -> bool {
649    let (stay, end) = HOLD_SPAN;
650    let step = RISE_STEP.get();
651    let level = |time: f64| levels.envelope(Seconds::clamped(time), Seconds::clamped(window));
652    let back = |from: f64| (0..).map(move |k| from - cast::usize_to_f64(k) * step);
653    let Some(fell) = back(low)
654        .take_while(|&time| time >= 0.0 && time >= low - HOLD_SEARCH.get())
655        .find(|&time| level(time) >= end * peak)
656    else {
657        return low >= HOLD_SEARCH.get();
658    };
659    let hold = HOLD_BEFORE.get().max(HOLD_RATIO * (rose - fell));
660    fell >= hold
661        && back(fell)
662            .take_while(|&time| time > fell - hold)
663            .all(|time| level(time) >= stay * peak)
664}
665
666fn same_key(a: &Note, b: &Note) -> bool {
667    (a.pitch().get() - b.pitch().get()).abs() < 0.5
668}
669
670/// `first` stretched to the later offset of the two, with the higher
671/// velocity.
672fn joined(first: &Note, second: &Note) -> Note {
673    let offset = if second.offset() > first.offset() {
674        second.offset()
675    } else {
676        first.offset()
677    };
678    let Some(note) = piece(first, first.onset(), offset) else {
679        return first.clone();
680    };
681    match first.velocity().max(second.velocity()) {
682        Some(velocity) => note.with_velocity(velocity),
683        None => note,
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690    use crate::audio::SampleRate;
691    use crate::notes::{TrackFrame, Velocity};
692    use crate::units::{Decibels, Hz};
693
694    fn s(value: f64) -> Seconds {
695        Seconds::new(value).unwrap()
696    }
697
698    fn note(onset: f64, offset: f64, pitch: f64) -> Note {
699        Note::new(s(onset), s(offset), MidiPitch::new(pitch).unwrap()).unwrap()
700    }
701
702    /// A note with the decoder's velocity, its mean activation.
703    fn heard(onset: f64, offset: f64, pitch: f64, velocity: u8) -> Note {
704        note(onset, offset, pitch).with_velocity(Velocity::new(velocity).unwrap())
705    }
706
707    /// A track voiced at `pitch` from 0 to 2 s, every 5 ms.
708    fn track_at(pitch: f64) -> PitchTrack {
709        PitchTrack::from_frames(
710            (0..400)
711                .map(|i| TrackFrame {
712                    time: s(f64::from(i) * 0.005),
713                    f0: Some(Hz::from(MidiPitch::new(pitch).unwrap())),
714                    level: Decibels::new(-3.0).unwrap(),
715                })
716                .collect(),
717        )
718    }
719
720    fn pitches(list: &NoteList) -> Vec<f64> {
721        list.notes().iter().map(|n| n.pitch().get()).collect()
722    }
723
724    #[test]
725    fn a_note_an_octave_above_the_track_moves_down() {
726        let notes = NoteList::new(vec![heard(0.5, 1.0, 45.0, 90), heard(0.5, 0.6, 57.0, 37)]);
727        let (checked, check) = check_octaves(&notes, &track_at(45.0));
728        assert_eq!(pitches(&checked), vec![45.0, 45.0]);
729        assert_eq!(
730            check,
731            OctaveCheck {
732                moved: 1,
733                flagged: 0,
734                unchecked: 0
735            }
736        );
737        assert!(
738            checked
739                .notes()
740                .iter()
741                .all(|n| n.octave_uncertain() == Some(false))
742        );
743        // The copy lands on its note and merges into it.
744        let merged = merge_overlaps(&checked);
745        assert_eq!(
746            merged.notes(),
747            &[heard(0.5, 1.0, 45.0, 90).with_octave_uncertain(false)]
748        );
749    }
750
751    /// The decoder gives every note of an octave bass a twin an octave
752    /// away with nearly its span; the true note is the stronger. Figures
753    /// from the 16th-note octave part of the evaluation.
754    #[test]
755    fn an_octave_bass_keeps_its_true_upper_notes() {
756        let notes = NoteList::new(vec![
757            heard(0.755, 0.859, 45.0, 94),
758            heard(0.755, 0.871, 57.0, 46),
759            heard(0.859, 0.998, 45.0, 48),
760            heard(0.871, 0.998, 57.0, 100),
761        ]);
762        let (checked, check) = check_octaves(&notes, &track_at(45.0));
763        assert_eq!(check.moved, 1);
764        let merged = merge_overlaps(&checked);
765        let kept: Vec<(f64, f64)> = merged
766            .notes()
767            .iter()
768            .map(|n| (n.onset().get(), n.pitch().get()))
769            .collect();
770        // The weak A2 twin of the true A3 stays for the manual pass.
771        assert_eq!(kept, vec![(0.755, 45.0), (0.859, 45.0), (0.871, 57.0)]);
772    }
773
774    #[test]
775    fn a_copy_moves_only_inside_its_lower_note() {
776        let moved = |lower: Note| {
777            let notes = NoteList::new(vec![lower, heard(0.5, 0.6, 57.0, 37)]);
778            check_octaves(&notes, &track_at(45.0)).1.moved
779        };
780        let slack = COPY_SLACK.get();
781        assert_eq!(moved(heard(0.5 + slack, 0.6 - slack, 45.0, 90)), 1);
782        assert_eq!(moved(heard(0.5 + slack + 1e-3, 1.0, 45.0, 90)), 0);
783        assert_eq!(moved(heard(0.4, 0.6 - slack - 1e-3, 45.0, 90)), 0);
784        // A weaker lower note, or one without a velocity, moves nothing.
785        assert_eq!(moved(heard(0.5, 1.0, 45.0, 37)), 0);
786        assert_eq!(moved(note(0.5, 1.0, 45.0)), 0);
787    }
788
789    #[test]
790    fn a_lone_note_an_octave_above_the_track_keeps_its_pitch() {
791        let notes = NoteList::new(vec![note(0.5, 1.0, 57.0), note(0.8, 1.2, 45.0)]);
792        let (checked, check) = check_octaves(&notes, &track_at(45.0));
793        assert_eq!(pitches(&checked), vec![57.0, 45.0]);
794        assert_eq!(checked.notes()[0].octave_uncertain(), Some(true));
795        assert_eq!((check.moved, check.flagged), (0, 1));
796    }
797
798    #[test]
799    fn a_note_three_semitones_off_keeps_its_pitch_and_the_flag() {
800        let notes = NoteList::new(vec![note(0.5, 1.0, 48.0)]);
801        let (checked, check) = check_octaves(&notes, &track_at(45.0));
802        assert_eq!(pitches(&checked), vec![48.0]);
803        assert_eq!(checked.notes()[0].octave_uncertain(), Some(true));
804        assert_eq!(check.flagged, 1);
805    }
806
807    #[test]
808    fn the_gap_rules_hold_at_their_bounds() {
809        // The note sounds over its lower octave, which starts earlier, so
810        // the note under test sorts last.
811        let flag = |pitch: f64, track: f64| {
812            let notes = NoteList::new(vec![
813                heard(0.45, 1.0, pitch - 12.0, 90),
814                heard(0.5, 1.0, pitch, 40),
815            ]);
816            let (checked, _) = check_octaves(&notes, &track_at(track));
817            let last = checked.notes().last().unwrap();
818            (last.pitch().get(), last.octave_uncertain())
819        };
820        // Agreement within one semitone.
821        assert_eq!(flag(46.0, 45.0), (46.0, Some(false)));
822        assert_eq!(flag(46.0, 44.9), (46.0, Some(true)));
823        // An octave within half a semitone, at or below the ceiling.
824        assert_eq!(flag(57.0, 45.5), (45.0, Some(false)));
825        assert_eq!(flag(57.0, 44.4), (57.0, Some(true)));
826        // Above the ceiling an octave only flags.
827        assert_eq!(flag(58.0, 46.0), (58.0, Some(true)));
828        // A note an octave below the track keeps its pitch.
829        assert_eq!(flag(33.0, 45.0), (33.0, Some(true)));
830    }
831
832    #[test]
833    fn an_even_count_of_frames_takes_the_middle_two() {
834        // Frames at 44, 44, 46 and 46 have the median 45, an octave below
835        // the copy; either middle frame alone would not be.
836        let track = PitchTrack::from_frames(
837            [44.0, 44.0, 46.0, 46.0]
838                .iter()
839                .enumerate()
840                .map(|(i, &pitch)| TrackFrame {
841                    time: s(0.5 + 0.005 * f64::from(u32::try_from(i).unwrap())),
842                    f0: Some(Hz::from(MidiPitch::new(pitch).unwrap())),
843                    level: Decibels::new(-3.0).unwrap(),
844                })
845                .collect(),
846        );
847        let notes = NoteList::new(vec![heard(0.49, 1.0, 45.0, 90), heard(0.5, 0.6, 57.0, 37)]);
848        assert_eq!(check_octaves(&notes, &track).1.moved, 1);
849    }
850
851    #[test]
852    fn a_note_with_too_few_voiced_frames_stays_unchecked() {
853        for (frames, unchecked) in [(2, 1), (3, 0)] {
854            let track = PitchTrack::from_frames(
855                (0..frames)
856                    .map(|i| TrackFrame {
857                        time: s(0.5 + f64::from(i) * 0.005),
858                        f0: Some(Hz::from(MidiPitch::new(45.0).unwrap())),
859                        level: Decibels::new(-3.0).unwrap(),
860                    })
861                    .collect(),
862            );
863            let notes = NoteList::new(vec![note(0.5, 1.0, 57.0)]);
864            let (checked, check) = check_octaves(&notes, &track);
865            assert_eq!(check.unchecked, unchecked, "{frames} frames");
866            assert_eq!(
867                checked.notes()[0].octave_uncertain().is_none(),
868                unchecked == 1
869            );
870        }
871    }
872
873    /// A model run of 80 frames with an onset of `strength` at A2 on
874    /// frame 30, and the time of a frame.
875    fn run_with_onset(strength: f32) -> (ModelRun, impl Fn(usize) -> f64) {
876        run_with_onset_at(strength, 30)
877    }
878
879    /// A model run with an onset of `strength` at A2 on `frame`, 50 frames
880    /// before its end, and the time of a frame.
881    fn run_with_onset_at(strength: f32, frame: usize) -> (ModelRun, impl Fn(usize) -> f64) {
882        use basic_pitch::{Posteriorgram, Posteriorgrams};
883        let (frames, bin) = (frame + 50, 45 - 21);
884        let mut onsets = Posteriorgram::zeros(frames, 88);
885        onsets.as_mut_slice()[frame * 88 + bin] = strength;
886        let grams = Posteriorgrams::new(
887            Posteriorgram::zeros(frames, 88),
888            onsets,
889            Posteriorgram::zeros(frames, 264),
890        )
891        .unwrap();
892        (
893            ModelRun::from_posteriorgrams(grams),
894            basic_pitch::notes::frame_to_time,
895        )
896    }
897
898    /// The ghost merge relies on this: a held key with a second onset
899    /// decodes as two notes that touch and do not overlap.
900    #[test]
901    fn the_decoder_gives_a_repeat_of_one_key_no_overlap() {
902        use basic_pitch::{Posteriorgram, Posteriorgrams};
903        let (frames, bin) = (80, 45 - 21);
904        let mut held = Posteriorgram::zeros(frames, 88);
905        let mut onsets = Posteriorgram::zeros(frames, 88);
906        for frame in 10..60 {
907            held.as_mut_slice()[frame * 88 + bin] = 0.8;
908        }
909        for frame in [10, 30] {
910            onsets.as_mut_slice()[frame * 88 + bin] = 0.9;
911        }
912        let run = ModelRun::from_posteriorgrams(
913            Posteriorgrams::new(held, onsets, Posteriorgram::zeros(frames, 264)).unwrap(),
914        );
915        let settings = crate::notes::Profile::Bass.settings(crate::units::Bpm::new(120.0).unwrap());
916        let decoded = run.decode(&settings).unwrap();
917        let a2: Vec<&Note> = decoded
918            .notes()
919            .iter()
920            .filter(|decoded| (decoded.pitch().get() - 45.0).abs() < 0.5)
921            .collect();
922        assert_eq!(a2.len(), 2);
923        assert!(a2[0].offset() <= a2[1].onset());
924        assert_eq!(
925            merge_overlaps(&decoded).notes().len(),
926            decoded.notes().len()
927        );
928    }
929
930    #[test]
931    fn the_onset_strength_reads_one_frame_either_side() {
932        let (run, time) = run_with_onset(0.9);
933        let a2 = MidiPitch::new(45.0).unwrap();
934        let at = |t| run.onset_strength(s(t), a2).unwrap();
935        assert!((at(time(30) + 0.01) - 0.9).abs() < 1e-6);
936        assert!(at(time(33)).abs() < 1e-6);
937        assert_eq!(
938            run.onset_strength(s(time(30)), MidiPitch::new(120.0).unwrap()),
939            None
940        );
941    }
942
943    #[test]
944    fn a_split_on_a_weak_onset_joins_and_a_repeat_stays() {
945        for (strength, expected) in [(0.6, 1), (0.67, 1), (0.69, 2), (0.9, 2)] {
946            let (run, time) = run_with_onset(strength);
947            let notes = NoteList::new(vec![
948                note(time(10), time(30), 45.0),
949                note(time(30), time(45), 45.0),
950            ]);
951            let joined = join_splits(&notes, &run);
952            assert_eq!(joined.notes().len(), expected, "onset {strength}");
953            if expected == 1 {
954                assert_eq!(joined.notes()[0], note(time(10), time(45), 45.0));
955            }
956        }
957    }
958
959    #[test]
960    fn a_split_joins_within_its_gap_and_only_as_a_tail() {
961        let (run, time) = run_with_onset(0.0);
962        let gap = SPLIT_GAP.get();
963        let count = |first: (f64, f64), second: (f64, f64)| {
964            let notes = NoteList::new(vec![
965                note(first.0, first.1, 45.0),
966                note(second.0, second.1, 45.0),
967            ]);
968            join_splits(&notes, &run).notes().len()
969        };
970        let (start, end) = (time(30), time(40));
971        assert_eq!(count((0.1, start - gap), (start, end)), 1);
972        assert_eq!(count((0.1, start - gap - 1e-3), (start, end)), 2);
973        // Two model frames are 23.2 ms.
974        assert_eq!(count((0.1, start - 0.023), (start, end)), 1);
975        assert_eq!(count((0.1, start - 0.024), (start, end)), 2);
976    }
977
978    #[test]
979    fn a_weaker_piece_never_takes_a_stronger_note() {
980        let (run, time) = run_with_onset(0.0);
981        let joined = |first: u8, second: u8| {
982            let notes = NoteList::new(vec![
983                heard(time(10), time(30), 45.0, first),
984                heard(time(30), time(60), 45.0, second),
985            ]);
986            join_splits(&notes, &run).notes().len()
987        };
988        assert_eq!(joined(90, 50), 1);
989        assert_eq!(joined(90, 90), 1);
990        // An early fragment of the attack before the true note: joining it
991        // would move the onset.
992        assert_eq!(joined(40, 90), 2);
993    }
994
995    fn rate() -> SampleRate {
996        SampleRate::new(22_050).unwrap()
997    }
998
999    /// The model frames of [`LEAD_SAMPLES`].
1000    const LEAD_FRAMES: usize = 137;
1001
1002    /// The samples of [`stem_at`] before its time 0: more than
1003    /// [`HOLD_SEARCH`], and a whole number of model frames and of level hops
1004    /// of 137 samples, so both read the stem as without them.
1005    const LEAD_SAMPLES: i32 = 35_072;
1006
1007    /// A stem at 22,050 Hz from [`LEAD_SAMPLES`] before time 0 to 1 s after
1008    /// it: a sine at `f0` and a 1500 Hz sine with amplitudes `level(t)`,
1009    /// `t` in seconds from `onset`.
1010    fn stem_at(f0: f64, onset: f64, level: impl Fn(f64) -> (f64, f64)) -> Vec<f64> {
1011        (-LEAD_SAMPLES..22_050)
1012            .map(|index| {
1013                let t = f64::from(index) / 22_050.0;
1014                let (note, high) = level(t - onset);
1015                note * (std::f64::consts::TAU * f0 * t).sin()
1016                    + high * (std::f64::consts::TAU * 1500.0 * t).sin()
1017            })
1018            .collect()
1019    }
1020
1021    /// [`stem_at`] an A2, whose first eight harmonics lie far from 1500 Hz.
1022    fn stem(onset: f64, level: impl Fn(f64) -> (f64, f64)) -> Vec<f64> {
1023        stem_at(110.0, onset, level)
1024    }
1025
1026    /// The notes of `pitch` after the join of two touching pieces that meet
1027    /// at [`onset`], on a weak onset, over a stem from [`stem_at`].
1028    fn joined_at(pitch: f64, mono: &[f64]) -> usize {
1029        let (run, time) = run_with_onset_at(0.0, LEAD_FRAMES + 30);
1030        let notes = NoteList::new(vec![
1031            note(time(LEAD_FRAMES + 10), time(LEAD_FRAMES + 30), pitch),
1032            note(time(LEAD_FRAMES + 30), time(LEAD_FRAMES + 45), pitch),
1033        ]);
1034        join_splits_with(&notes, &run, &StemLevels::new(mono, rate()))
1035            .notes()
1036            .len()
1037    }
1038
1039    fn joined_a2(mono: &[f64]) -> usize {
1040        joined_at(45.0, mono)
1041    }
1042
1043    fn onset() -> f64 {
1044        basic_pitch::notes::frame_to_time(30)
1045    }
1046
1047    #[test]
1048    fn a_repeat_after_a_gap_of_silence_stays_apart() {
1049        let gap = stem(onset(), |t| {
1050            (if (-0.05..0.0).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1051        });
1052        assert_eq!(joined_a2(&gap), 2);
1053        assert_eq!(joined_a2(&stem(onset(), |_| (0.5, 0.0))), 1);
1054        // Silence on both sides is no attack.
1055        assert_eq!(joined_a2(&vec![0.0; 22_050]), 1);
1056    }
1057
1058    #[test]
1059    fn the_stem_dips_by_attack_dip() {
1060        // 10 ms of silence before the onset gives the rise its start. The
1061        // 1500 Hz tone before them holds the stem's quietest frame 19.5 or
1062        // 18.5 dB below its loudest, while the harmonics dip past 40 dB.
1063        for (high, expected) in [(0.07, 2), (0.079, 1)] {
1064            let mono = stem(onset(), |t| {
1065                if t >= 0.0 {
1066                    (0.5, high)
1067                } else if t > -0.01 {
1068                    (0.0, 0.0)
1069                } else {
1070                    (0.001, high)
1071                }
1072            });
1073            assert_eq!(joined_a2(&mono), expected, "1500 Hz at {high}");
1074        }
1075    }
1076
1077    #[test]
1078    fn the_harmonics_dip_by_harmonic_dip() {
1079        // The A2 harmonics dip 23.5 or 20.5 dB; the 1500 Hz tone after the
1080        // onset lifts the stem's dip past 30 dB.
1081        for (low, expected) in [(0.0334, 2), (0.0472, 1)] {
1082            let mono = stem(onset(), |t| if t < 0.0 { (low, 0.0) } else { (0.5, 0.5) });
1083            assert_eq!(joined_a2(&mono), expected, "A2 at {low} before");
1084        }
1085    }
1086
1087    #[test]
1088    fn a_dc_offset_counts_for_nothing() {
1089        let gap = |offset: f64| {
1090            stem(onset(), |t| {
1091                (if (-0.05..0.0).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1092            })
1093            .into_iter()
1094            .map(|x| x + offset)
1095            .collect::<Vec<_>>()
1096        };
1097        assert_eq!(joined_a2(&gap(0.2)), 2);
1098    }
1099
1100    #[test]
1101    fn a_slow_rise_is_no_attack() {
1102        // At A2 the envelope window spans 9.1 ms, which a step climbs in
1103        // 7.3 ms, so a rise may take 14.3 ms: a linear attack of 13 ms
1104        // stays apart, one of 14 ms joins.
1105        for (ramp, expected) in [(0.013, 2), (0.014, 1)] {
1106            let mono = stem(onset(), |t| (0.5 * (t / ramp).clamp(0.0, 1.0), 0.0));
1107            assert_eq!(joined_a2(&mono), expected, "ramp {ramp} s");
1108        }
1109    }
1110
1111    #[test]
1112    fn a_high_note_reads_through_rise_window() {
1113        // One period of A5 spans 1.1 ms; the 4 ms window lets a linear
1114        // attack of 10 ms stay apart, and one of 11 ms joins.
1115        for (ramp, expected) in [(0.01, 2), (0.011, 1)] {
1116            let mono = stem_at(880.0, onset(), |t| (0.5 * (t / ramp).clamp(0.0, 1.0), 0.0));
1117            assert_eq!(joined_at(81.0, &mono), expected, "ramp {ramp} s");
1118        }
1119    }
1120
1121    #[test]
1122    fn a_rise_starts_by_rise_latest() {
1123        // Silence from 50 ms before the onset to 30 ms after it: the rise
1124        // from 20 ms after the onset is fast. To 35 ms after it, the rise
1125        // from there is slow, and the rise from later silence is too late.
1126        for (end, expected) in [(0.03, 2), (0.035, 1)] {
1127            let mono = stem(onset(), |t| {
1128                (if (-0.05..end).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1129            });
1130            assert_eq!(joined_a2(&mono), expected, "silence to {end} s");
1131        }
1132    }
1133
1134    #[test]
1135    fn a_low_note_reads_past_attack_after() {
1136        // At B0 the window spans 32.4 ms and a rise may take 32.9 ms. With
1137        // silence to 46 ms after the onset, the rise from 20 ms is fast. To
1138        // 54 ms, it is slow, but only readings past 50 ms reach its peak.
1139        for (end, expected) in [(0.046, 2), (0.054, 1)] {
1140            let mono = stem_at(30.868, onset(), |t| {
1141                (if (-0.05..end).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1142            });
1143            assert_eq!(joined_at(23.0, &mono), expected, "silence to {end} s");
1144        }
1145    }
1146
1147    /// The notes after the join over `mono` from [`stem_at`] without its
1148    /// lead, which starts the stem 345 ms before [`onset`].
1149    fn joined_without_lead(mono: &[f64]) -> usize {
1150        let (run, time) = run_with_onset(0.0);
1151        let notes = NoteList::new(vec![
1152            note(time(10), time(30), 45.0),
1153            note(time(30), time(45), 45.0),
1154        ]);
1155        let lead = usize::try_from(LEAD_SAMPLES).unwrap();
1156        join_splits_with(&notes, &run, &StemLevels::new(&mono[lead..], rate()))
1157            .notes()
1158            .len()
1159    }
1160
1161    /// A note of `hold` s from silence, `gap` s of silence and the onset.
1162    fn note_before_gap(hold: f64, gap: f64) -> Vec<f64> {
1163        stem(onset(), |t| {
1164            let on = (-gap - hold..-gap).contains(&t) || t >= 0.0;
1165            (if on { 0.5 } else { 0.0 }, 0.0)
1166        })
1167    }
1168
1169    #[test]
1170    fn a_rise_needs_a_hold_before_its_gap() {
1171        // Before a 30 ms gap the dip below six tenths of the peak lasts
1172        // 32 ms, and HOLD_RATIO times that stays under HOLD_BEFORE. Through
1173        // the 9.1 ms window of A2 a note of 59 ms holds above half the peak
1174        // for HOLD_BEFORE and the repeat stays apart; one of 58 ms joins.
1175        for (hold, expected) in [(0.059, 2), (0.058, 1)] {
1176            assert_eq!(
1177                joined_a2(&note_before_gap(hold, 0.03)),
1178                expected,
1179                "held {hold} s"
1180            );
1181        }
1182    }
1183
1184    #[test]
1185    fn a_hold_lasts_hold_ratio_times_the_dip() {
1186        // Before a 100 ms gap the dip lasts 100 ms, so the note must hold
1187        // for 150 ms. A note of 147 ms holds above half the peak for 151 ms
1188        // and the repeat stays apart; one of 146 ms holds for 148 ms and
1189        // joins.
1190        for (hold, expected) in [(0.147, 2), (0.146, 1)] {
1191            assert_eq!(
1192                joined_a2(&note_before_gap(hold, 0.1)),
1193                expected,
1194                "held {hold} s"
1195            );
1196        }
1197    }
1198
1199    #[test]
1200    fn a_hold_cut_by_the_stem_start_is_too_short() {
1201        // Before a 150 ms gap the note must hold for 225 ms. Without the
1202        // lead the stem starts 195 ms before the gap.
1203        let mono = stem(onset(), |t| {
1204            (if t < -0.15 || t >= 0.0 { 0.5 } else { 0.0 }, 0.0)
1205        });
1206        assert_eq!(joined_a2(&mono), 2);
1207        assert_eq!(joined_without_lead(&mono), 1);
1208    }
1209
1210    #[test]
1211    fn a_dip_cut_by_the_stem_start_is_no_gap() {
1212        // Silence from the stem start to the onset: with the lead it lasts
1213        // longer than HOLD_SEARCH, without it 345 ms.
1214        let mono = stem(onset(), |t| (if t < 0.0 { 0.0 } else { 0.5 }, 0.0));
1215        assert_eq!(joined_a2(&mono), 2);
1216        assert_eq!(joined_without_lead(&mono), 1);
1217    }
1218
1219    #[test]
1220    fn the_rise_reads_rise_read_limits_past_rise_latest() {
1221        // After a 50 ms gap, a step to 0.4 and a second step to 1. At A2 a
1222        // rise may take 14.3 ms, and the readings run to 62.9 ms. A second
1223        // step at 65 ms reads inside them, so the climb from silence to
1224        // its peak is slow and the note joins; at 66 ms the peak lies past
1225        // them and the step to 0.4 reads fast.
1226        for (late, expected) in [(0.065, 1), (0.066, 2)] {
1227            let mono = stem(onset(), |t| {
1228                let level = if t < -0.05 {
1229                    0.5
1230                } else if t < 0.0 {
1231                    0.0
1232                } else if t < late {
1233                    0.4
1234                } else {
1235                    1.0
1236                };
1237                (level, 0.0)
1238            });
1239            assert_eq!(joined_a2(&mono), expected, "step to 1 at {late} s");
1240        }
1241    }
1242
1243    #[test]
1244    fn the_hold_stays_above_the_first_share_of_hold_span() {
1245        // A note that ramps up over 40 ms and then holds, before a 50 ms
1246        // gap that asks for a hold of 75 ms. The ramp lies above half the
1247        // peak for its last 20 ms, so a hold of 55 ms after it lasts long
1248        // enough and one of 54 ms does not. Above four tenths the ramp
1249        // would add 4 ms.
1250        for (hold, expected) in [(0.055, 2), (0.054, 1)] {
1251            let mono = stem(onset(), |t| {
1252                let start = -0.09 - hold;
1253                let level = if t >= 0.0 {
1254                    0.5
1255                } else if t >= -0.05 {
1256                    0.0
1257                } else {
1258                    0.5 * ((t - start) / 0.04).clamp(0.0, 1.0)
1259                };
1260                (level, 0.0)
1261            });
1262            assert_eq!(joined_a2(&mono), expected, "held {hold} s");
1263        }
1264    }
1265
1266    #[test]
1267    fn the_hold_ends_within_hold_search() {
1268        // A 20 ms burst over a floor below six tenths of the peak, before a
1269        // 50 ms gap. Ending 356 ms before the gap, the burst's fall lies
1270        // within HOLD_SEARCH of the rise, whose low lies a few ms before
1271        // the onset: the burst holds too briefly and the note joins.
1272        // Ending 357 ms before it, the fall lies past the search, and the
1273        // long dip holds.
1274        for (before, expected) in [(0.356, 1), (0.357, 2)] {
1275            let mono = stem(onset(), |t| {
1276                let level = if t >= 0.0 {
1277                    0.5
1278                } else if t >= -0.05 {
1279                    0.0
1280                } else if (-0.07 - before..-0.05 - before).contains(&t) {
1281                    0.5
1282                } else {
1283                    0.2
1284                };
1285                (level, 0.0)
1286            });
1287            assert_eq!(joined_a2(&mono), expected, "burst {before} s before");
1288        }
1289    }
1290
1291    #[test]
1292    fn the_fastest_rise_counts() {
1293        // A held note, a 25 ms gap and a step 35 ms before the onset, then
1294        // 15 ms of silence and a slow climb over 45 ms from the onset.
1295        let mono = stem(onset(), |t| {
1296            let level = if t < -0.06 {
1297                0.4
1298            } else if t < -0.035 {
1299                0.0
1300            } else if t < -0.015 {
1301                0.4
1302            } else if t < 0.0 {
1303                0.0
1304            } else {
1305                0.4 * (t / 0.045).min(1.0)
1306            };
1307            (level, 0.0)
1308        });
1309        assert_eq!(joined_a2(&mono), 2);
1310    }
1311
1312    #[test]
1313    fn the_fastest_held_rise_counts() {
1314        // At A5 a rise may take 10.2 ms. A held note, 30 ms of silence and
1315        // a slow ramp over 20 ms from 60 ms before the onset, then a 6 ms
1316        // dip 13 ms after it and a step back. The ramp holds 63 ms before
1317        // the dip, so both rises count, and the step is the faster.
1318        let mono = stem_at(880.0, onset(), |t| {
1319            let level = if t < -0.09 {
1320                0.5
1321            } else if t < -0.06 {
1322                0.0
1323            } else if t < -0.04 {
1324                25.0 * (t + 0.06)
1325            } else if (0.013..0.019).contains(&t) {
1326                0.0
1327            } else {
1328                0.5
1329            };
1330            (level, 0.0)
1331        });
1332        assert_eq!(joined_at(81.0, &mono), 2);
1333    }
1334
1335    #[test]
1336    fn the_loudest_frame_lies_within_attack_after() {
1337        // A step to 0.4 at the onset climbs half the way to a step to 1 at
1338        // 100 ms, but not to one at 40 ms.
1339        for (late, expected) in [(0.04, 1), (0.1, 2)] {
1340            let mono = stem(onset(), |t| {
1341                let level = if t < 0.0 {
1342                    0.0
1343                } else if t < late {
1344                    0.4
1345                } else {
1346                    1.0
1347                };
1348                (level, 0.0)
1349            });
1350            assert_eq!(joined_a2(&mono), expected, "step to 1 at {late} s");
1351        }
1352    }
1353
1354    #[test]
1355    fn the_quiet_frames_lie_within_attack_before() {
1356        // 30 ms of silence ending 35 ms before the onset, or 80 ms before.
1357        for (end, expected) in [(0.035, 2), (0.08, 1)] {
1358            let mono = stem(onset(), |t| {
1359                (
1360                    if (-end - 0.03..-end).contains(&t) {
1361                        0.0
1362                    } else {
1363                        0.5
1364                    },
1365                    0.0,
1366                )
1367            });
1368            assert_eq!(joined_a2(&mono), expected, "gap ending {end} s before");
1369        }
1370    }
1371
1372    #[test]
1373    fn a_note_at_either_end_of_the_stem_reads_only_frames_inside_it() {
1374        let (run, _) = run_with_onset(0.0);
1375        let mono = &stem(0.0, |_| (0.5, 0.0))[..11_025];
1376        let levels = StemLevels::new(mono, rate());
1377        for (first, second) in [((0.0, 0.01), (0.01, 0.2)), ((0.3, 0.497), (0.497, 0.6))] {
1378            let notes = NoteList::new(vec![
1379                note(first.0, first.1, 45.0),
1380                note(second.0, second.1, 45.0),
1381            ]);
1382            assert_eq!(join_splits_with(&notes, &run, &levels).notes().len(), 1);
1383        }
1384    }
1385
1386    #[test]
1387    fn the_quiet_before_the_held_note_never_counts() {
1388        let (run, time) = run_with_onset_at(0.0, LEAD_FRAMES + 30);
1389        // The held note starts from silence; its split piece 46 ms later
1390        // sees only frames from that start.
1391        let mono = stem(time(26), |t| (if t < 0.0 { 0.0 } else { 0.5 }, 0.0));
1392        let notes = NoteList::new(vec![
1393            note(time(LEAD_FRAMES + 26), time(LEAD_FRAMES + 30), 45.0),
1394            note(time(LEAD_FRAMES + 30), time(LEAD_FRAMES + 45), 45.0),
1395        ]);
1396        assert_eq!(
1397            join_splits_with(&notes, &run, &StemLevels::new(&mono, rate()))
1398                .notes()
1399                .len(),
1400            1
1401        );
1402    }
1403
1404    /// The onsets, in s from [`onset`], of the pieces of one note of
1405    /// `pitch` from `from` s to `to` s around it, split over `mono` from
1406    /// [`stem_at`].
1407    fn split_at(pitch: f64, mono: &[f64], from: f64, to: f64) -> Vec<f64> {
1408        let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1409        let notes = NoteList::new(vec![note(start + from, start + to, pitch)]);
1410        split_at_attacks(&notes, &StemLevels::new(mono, rate()))
1411            .notes()
1412            .iter()
1413            .map(|piece| piece.onset().get() - start)
1414            .collect()
1415    }
1416
1417    /// [`stem_at`] `f0` at `before` until 50 ms before [`onset`], silent
1418    /// to it, and at 0.5 from it.
1419    fn gap_at(f0: f64, before: f64) -> Vec<f64> {
1420        stem_at(f0, onset(), |t| {
1421            let level = if t < -0.05 {
1422                before
1423            } else if t < 0.0 {
1424                0.0
1425            } else {
1426                0.5
1427            };
1428            (level, 0.0)
1429        })
1430    }
1431
1432    #[test]
1433    fn a_note_splits_at_the_last_tenth_before_the_climb() {
1434        // At A2 the window spans 9.1 ms, and its reading falls to a tenth
1435        // of the peak 0.49 of it, 4.5 ms, before a step from silence; the
1436        // readings start 12.5 ms before the dip frame, a few ms off that.
1437        let pieces = split_at(45.0, &gap_at(110.0, 0.5), -0.3, 0.3);
1438        assert_eq!(pieces.len(), 2);
1439        assert!(
1440            (pieces[1] + 0.0053).abs() < 0.0005,
1441            "split at {}",
1442            pieces[1]
1443        );
1444        assert_eq!(
1445            split_at(45.0, &stem(onset(), |_| (0.5, 0.0)), -0.3, 0.3).len(),
1446            1
1447        );
1448    }
1449
1450    #[test]
1451    fn the_piece_after_a_split_lasts_attack_after() {
1452        // The split falls 5.3 ms before the onset.
1453        for (end, expected) in [(0.0452, 2), (0.0442, 1)] {
1454            assert_eq!(
1455                split_at(45.0, &gap_at(110.0, 0.5), -0.3, end).len(),
1456                expected,
1457                "note to {end} s"
1458            );
1459        }
1460    }
1461
1462    #[test]
1463    fn the_piece_before_a_split_reaches_the_second_share_of_hold_span() {
1464        // Before the gap the stem lies at 0.61 or 0.59 of its level after
1465        // it. Below six tenths the hold search finds no fall and the long
1466        // dip holds, so only the piece before keeps it whole.
1467        for (before, expected) in [(0.305, 2), (0.295, 1)] {
1468            assert_eq!(
1469                split_at(45.0, &gap_at(110.0, before), -0.3, 0.3).len(),
1470                expected,
1471                "{before} before the gap"
1472            );
1473        }
1474    }
1475
1476    #[test]
1477    fn the_piece_before_a_split_is_loud_hold_before_ahead_of_the_dip() {
1478        // The dip frame, the last silent one, centres 12.5 ms before the
1479        // onset. From 75 ms before it, the note holds a frame centred
1480        // HOLD_BEFORE before that; from 70 ms, it holds none.
1481        for (from, expected) in [(-0.075, 2), (-0.07, 1)] {
1482            assert_eq!(
1483                split_at(45.0, &gap_at(110.0, 0.5), from, 0.3).len(),
1484                expected,
1485                "note from {from} s"
1486            );
1487        }
1488    }
1489
1490    #[test]
1491    fn a_split_needs_an_attack() {
1492        // At A2 a rise may take 14.3 ms. After a 50 ms gap a linear attack
1493        // of 12 ms splits and one of 13 ms rises too slowly.
1494        for (ramp, expected) in [(0.012, 2), (0.013, 1)] {
1495            let mono = stem(onset(), |t| {
1496                let level = if t < -0.05 {
1497                    0.5
1498                } else {
1499                    0.5 * (t / ramp).clamp(0.0, 1.0)
1500                };
1501                (level, 0.0)
1502            });
1503            assert_eq!(
1504                split_at(45.0, &mono, -0.3, 0.3).len(),
1505                expected,
1506                "ramp {ramp} s"
1507            );
1508        }
1509    }
1510
1511    #[test]
1512    fn a_quieter_repeat_splits() {
1513        // After a 25 ms gap the first readings still see the note before
1514        // it, 6 dB louder, above nine tenths of the peak after the dip;
1515        // the climb starts at the quietest reading.
1516        let mono = stem(onset(), |t| {
1517            let level = if t < -0.025 {
1518                1.0
1519            } else if t < 0.0 {
1520                0.0
1521            } else {
1522                0.5
1523            };
1524            (level, 0.0)
1525        });
1526        let pieces = split_at(45.0, &mono, -0.3, 0.3);
1527        assert_eq!(pieces.len(), 2);
1528        assert!(
1529            (pieces[1] + 0.0053).abs() < 0.0005,
1530            "split at {}",
1531            pieces[1]
1532        );
1533    }
1534
1535    /// [`stem`] with its A2 at `low(t)` and a tone at its second harmonic
1536    /// at `high(t)`.
1537    fn harmonic_stem(level: impl Fn(f64) -> (f64, f64)) -> Vec<f64> {
1538        (-LEAD_SAMPLES..22_050)
1539            .map(|index| {
1540                let t = f64::from(index) / 22_050.0;
1541                let (low, high) = level(t - onset());
1542                low * (std::f64::consts::TAU * 110.0 * t).sin()
1543                    + high * (std::f64::consts::TAU * 220.0 * t).sin()
1544            })
1545            .collect()
1546    }
1547
1548    #[test]
1549    fn the_fundamental_dips_by_fundamental_dip() {
1550        // An A2 at 0.5 under a tone at its second harmonic at 2, and in the
1551        // gap the A2 alone: the stem and the harmonics dip past 30 dB. The
1552        // fundamental reads up to about 0.55 after the gap, where the
1553        // tone's step leaks into its bin, so 0.0375 lies 23.3 dB below it
1554        // and 0.04 22.7 dB.
1555        for (floor, expected) in [(0.0375, 2), (0.04, 1)] {
1556            let mono = harmonic_stem(|t| {
1557                if (-0.05..0.0).contains(&t) {
1558                    (floor, 0.0)
1559                } else {
1560                    (0.5, 2.0)
1561                }
1562            });
1563            assert_eq!(
1564                split_at(45.0, &mono, -0.3, 0.3).len(),
1565                expected,
1566                "A2 at {floor} in the gap"
1567            );
1568        }
1569    }
1570
1571    #[test]
1572    fn a_last_piece_starts_the_next_note_within_split_gap() {
1573        // The decoder ends the note 58 ms after the onset and starts the
1574        // next one of its key 23 or 24 ms later. Within SPLIT_GAP the cut
1575        // piece starts that note; past it, the piece stays a note.
1576        let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1577        let levels_mono = gap_at(110.0, 0.5);
1578        let levels = StemLevels::new(&levels_mono, rate());
1579        for (gap, expected) in [(0.023, 2), (0.024, 3)] {
1580            let notes = NoteList::new(vec![
1581                heard(start - 0.3, start + 0.058, 45.0, 80),
1582                heard(start + 0.058 + gap, start + 0.3, 45.0, 90).with_octave_uncertain(true),
1583            ]);
1584            let pieces = split_at_attacks(&notes, &levels);
1585            assert_eq!(pieces.notes().len(), expected, "gap {gap} s");
1586            let last = &pieces.notes()[expected - 1];
1587            if expected == 2 {
1588                assert!((last.onset().get() - start + 0.0053).abs() < 0.0005);
1589                assert_eq!(last.velocity(), Some(Velocity::new(90).unwrap()));
1590                assert_eq!(last.octave_uncertain(), Some(true));
1591            }
1592        }
1593        // A note with no cut keeps the next note as it was.
1594        let steady = stem(onset(), |_| (0.5, 0.0));
1595        let notes = NoteList::new(vec![
1596            heard(start - 0.3, start + 0.058, 45.0, 80),
1597            heard(start + 0.07, start + 0.3, 45.0, 90),
1598        ]);
1599        assert_eq!(
1600            split_at_attacks(&notes, &StemLevels::new(&steady, rate()))
1601                .notes()
1602                .len(),
1603            2
1604        );
1605        // A next note of another key stays as it was.
1606        let notes = NoteList::new(vec![
1607            heard(start - 0.3, start + 0.058, 45.0, 80),
1608            heard(start + 0.07, start + 0.3, 47.0, 90),
1609        ]);
1610        assert_eq!(split_at_attacks(&notes, &levels).notes().len(), 3);
1611    }
1612
1613    #[test]
1614    fn a_short_last_piece_stays_before_a_next_note_with_its_own_attack() {
1615        // A second gap from 60 to 90 ms after the onset: the decoder ends
1616        // the note 25 ms into it and starts the next note 10 ms later, at
1617        // 95 ms. The cut piece lasts 90.3 ms, within the 100 ms limit, but
1618        // the next note has its own attack, so the piece stays a note and
1619        // the next one keeps its onset.
1620        let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1621        let mono = stem(onset(), |t| {
1622            let gap = (-0.05..0.0).contains(&t) || (0.06..0.09).contains(&t);
1623            (if gap { 0.0 } else { 0.5 }, 0.0)
1624        });
1625        let notes = NoteList::new(vec![
1626            heard(start - 0.3, start + 0.085, 45.0, 80),
1627            heard(start + 0.095, start + 0.4, 45.0, 90),
1628        ]);
1629        let pieces = split_at_attacks(&notes, &StemLevels::new(&mono, rate()));
1630        let onsets: Vec<f64> = pieces
1631            .notes()
1632            .iter()
1633            .map(|piece| piece.onset().get() - start)
1634            .collect();
1635        assert_eq!(onsets.len(), 3, "{onsets:?}");
1636        assert!((onsets[1] + 0.0053).abs() < 0.0005, "{onsets:?}");
1637        assert!((onsets[2] - 0.095).abs() < 1e-9, "{onsets:?}");
1638    }
1639
1640    #[test]
1641    fn a_long_last_piece_stays_before_a_next_note_with_its_own_attack() {
1642        // A second gap from 100 to 140 ms after the onset: the decoder ends
1643        // the note 25 ms into it and starts the next note 20 ms later, at
1644        // 145 ms. The cut piece lasts 130 ms, past the 100 ms limit, so it
1645        // stays a note and the next one keeps its onset.
1646        let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1647        let mono = stem(onset(), |t| {
1648            let gap = (-0.05..0.0).contains(&t) || (0.1..0.14).contains(&t);
1649            (if gap { 0.0 } else { 0.5 }, 0.0)
1650        });
1651        let notes = NoteList::new(vec![
1652            heard(start - 0.3, start + 0.125, 45.0, 80),
1653            heard(start + 0.145, start + 0.4, 45.0, 90),
1654        ]);
1655        let pieces = split_at_attacks(&notes, &StemLevels::new(&mono, rate()));
1656        let onsets: Vec<f64> = pieces
1657            .notes()
1658            .iter()
1659            .map(|piece| piece.onset().get() - start)
1660            .collect();
1661        assert_eq!(onsets.len(), 3, "{onsets:?}");
1662        assert!((onsets[1] + 0.0053).abs() < 0.0005, "{onsets:?}");
1663        assert!((onsets[2] - 0.145).abs() < 1e-9, "{onsets:?}");
1664    }
1665
1666    #[test]
1667    fn the_trough_comes_from_the_dip_frame_s_26_readings() {
1668        // An A4 at 0.5, a gap at -54 dB that ends at the dip frame's
1669        // centre, a 10 or 11 ms repeat and a silent 25 ms gap. Each reading
1670        // spans 4 ms. After the 10 ms repeat the 26th reading, at the
1671        // frame's end, is the first silent one: the trough, so the cut
1672        // falls after the second gap. After the 11 ms repeat the first
1673        // silent reading is the 27th, outside the frame, so the trough
1674        // stays in the first gap and the cut falls before the repeat.
1675        let frame = 100;
1676        let len = 44_100;
1677        let centre = StemLevels::new(&vec![0.0; len], rate())
1678            .frame_time(frame)
1679            .get();
1680        for (repeat, late) in [(0.01, true), (0.011, false)] {
1681            let mono: Vec<f64> = (0..len)
1682                .map(|index| {
1683                    let t = f64::from(u32::try_from(index).unwrap()) / 22_050.0 - centre;
1684                    let level = if (-0.02..0.0).contains(&t) {
1685                        0.002
1686                    } else if (repeat..repeat + 0.025).contains(&t) {
1687                        0.0
1688                    } else {
1689                        0.5
1690                    };
1691                    level * (std::f64::consts::TAU * 440.0 * (t + centre)).sin()
1692                })
1693                .collect();
1694            let levels = StemLevels::new(&mono, rate());
1695            let level = |m: usize| if m == frame { 0.001 } else { 1.0 };
1696            let onset = split_point(&levels, frame, MidiPitch::new(69.0).unwrap(), &level, 1.0)
1697                .map(|onset| onset.get() - centre);
1698            let cut = onset.unwrap();
1699            if late {
1700                assert!(cut > repeat + 0.02, "repeat {repeat} s: cut {cut}");
1701            } else {
1702                assert!(cut < 0.0, "repeat {repeat} s: cut {cut}");
1703            }
1704        }
1705    }
1706
1707    #[test]
1708    fn the_climb_starts_in_the_dip_frame() {
1709        // A 35 ms gap at -54 dB, a 15 ms repeat and a 40 ms gap of silence.
1710        // The note splits 5 ms before the end of the first gap, as when
1711        // both gaps are silent; the deeper second gap does not move the
1712        // cut past the repeat.
1713        for floor in [0.002, 0.0] {
1714            let mono = stem(onset(), |t| {
1715                let level = if (0.0..0.035).contains(&t) {
1716                    floor
1717                } else if (0.05..0.09).contains(&t) {
1718                    0.0
1719                } else {
1720                    0.5
1721                };
1722                (level, 0.0)
1723            });
1724            let onsets = split_at(45.0, &mono, -0.3, 0.3);
1725            assert_eq!(onsets.len(), 2, "floor {floor}: {onsets:?}");
1726            assert!(
1727                (onsets[1] - 0.03).abs() < 0.001,
1728                "floor {floor}: {onsets:?}"
1729            );
1730        }
1731    }
1732
1733    #[test]
1734    fn a_short_repeat_after_a_short_gap_takes_the_cut_piece() {
1735        // Gates end 100 ms before the onset and `length` after it, and a
1736        // third opens 15 ms later, until 450 ms. The decoder starts the
1737        // repeat 5 ms before the third gate. With a gate of 80 ms the cut
1738        // piece lasts 85.3 ms, and the attack rule reads no attack after a
1739        // gap that short, so the piece starts the repeat: 2 notes, and the
1740        // repeat starts at the cut. A gate of 100 ms gives a piece past
1741        // the limit, and the repeat keeps its own onset.
1742        let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1743        for (length, expected) in [(0.08, 2), (0.1, 3)] {
1744            let mono = stem(onset(), |t| {
1745                let open =
1746                    t < -0.1 || (0.0..length).contains(&t) || (length + 0.015..0.45).contains(&t);
1747                (if open { 0.5 } else { 0.0 }, 0.0)
1748            });
1749            let notes = NoteList::new(vec![
1750                heard(start - 0.3, start + length, 45.0, 80),
1751                heard(start + length + 0.01, start + 0.45, 45.0, 90),
1752            ]);
1753            let pieces = split_at_attacks(&notes, &StemLevels::new(&mono, rate()));
1754            let onsets: Vec<f64> = pieces
1755                .notes()
1756                .iter()
1757                .map(|piece| piece.onset().get() - start)
1758                .collect();
1759            assert_eq!(onsets.len(), expected, "gate {length} s: {onsets:?}");
1760            let last = onsets[expected - 1];
1761            let onset = if expected == 2 {
1762                -0.0053
1763            } else {
1764                length + 0.01
1765            };
1766            assert!((last - onset).abs() < 0.0005, "gate {length} s: {onsets:?}");
1767        }
1768    }
1769
1770    #[test]
1771    fn a_last_piece_starts_the_next_note_up_to_twice_attack_after() {
1772        // The cut falls 5.3 ms before the onset, and the decoder ends the
1773        // note 94 or 96 ms after it: a last piece of 99.3 or 101.3 ms.
1774        let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1775        let mono = gap_at(110.0, 0.5);
1776        let levels = StemLevels::new(&mono, rate());
1777        for (end, expected) in [(0.094, 2), (0.096, 3)] {
1778            let notes = NoteList::new(vec![
1779                heard(start - 0.3, start + end, 45.0, 80),
1780                heard(start + end + 0.02, start + 0.4, 45.0, 90),
1781            ]);
1782            let pieces = split_at_attacks(&notes, &levels);
1783            assert_eq!(pieces.notes().len(), expected, "end {end} s");
1784        }
1785    }
1786
1787    #[test]
1788    fn a_long_last_piece_stays_before_a_repeat_after_a_short_gap() {
1789        // Gates end 100 ms before the onset and 200 ms after it, and a
1790        // third opens 20, 15 or 10 ms later, until 450 ms. The decoder
1791        // reads the first two gates as one note to 200 ms and starts the
1792        // next 5 ms before the third gate, within SPLIT_GAP. The cut piece
1793        // lasts 200 ms, and the repeat keeps its own onset.
1794        let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1795        for gap in [0.02, 0.015, 0.01] {
1796            let mono = stem(onset(), |t| {
1797                let open = t < -0.1 || (0.0..0.2).contains(&t) || (0.2 + gap..0.45).contains(&t);
1798                (if open { 0.5 } else { 0.0 }, 0.0)
1799            });
1800            let notes = NoteList::new(vec![
1801                heard(start - 0.3, start + 0.2, 45.0, 80),
1802                heard(start + 0.2 + gap - 0.005, start + 0.45, 45.0, 90),
1803            ]);
1804            let pieces = split_at_attacks(&notes, &StemLevels::new(&mono, rate()));
1805            let onsets: Vec<f64> = pieces
1806                .notes()
1807                .iter()
1808                .map(|piece| piece.onset().get() - start)
1809                .collect();
1810            assert_eq!(onsets.len(), 3, "gap {gap} s: {onsets:?}");
1811            assert!((onsets[2] - (0.195 + gap)).abs() < 1e-9, "{onsets:?}");
1812        }
1813    }
1814
1815    #[test]
1816    fn a_note_from_the_stem_start_reads_only_the_stem() {
1817        // No lead: the note starts on the stem's first frame. A steady stem
1818        // and a gap 20 ms after the start give no split.
1819        let lead = usize::try_from(LEAD_SAMPLES).unwrap();
1820        for mono in [
1821            stem(onset(), |_| (0.5, 0.0)),
1822            stem(0.0, |t| {
1823                (if (0.02..0.07).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1824            }),
1825        ] {
1826            let levels = StemLevels::new(&mono[lead..], rate());
1827            let notes = NoteList::new(vec![note(0.0, 0.5, 45.0)]);
1828            assert_eq!(split_at_attacks(&notes, &levels).notes().len(), 1);
1829        }
1830    }
1831
1832    #[test]
1833    fn every_piece_keeps_the_velocity_and_the_octave_flag() {
1834        let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1835        let mono = gap_at(110.0, 0.5);
1836        let notes = NoteList::new(vec![
1837            heard(start - 0.3, start + 0.3, 45.0, 77).with_octave_uncertain(true),
1838        ]);
1839        let pieces = split_at_attacks(&notes, &StemLevels::new(&mono, rate()));
1840        assert_eq!(pieces.notes().len(), 2);
1841        for piece in pieces.notes() {
1842            assert_eq!(piece.velocity(), Some(Velocity::new(77).unwrap()));
1843            assert_eq!(piece.octave_uncertain(), Some(true));
1844        }
1845        // The pieces span the note: its onset, the cut, and its offset.
1846        let [first, last] = pieces.notes() else {
1847            unreachable!()
1848        };
1849        assert_eq!(first.onset(), notes.notes()[0].onset());
1850        assert_eq!(first.offset(), last.onset());
1851        assert_eq!(last.offset(), notes.notes()[0].offset());
1852    }
1853
1854    #[test]
1855    fn the_rise_window_spans_a_period_and_at_least_rise_window() {
1856        let at = |pitch: f64| rise_window(MidiPitch::new(pitch).unwrap());
1857        assert!((at(45.0) - 1.0 / 110.0).abs() < 1e-12);
1858        // A5 spans 1.1 ms.
1859        assert_eq!(at(81.0), RISE_WINDOW.get());
1860    }
1861
1862    #[test]
1863    fn notes_split_from_split_lowest() {
1864        assert_eq!(split_at(33.0, &gap_at(55.0, 0.5), -0.3, 0.3).len(), 2);
1865        assert_eq!(split_at(32.0, &gap_at(51.913, 0.5), -0.3, 0.3).len(), 1);
1866        // The same stem read as A1 splits.
1867        assert_eq!(split_at(33.0, &gap_at(51.913, 0.5), -0.3, 0.3).len(), 2);
1868    }
1869
1870    #[test]
1871    fn two_repeated_notes_with_a_clear_gap_stay_two() {
1872        let (run, time) = run_with_onset(0.0);
1873        let notes = NoteList::new(vec![
1874            note(time(10), time(25), 45.0),
1875            note(time(30), time(50), 45.0),
1876        ]);
1877        assert_eq!(join_splits(&notes, &run).notes().len(), 2);
1878    }
1879
1880    #[test]
1881    fn a_merged_note_takes_the_higher_velocity() {
1882        let notes = NoteList::new(vec![heard(0.0, 0.5, 45.0, 40), heard(0.2, 0.8, 45.0, 90)]);
1883        assert_eq!(merge_overlaps(&notes).notes(), &[heard(0.0, 0.8, 45.0, 90)]);
1884    }
1885
1886    #[test]
1887    fn notes_of_one_key_merge_only_when_they_overlap() {
1888        let notes = NoteList::new(vec![
1889            note(0.0, 0.5, 45.0),
1890            note(0.2, 0.8, 45.0),
1891            note(0.3, 0.4, 52.0),
1892            note(1.0, 1.5, 45.0),
1893        ]);
1894        assert_eq!(
1895            merge_overlaps(&notes).notes(),
1896            &[
1897                note(0.0, 0.8, 45.0),
1898                note(0.3, 0.4, 52.0),
1899                note(1.0, 1.5, 45.0)
1900            ]
1901        );
1902    }
1903}