Skip to main content

sound_to_patch/notes/
finish.rs

1//! The final note list: the list rewritten with the layer report, in the
2//! ticks of `notes.mid`.
3//!
4//! [`finish`] reads what the report says of each note and of the line. It
5//! merges each pitch drop into one note at its settled pitch, gives a note
6//! under a vibrato the key of the vibrato's mean pitch, ends each note at
7//! its note-off knee, and writes each glide as two overlapping notes. The
8//! velocity follows the full-band level of each note when the level spread
9//! gives velocity a path to the level, and is one constant otherwise. Only
10//! the pitch motion of a mono line that no route models becomes bends.
11
12use super::write::{Span, encode_spans, ticks_per_second};
13use super::{
14    Contour, DEFAULT_VELOCITY, MODEL_RATE, MidiWriteError, NoteList, TRACK_FRAME, TRACK_HOP,
15    Velocity, min_note_length, profile,
16};
17
18use crate::cast;
19use crate::render::{MAX_PITCH_BEND, PITCH_BEND_CENTRE};
20use crate::units::{Bpm, Decibels, Hz, MidiPitch, Seconds, Semitones};
21
22/// A glide join overlaps the two notes by at least this long, rounded up
23/// to whole ticks.
24pub const GLIDE_OVERLAP: Seconds = Seconds::from_const(0.005);
25/// A note without a note-off knee ends this long before its end in the
26/// list.
27pub const NO_KNEE_CUT: Seconds = Seconds::from_const(0.050);
28/// A pad note ends where its level falls this far below its plateau.
29pub const PAD_FALL: Decibels = Decibels::from_const(20.0);
30/// A glide joins two notes when the later onset lies this close to the
31/// glide's motion, from its start to its end.
32pub const GLIDE_SLACK: Seconds = Seconds::from_const(0.050);
33/// A pitch drop takes in the notes that start from this long before its
34/// onset until it settles.
35pub const DROP_SLACK: Seconds = Seconds::from_const(0.030);
36/// A note of a mono line gets bends when its contour leaves its key by
37/// this much outside every route's span, over the tracker's reach.
38pub const BEND_MIN: Semitones = Semitones::from_const(0.5);
39/// The bend range the file assumes; a host must set the synth to it, and
40/// `render` reads no bend.
41pub const BEND_RANGE: Semitones = Semitones::from_const(2.0);
42/// The level law: a note `d` dB under the loudest gets velocity
43/// `127 * 10^(-d / 40)`.
44pub const VELOCITY_LAW_DB: f64 = 40.0;
45
46/// What the layer report says of one note of the list.
47#[derive(Clone, Copy, Debug, Default, PartialEq)]
48pub struct NoteFacts {
49    /// The note-off knee; `None` when no knee shows.
50    pub knee: Option<Seconds>,
51    /// Where the level of a note of a polyphonic line falls [`PAD_FALL`]
52    /// below its plateau, within its span in the list; `None` when the
53    /// level does not fall that far by its end in the list, or the line is
54    /// mono.
55    pub pad_end: Option<Seconds>,
56    /// The full-band level of the note.
57    pub level: Option<Decibels>,
58    /// The envelope layer read the note, so a missing knee means none
59    /// shows; without a read nothing was observed.
60    pub read: bool,
61}
62
63/// A glide of the pitch layer.
64#[derive(Clone, Copy, Debug, PartialEq)]
65pub struct GlideSpan {
66    /// The start of the motion.
67    pub start: Seconds,
68    /// The interval from the start pitch to the end pitch.
69    pub interval: Semitones,
70    /// The full 0 to 100 % time.
71    pub time: Seconds,
72}
73
74/// A pitch drop of the pitch layer.
75#[derive(Clone, Copy, Debug, PartialEq)]
76pub struct DropSpan {
77    /// The onset of the strike.
78    pub onset: Seconds,
79    /// The settled frequency.
80    pub f_end: Hz,
81    /// The frequency above `f_end` at the onset; negative for a rise.
82    pub df_hz: f64,
83    /// Where the drop has settled.
84    pub settled: Seconds,
85}
86
87/// A vibrato of the pitch layer.
88#[derive(Clone, Copy, Debug, PartialEq)]
89pub struct VibratoSpan {
90    /// The start of the span.
91    pub start: Seconds,
92    /// The end of the span.
93    pub end: Seconds,
94    /// The mean pitch of the span.
95    pub pitch: Hz,
96}
97
98/// Reads the full-band level of the stem from one time to another, with
99/// the RMS window of a frequency's period.
100pub type LevelReader<'a> = &'a dyn Fn(Seconds, Seconds, Hz) -> Option<Decibels>;
101
102/// What [`finish`] reads.
103#[derive(Clone, Copy)]
104pub struct FinishInput<'a> {
105    /// The note list.
106    pub notes: &'a NoteList,
107    /// One record per note of the list, in its order; a missing one reads
108    /// as no knee, no pad end and no level.
109    pub facts: &'a [NoteFacts],
110    /// The tempo of the file.
111    pub bpm: Bpm,
112    /// One voice at a time: notes overlap only at glide joins. Otherwise
113    /// the line is polyphonic, and a note ends at its pad end where it has
114    /// one.
115    pub mono: bool,
116    /// Velocity has a path to the level, so it follows each note's level.
117    pub level_velocity: bool,
118    /// The glides, in time order.
119    pub glides: &'a [GlideSpan],
120    /// The pitch drops.
121    pub drops: &'a [DropSpan],
122    /// The vibratos.
123    pub vibrato: &'a [VibratoSpan],
124    /// The full-band level of the stem. It fills a level that the facts
125    /// lack.
126    pub level: LevelReader<'a>,
127}
128
129/// Which rule set the end of a final note.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum EndRule {
132    /// The note-off knee.
133    Knee,
134    /// No knee shows: [`NO_KNEE_CUT`] before the end in the list.
135    NoKnee,
136    /// The pad end.
137    Pad,
138    /// The cut would make the note shorter than the BPM's minimum.
139    Minimum,
140    /// The next note of a mono line starts there.
141    NextNote,
142    /// A glide join: [`GLIDE_OVERLAP`] after the next note-on.
143    Glide,
144    /// No rule read the note: the envelope layer skipped it and it has no
145    /// pad end. It ends [`NO_KNEE_CUT`] before its end in the list, as
146    /// without a knee.
147    Unread,
148}
149
150impl EndRule {
151    /// The name `layers.json` gives the rule.
152    #[must_use]
153    pub fn name(self) -> &'static str {
154        match self {
155            Self::Knee => "knee",
156            Self::NoKnee => "no_knee",
157            Self::Pad => "pad",
158            Self::Minimum => "minimum",
159            Self::NextNote => "next_note",
160            Self::Glide => "glide",
161            Self::Unread => "unread",
162        }
163    }
164}
165
166/// One note of the final list, in ticks.
167#[derive(Clone, Copy, Debug, PartialEq)]
168pub struct FinalNote {
169    /// The note-on tick.
170    pub start: u64,
171    /// The note-off tick, after `start`.
172    pub end: u64,
173    /// The key.
174    pub key: u8,
175    /// The velocity.
176    pub velocity: Velocity,
177    /// The rule that set the end.
178    pub end_rule: EndRule,
179    /// The note's onset in the list; the earliest one for a merged drop.
180    pub list_onset: Seconds,
181    /// The note's end in the list; the latest one for a merged drop.
182    pub list_end: Seconds,
183    /// The full-band level, the highest of a merged drop.
184    pub level: Option<Decibels>,
185    /// The transcriber's octave flag, true when any merged note has it.
186    pub octave_uncertain: Option<bool>,
187}
188
189/// The final note list.
190#[derive(Clone, Debug, PartialEq)]
191pub struct FinalNotes {
192    /// The notes, sorted by start, then key.
193    pub notes: Vec<FinalNote>,
194    /// The pitch-bend events: a tick and a 14-bit value, centre 8192.
195    pub bends: Vec<(u64, u16)>,
196    /// The tempo of the ticks.
197    pub bpm: Bpm,
198    /// Velocity follows each note's level.
199    pub level_velocity: bool,
200    /// The line was mono, so the mono-line rules ran.
201    pub mono: bool,
202    /// The notes of a mono line dropped because another started on the
203    /// same tick.
204    pub dropped: usize,
205}
206
207impl FinalNotes {
208    /// The time of a tick.
209    #[must_use]
210    pub fn seconds(&self, tick: u64) -> Seconds {
211        seconds_of(tick, ticks_per_second(self.bpm))
212    }
213
214    /// Encodes the list as a format 0 file on channel 1, with a track
215    /// name when one is given.
216    ///
217    /// # Errors
218    ///
219    /// The errors of [`write_midi`](super::write_midi).
220    pub fn to_midi(&self, name: Option<&str>) -> Result<Vec<u8>, MidiWriteError> {
221        let spans: Vec<Span> = self
222            .notes
223            .iter()
224            .map(|n| (midly::num::u7::new(n.key), n.start, n.end, n.velocity))
225            .collect();
226        encode_spans(spans, &self.bends, self.bpm, name)
227    }
228}
229
230/// A note on its way to the final list, in seconds until the ticks.
231#[derive(Clone, Debug)]
232struct Work<'a> {
233    onset: f64,
234    list_onset: f64,
235    list_end: f64,
236    key: u8,
237    facts: NoteFacts,
238    octave_uncertain: Option<bool>,
239    contour: Option<&'a Contour>,
240}
241
242/// Rewrites the note list with the layer report.
243///
244/// The steps run in this order:
245///
246/// 1. Each pitch drop merges the notes that start from [`DROP_SLACK`]
247///    before its onset until it settles, touch each other and lie on keys
248///    from `f_end` to its start pitch, into one note at the key of
249///    `f_end`.
250/// 2. A note that a vibrato covers for more than half its length takes the
251///    key of the vibrato's mean pitch, when that lies within a semitone.
252/// 3. On a polyphonic line a note ends at its pad end where it has one.
253///    Otherwise it ends at its knee, else [`NO_KNEE_CUT`] before its end in
254///    the list. No note ends after its end in the list. A cut never makes a
255///    note shorter than [`min_note_length`]; a note the list gives shorter
256///    keeps its length.
257/// 4. Times become ticks with `round(seconds * BPM / 60 * 480)`, and each
258///    note lasts at least one tick. A note without a level reads it over
259///    this written span, which on a mono line stops at the next later
260///    onset.
261/// 5. In a mono line, of the notes that start on one tick only one stays:
262///    a note without the transcriber's octave flag before one with it, then
263///    the longer, then the lower key.
264/// 6. In a mono line, a glide joins the pair of neighbours whose interval
265///    lies within a semitone of its own and whose later onset lies within
266///    [`GLIDE_SLACK`] of its motion, the nearest to its start. The later
267///    note starts at the glide's start, or at its own onset when the glide
268///    starts after it, and the earlier one ends [`GLIDE_OVERLAP`] after
269///    that start, rounded up to whole ticks, whatever its knee says. Every
270///    other note of a mono line ends no later than the next note starts.
271/// 7. With level velocity, the loudest note gets 127 and a note `d` dB
272///    under it `round(127 * 10^(-d / 40))`, at least 1; a note without a
273///    level, and every note otherwise, gets [`DEFAULT_VELOCITY`].
274/// 8. On a mono line, a note whose contour leaves its key by [`BEND_MIN`]
275///    or more, of one sign, over adjacent frames that span the tracker's
276///    reach (a quarter frame plus one period), outside every glide, drop
277///    and vibrato span, gets a bend at each contour point outside those
278///    spans, clamped to [`BEND_RANGE`], and a centred bend at its end.
279#[must_use]
280pub fn finish(input: &FinishInput<'_>) -> FinalNotes {
281    let mut work: Vec<Work<'_>> = input
282        .notes
283        .notes()
284        .iter()
285        .enumerate()
286        .map(|(i, note)| Work {
287            onset: note.onset().get(),
288            list_onset: note.onset().get(),
289            list_end: note.offset().get(),
290            key: key_of(note.pitch().get()),
291            facts: input.facts.get(i).copied().unwrap_or_default(),
292            octave_uncertain: note.octave_uncertain(),
293            contour: note.contour(),
294        })
295        .collect();
296    for drop in input.drops {
297        merge_drop(&mut work, drop);
298    }
299    for note in &mut work {
300        follow_vibrato(note, input.vibrato);
301    }
302    let tps = ticks_per_second(input.bpm);
303    let tick = |seconds: f64| tick_of(seconds, tps);
304    let minimum = min_note_length(input.bpm).get();
305    let mut notes: Vec<(FinalNote, Option<&Contour>)> = work
306        .iter()
307        .map(|w| {
308            let (mut end, mut rule) = match (input.mono, w.facts.pad_end, w.facts.knee) {
309                (false, Some(pad), _) => (pad.get(), EndRule::Pad),
310                (_, _, Some(knee)) => (knee.get(), EndRule::Knee),
311                _ if w.facts.read => (w.list_end - NO_KNEE_CUT.get(), EndRule::NoKnee),
312                _ => (w.list_end - NO_KNEE_CUT.get(), EndRule::Unread),
313            };
314            end = end.min(w.list_end);
315            // The minimum lengthens a cut note up to its list end, and
316            // shortens none.
317            let floor = (w.onset + minimum).min(w.list_end);
318            if end < floor {
319                end = floor;
320                rule = EndRule::Minimum;
321            }
322            let start = tick(w.onset);
323            let note = FinalNote {
324                start,
325                end: tick(end).max(start + 1),
326                key: w.key,
327                velocity: DEFAULT_VELOCITY,
328                end_rule: rule,
329                list_onset: Seconds::clamped(w.list_onset),
330                list_end: Seconds::clamped(w.list_end),
331                level: w.facts.level,
332                octave_uncertain: w.octave_uncertain,
333            };
334            (note, w.contour)
335        })
336        .collect();
337    notes.sort_by_key(|(n, _)| (n.start, n.key));
338    for i in 0..notes.len() {
339        let note = notes[i].0;
340        if note.level.is_none() {
341            // On a mono line the span stops at the next later onset, as the
342            // layer's own span does, so a short note reads none of the next.
343            let next = notes[i..]
344                .iter()
345                .map(|(n, _)| n.start)
346                .find(|&start| input.mono && start > note.start)
347                .unwrap_or(u64::MAX);
348            notes[i].0.level = (input.level)(
349                seconds_of(note.start, tps),
350                seconds_of(note.end.min(next), tps),
351                Hz::from(MidiPitch::clamped(f64::from(note.key))),
352            );
353        }
354    }
355    let dropped = if input.mono {
356        join_mono(&mut notes, input, tps)
357    } else {
358        Vec::new()
359    };
360    let bends = if input.mono {
361        bends_of(&notes, &dropped, input, tps)
362    } else {
363        Vec::new()
364    };
365    let mut list: Vec<FinalNote> = notes.into_iter().map(|(n, _)| n).collect();
366    if input.level_velocity {
367        level_velocities(&mut list);
368    }
369    FinalNotes {
370        notes: list,
371        bends,
372        bpm: input.bpm,
373        level_velocity: input.level_velocity,
374        mono: input.mono,
375        dropped: dropped.len(),
376    }
377}
378
379/// The time of a tick.
380fn seconds_of(tick: u64, tps: f64) -> Seconds {
381    Seconds::clamped(cast::u64_to_f64(tick) / tps)
382}
383
384/// The nearest tick of a time, 0 before the start.
385fn tick_of(seconds: f64, tps: f64) -> u64 {
386    cast::round_to_usize(seconds * tps)
387        .and_then(|t| u64::try_from(t).ok())
388        .unwrap_or(0)
389}
390
391/// The nearest key of a pitch, within 0 to 127.
392fn key_of(pitch: f64) -> u8 {
393    cast::round_to_usize(pitch.clamp(0.0, 127.0))
394        .and_then(|key| u8::try_from(key).ok())
395        .unwrap_or(127)
396}
397
398/// The MIDI pitch of a frequency, `None` outside MIDI notes 0 to 127.
399fn pitch_of(f0: Hz) -> Option<f64> {
400    MidiPitch::try_from(f0).ok().map(MidiPitch::get)
401}
402
403/// Merges the notes a drop takes in into one at the key of `f_end`: from
404/// [`DROP_SLACK`] before its onset until it settles, each starting before
405/// the one before it ends plus that slack, each on a key from `f_end` to
406/// the drop's start pitch.
407fn merge_drop(work: &mut Vec<Work<'_>>, drop: &DropSpan) {
408    let Some(pitch) = pitch_of(drop.f_end) else {
409        return;
410    };
411    let start = Hz::new(drop.f_end.get() + drop.df_hz)
412        .ok()
413        .and_then(pitch_of)
414        .unwrap_or(pitch);
415    let (low, high) = (pitch.min(start).round(), pitch.max(start).round());
416    let from = drop.onset.get() - DROP_SLACK.get();
417    let mut taken: Vec<usize> = Vec::new();
418    for (i, note) in work.iter().enumerate() {
419        let touches = taken
420            .last()
421            .is_none_or(|&j| note.onset <= work[j].list_end + DROP_SLACK.get());
422        if (from..=drop.settled.get()).contains(&note.onset)
423            && (low..=high).contains(&f64::from(note.key))
424            && touches
425        {
426            taken.push(i);
427        }
428    }
429    let Some(&first) = taken.first() else {
430        return;
431    };
432    let mut merged = work[first].clone();
433    for &i in &taken[1..] {
434        let other = &work[i];
435        merged.onset = merged.onset.min(other.onset);
436        merged.list_onset = merged.list_onset.min(other.list_onset);
437        if other.list_end > merged.list_end {
438            merged.list_end = other.list_end;
439            merged.facts.knee = other.facts.knee;
440            merged.facts.pad_end = other.facts.pad_end;
441            merged.facts.read = other.facts.read;
442        }
443        merged.facts.level = match (merged.facts.level, other.facts.level) {
444            (Some(a), Some(b)) => Some(if b.get() > a.get() { b } else { a }),
445            (a, b) => a.or(b),
446        };
447        merged.octave_uncertain = match (merged.octave_uncertain, other.octave_uncertain) {
448            (Some(a), Some(b)) => Some(a || b),
449            (a, b) => a.or(b),
450        };
451    }
452    merged.key = key_of(pitch);
453    // The drop is a route of the preset: its motion writes no bend.
454    merged.contour = None;
455    for &i in taken.iter().rev() {
456        work.remove(i);
457    }
458    work.insert(first.min(work.len()), merged);
459}
460
461/// Gives a note the key of a vibrato that covers most of it.
462fn follow_vibrato(note: &mut Work<'_>, vibrato: &[VibratoSpan]) {
463    let length = note.list_end - note.onset;
464    for span in vibrato {
465        let covered = note.list_end.min(span.end.get()) - note.onset.max(span.start.get());
466        if covered > 0.5 * length
467            && let Some(pitch) = pitch_of(span.pitch)
468            && (pitch - f64::from(note.key)).abs() <= 1.0
469        {
470            note.key = key_of(pitch);
471        }
472    }
473}
474
475/// Keeps one of the notes of a mono line that start on one tick, writes
476/// the glide joins and ends every other note where the next starts.
477/// Returns the list spans of the notes that went.
478fn join_mono(
479    notes: &mut Vec<(FinalNote, Option<&Contour>)>,
480    input: &FinishInput<'_>,
481    tps: f64,
482) -> Vec<(f64, f64)> {
483    let dropped = keep_one_per_tick(notes);
484    let glides = input.glides;
485    let overlap = cast::ceil_to_usize(GLIDE_OVERLAP.get() * tps)
486        .and_then(|t| u64::try_from(t).ok())
487        .unwrap_or(1)
488        .max(1);
489    let tick = |seconds: f64| tick_of(seconds, tps);
490    let mut joined = vec![false; notes.len()];
491    for glide in glides {
492        let (from, to) = (
493            glide.start.get() - GLIDE_SLACK.get(),
494            glide.start.get() + glide.time.get() + GLIDE_SLACK.get(),
495        );
496        let at = tick(glide.start.get());
497        let best = (0..notes.len().saturating_sub(1))
498            .filter(|&i| !joined[i])
499            .filter(|&i| {
500                let (a, b) = (&notes[i].0, &notes[i + 1].0);
501                let onset = b.list_onset.get();
502                let interval = f64::from(b.key) - f64::from(a.key);
503                (from..=to).contains(&onset)
504                    && (interval - glide.interval.get()).abs() < 1.0
505                    && a.start < at.min(b.start)
506            })
507            .min_by(|&i, &j| {
508                let gap = |k: usize| (notes[k + 1].0.list_onset.get() - glide.start.get()).abs();
509                gap(i).total_cmp(&gap(j))
510            });
511        if let Some(i) = best {
512            // A glide that starts after the later onset leaves that onset.
513            let b = &mut notes[i + 1].0;
514            b.start = b.start.min(at);
515            let start = b.start;
516            let a = &mut notes[i].0;
517            a.end = start + overlap;
518            a.end_rule = EndRule::Glide;
519            joined[i] = true;
520        }
521    }
522    for i in 0..notes.len().saturating_sub(1) {
523        if joined[i] {
524            continue;
525        }
526        let next = notes[i + 1].0.start;
527        let a = &mut notes[i].0;
528        if a.end > next {
529            a.end = next;
530            a.end_rule = EndRule::NextNote;
531        }
532    }
533    dropped
534}
535
536/// Of the notes sorted by start, then key, that start on one tick, keeps a
537/// note without the octave flag before one with it, then the longer, then
538/// the lower key. Levels cannot rank them: twins read the same mix from the
539/// same start, so their levels differ only where the longer peaks later.
540/// Returns the list spans of the notes that went.
541fn keep_one_per_tick(notes: &mut Vec<(FinalNote, Option<&Contour>)>) -> Vec<(f64, f64)> {
542    let mut kept: Vec<(FinalNote, Option<&Contour>)> = Vec::with_capacity(notes.len());
543    let mut dropped = Vec::new();
544    for group in notes.chunk_by(|(a, _), (b, _)| a.start == b.start) {
545        let rank = |n: &FinalNote| (n.octave_uncertain != Some(true), n.end - n.start);
546        // The first of the best, so a tie keeps the lower key.
547        let best = (1..group.len()).fold(0, |best, i| {
548            if rank(&group[i].0) > rank(&group[best].0) {
549                i
550            } else {
551                best
552            }
553        });
554        for (i, item) in group.iter().enumerate() {
555            if i == best {
556                kept.push(*item);
557            } else {
558                dropped.push((item.0.list_onset.get(), item.0.list_end.get()));
559            }
560        }
561    }
562    *notes = kept;
563    dropped
564}
565
566/// Sets each velocity from the note's level under the loudest.
567fn level_velocities(notes: &mut [FinalNote]) {
568    let Some(loudest) = notes
569        .iter()
570        .filter_map(|n| n.level.map(Decibels::get))
571        .reduce(f64::max)
572    else {
573        return;
574    };
575    for note in notes {
576        note.velocity = note.level.map_or(DEFAULT_VELOCITY, |level| {
577            let value = 127.0 * 10f64.powf((level.get() - loudest) / VELOCITY_LAW_DB);
578            cast::round_to_usize(value.clamp(1.0, 127.0))
579                .and_then(|v| u8::try_from(v).ok())
580                .and_then(|v| Velocity::new(v).ok())
581                .unwrap_or(DEFAULT_VELOCITY)
582        });
583    }
584}
585
586/// The time step of the transcriber's note times, one model frame.
587fn model_frame() -> f64 {
588    profile::frame_ms() / 1000.0
589}
590
591/// How far the f0 track reads around a frame: from a quarter frame before
592/// its time to one period of `f0` after it. A frame at `t` reads the
593/// audio from `t - before` to `t + after`, and its audio time, where the
594/// pitch layer puts its records, is `t` less the lag `before - after / 2`.
595fn track_reach(f0: Hz) -> (f64, f64) {
596    let before = cast::usize_to_f64(TRACK_FRAME / 4) / MODEL_RATE.hz().get();
597    (before, 1.0 / f0.get().max(f64::MIN_POSITIVE))
598}
599
600/// The bends of the motion no route models.
601///
602/// A contour frame counts only when everything it reads lies inside its
603/// note and outside every other note of the list: the frames of a note
604/// change read the note before, and while two notes sound the one-voice
605/// f0 track follows only one of them. A note dropped for starting on the
606/// same tick still sounds in the stem, so its list span counts too. The
607/// note's span is its list onset and end, each one model frame inward, the
608/// time step of the transcriber's notes.
609fn bends_of(
610    notes: &[(FinalNote, Option<&Contour>)],
611    dropped: &[(f64, f64)],
612    input: &FinishInput<'_>,
613    tps: f64,
614) -> Vec<(u64, u16)> {
615    let modelled = |t: f64| {
616        input
617            .glides
618            .iter()
619            .any(|g| (g.start.get()..=g.start.get() + g.time.get()).contains(&t))
620            || input
621                .drops
622                .iter()
623                .any(|d| (d.onset.get()..=d.settled.get()).contains(&t))
624            || input
625                .vibrato
626                .iter()
627                .any(|v| (v.start.get()..=v.end.get()).contains(&t))
628    };
629    let mut events: Vec<(u64, u16)> = Vec::new();
630    for (i, (note, contour)) in notes.iter().enumerate() {
631        let Some(contour) = contour else { continue };
632        // A bend stops where the next note starts, so it never bends that
633        // note.
634        let stop = notes
635            .get(i + 1)
636            .map_or(note.end, |(next, _)| note.end.min(next.start));
637        let (before, after) = track_reach(Hz::from(MidiPitch::clamped(f64::from(note.key))));
638        let (onset, end) = (
639            note.list_onset.get() + model_frame(),
640            note.list_end.get() - model_frame(),
641        );
642        let apart = |(onset, end): (f64, f64), (from, to): (f64, f64)| to <= onset || end <= from;
643        let alone = |from: f64, to: f64| {
644            notes.iter().enumerate().all(|(j, (other, _))| {
645                j == i || apart((other.list_onset.get(), other.list_end.get()), (from, to))
646            }) && dropped.iter().all(|&span| apart(span, (from, to)))
647        };
648        // Each kept frame: its tick, its offset from the key and its track
649        // time.
650        let points: Vec<(u64, f64, f64)> = contour
651            .points()
652            .iter()
653            .filter(|p| {
654                let (from, to) = (p.time.get() - before, p.time.get() + after);
655                from >= onset && to <= end && alone(from, to)
656            })
657            .filter_map(|p| {
658                let audio = p.time.get() - (before - 0.5 * after);
659                let at = tick_of(audio, tps);
660                let deviation = pitch_of(p.f0)? - f64::from(note.key);
661                (!modelled(audio) && (note.start..stop).contains(&at)).then_some((
662                    at,
663                    deviation,
664                    p.time.get(),
665                ))
666            })
667            .collect();
668        if !persists(&points, before + after) {
669            continue;
670        }
671        for (at, deviation, _) in points {
672            let share = deviation.clamp(-BEND_RANGE.get(), BEND_RANGE.get()) / BEND_RANGE.get();
673            let value = cast::round_to_usize(
674                (f64::from(PITCH_BEND_CENTRE)
675                    + share * f64::from(MAX_PITCH_BEND - PITCH_BEND_CENTRE))
676                .max(0.0),
677            )
678            .and_then(|v| u16::try_from(v).ok())
679            .unwrap_or(PITCH_BEND_CENTRE)
680            .min(MAX_PITCH_BEND);
681            events.push((at, value));
682        }
683        events.push((stop, PITCH_BEND_CENTRE));
684    }
685    in_force(events)
686}
687
688/// Whether the kept frames hold an offset of [`BEND_MIN`] or more, of one
689/// sign, over adjacent track frames that span `reach`, the tracker's reach
690/// of the note: a quarter frame plus one period. A shorter excursion fits
691/// inside what one frame reads, so jitter and a single event, such as a
692/// hit inside a merged note, cannot tell it from motion.
693fn persists(points: &[(u64, f64, f64)], reach: f64) -> bool {
694    let hop = cast::usize_to_f64(TRACK_HOP) / MODEL_RATE.hz().get();
695    // (first time, last time, above the key) of the run of offset frames.
696    let mut run: Option<(f64, f64, bool)> = None;
697    for &(_, deviation, time) in points {
698        if deviation.abs() < BEND_MIN.get() {
699            run = None;
700            continue;
701        }
702        let sign = deviation > 0.0;
703        // Adjacent frames lie one hop apart; a missing frame ends the run.
704        let first = match run {
705            Some((first, last, s)) if s == sign && time - last <= 1.5 * hop => first,
706            _ => time,
707        };
708        if time - first >= reach {
709            return true;
710        }
711        run = Some((first, time, sign));
712    }
713    false
714}
715
716/// The bends that change the value in force: of the bends on one tick the
717/// last added wins, and one equal to the value before it is left out. A
718/// file starts centred.
719fn in_force(mut events: Vec<(u64, u16)>) -> Vec<(u64, u16)> {
720    // Stable, so the bends of one tick keep their order.
721    events.sort_by_key(|&(at, _)| at);
722    let mut bends: Vec<(u64, u16)> = Vec::new();
723    for (i, &(at, value)) in events.iter().enumerate() {
724        if events.get(i + 1).is_some_and(|&(next, _)| next == at) {
725            continue;
726        }
727        if bends.last().map_or(PITCH_BEND_CENTRE, |&(_, v)| v) != value {
728            bends.push((at, value));
729        }
730    }
731    bends
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737    use crate::notes::{ContourPoint, Note};
738    use midly::{MidiMessage, Smf, TrackEventKind};
739    use proptest::prelude::*;
740
741    fn s(value: f64) -> Seconds {
742        Seconds::new(value).unwrap()
743    }
744
745    fn note(onset: f64, offset: f64, key: f64) -> Note {
746        Note::new(s(onset), s(offset), MidiPitch::new(key).unwrap()).unwrap()
747    }
748
749    fn knee(at: f64) -> NoteFacts {
750        NoteFacts {
751            knee: Some(s(at)),
752            read: true,
753            ..NoteFacts::default()
754        }
755    }
756
757    /// A note the envelope layer read without a knee.
758    fn kneeless() -> NoteFacts {
759        NoteFacts {
760            read: true,
761            ..NoteFacts::default()
762        }
763    }
764
765    fn bpm(value: f64) -> Bpm {
766        Bpm::new(value).unwrap()
767    }
768
769    fn input<'a>(notes: &'a NoteList, facts: &'a [NoteFacts], tempo: f64) -> FinishInput<'a> {
770        FinishInput {
771            notes,
772            facts,
773            bpm: bpm(tempo),
774            mono: true,
775            level_velocity: false,
776            glides: &[],
777            drops: &[],
778            vibrato: &[],
779            level: &|_, _, _| None,
780        }
781    }
782
783    /// The notes of a file as (start, end, key, velocity) ticks, and its
784    /// bends as (tick, value).
785    #[expect(clippy::type_complexity, reason = "a test reader")]
786    fn parse(bytes: &[u8]) -> (Vec<(u64, u64, u8, u8)>, Vec<(u64, u16)>) {
787        let smf = Smf::parse(bytes).unwrap();
788        let (mut now, mut open, mut notes, mut bends) = (0_u64, Vec::new(), Vec::new(), Vec::new());
789        for event in &smf.tracks[0] {
790            now += u64::from(event.delta.as_int());
791            if let TrackEventKind::Midi { message, .. } = event.kind {
792                match message {
793                    MidiMessage::NoteOn { key, vel } => {
794                        open.push((now, key.as_int(), vel.as_int()));
795                    }
796                    MidiMessage::NoteOff { key, .. } => {
797                        let i = open.iter().position(|o| o.1 == key.as_int()).unwrap();
798                        let (start, key, vel) = open.remove(i);
799                        notes.push((start, now, key, vel));
800                    }
801                    MidiMessage::PitchBend { bend } => bends.push((now, bend.0.as_int())),
802                    _ => {}
803                }
804            }
805        }
806        notes.sort_unstable();
807        (notes, bends)
808    }
809
810    #[test]
811    fn a_known_line_writes_its_glide_and_pluck_at_fixed_ticks() {
812        // 120 BPM: 960 ticks per second. A glides into B from 0.95 s over
813        // 100 ms; C is a pluck with no knee.
814        let notes = NoteList::new(vec![
815            note(0.5, 1.0, 45.0).with_octave_uncertain(false),
816            note(1.0, 1.6, 50.0).with_octave_uncertain(true),
817            note(2.0, 2.6, 47.0).with_octave_uncertain(false),
818        ]);
819        let facts = [knee(0.9), knee(1.5), kneeless()];
820        let glides = [GlideSpan {
821            start: s(0.95),
822            interval: Semitones::new(5.0).unwrap(),
823            time: s(0.1),
824        }];
825        let finished = finish(&FinishInput {
826            glides: &glides,
827            ..input(&notes, &facts, 120.0)
828        });
829        let rows: Vec<_> = finished
830            .notes
831            .iter()
832            .map(|n| (n.start, n.end, n.key, n.end_rule, n.octave_uncertain))
833            .collect();
834        assert_eq!(
835            rows,
836            [
837                // 912 + ceil(0.005 s * 960) = 912 + 5.
838                (480, 917, 45, EndRule::Glide, Some(false)),
839                (912, 1440, 50, EndRule::Knee, Some(true)),
840                // 2.6 s - 50 ms.
841                (1920, 2448, 47, EndRule::NoKnee, Some(false)),
842            ]
843        );
844        let (parsed, bends) = parse(&finished.to_midi(Some("line.wav")).unwrap());
845        assert_eq!(
846            parsed,
847            [
848                (480, 917, 45, 100),
849                (912, 1440, 50, 100),
850                (1920, 2448, 47, 100)
851            ]
852        );
853        assert_eq!(bends, []);
854    }
855
856    #[test]
857    fn velocities_follow_the_level_only_with_a_path_to_it() {
858        let notes = NoteList::new(vec![note(0.0, 0.5, 40.0), note(1.0, 1.5, 40.0)]);
859        let level = |db| NoteFacts {
860            level: Some(Decibels::new(db).unwrap()),
861            ..knee(10.0)
862        };
863        let facts = [level(-10.0), level(-16.0)];
864        let velocities = |level_velocity| {
865            finish(&FinishInput {
866                level_velocity,
867                ..input(&notes, &facts, 120.0)
868            })
869            .notes
870            .iter()
871            .map(|n| n.velocity.get())
872            .collect::<Vec<_>>()
873        };
874        // 127 * 10^(-6 / 40) = 89.9.
875        assert_eq!(velocities(true), [127, 90]);
876        assert_eq!(velocities(false), [100, 100]);
877    }
878
879    #[test]
880    fn a_pitch_drop_leaves_one_note_at_f_end() {
881        let notes = NoteList::new(vec![
882            note(1.0, 1.04, 43.0),
883            note(1.03, 1.6, 42.0),
884            note(2.0, 2.5, 45.0),
885        ]);
886        // The drop starts 3 semitones up, so the 45 after it stays.
887        let f_end = Hz::from(MidiPitch::new(41.0).unwrap());
888        let drops = [DropSpan {
889            onset: s(1.0),
890            f_end,
891            df_hz: f_end.get() * (2f64.powf(0.25) - 1.0),
892            settled: s(2.5),
893        }];
894        let finished = finish(&FinishInput {
895            drops: &drops,
896            ..input(&notes, &[], 120.0)
897        });
898        let keys: Vec<_> = finished
899            .notes
900            .iter()
901            .map(|n| (n.key, n.list_onset, n.list_end))
902            .collect();
903        assert_eq!(keys, [(41, s(1.0), s(1.6)), (45, s(2.0), s(2.5))]);
904    }
905
906    #[test]
907    fn a_vibrato_gives_its_mean_key() {
908        let notes = NoteList::new(vec![note(0.0, 1.0, 61.0)]);
909        let vibrato = [VibratoSpan {
910            start: s(0.1),
911            end: s(0.9),
912            pitch: Hz::from(MidiPitch::new(60.2).unwrap()),
913        }];
914        let finished = finish(&FinishInput {
915            vibrato: &vibrato,
916            ..input(&notes, &[], 120.0)
917        });
918        assert_eq!(finished.notes[0].key, 60);
919    }
920
921    #[test]
922    fn a_pad_note_ends_at_its_pad_end() {
923        let notes = NoteList::new(vec![note(0.0, 2.0, 60.0), note(0.0, 2.0, 64.0)]);
924        let facts = [
925            NoteFacts {
926                pad_end: Some(s(1.5)),
927                ..knee(1.0)
928            },
929            knee(1.0),
930        ];
931        let finished = finish(&FinishInput {
932            mono: false,
933            ..input(&notes, &facts, 120.0)
934        });
935        let ends: Vec<_> = finished.notes.iter().map(|n| (n.end, n.end_rule)).collect();
936        assert_eq!(ends, [(1440, EndRule::Pad), (960, EndRule::Knee)]);
937        // An unread note of a polyphonic line ends 50 ms before its list
938        // end, as on a mono line, where a pad end counts for nothing.
939        let facts = [
940            NoteFacts {
941                pad_end: Some(s(1.5)),
942                ..knee(1.0)
943            },
944            NoteFacts::default(),
945        ];
946        let finished = finish(&FinishInput {
947            mono: false,
948            ..input(&notes, &facts, 120.0)
949        });
950        let ends: Vec<_> = finished.notes.iter().map(|n| (n.end, n.end_rule)).collect();
951        assert_eq!(ends, [(1440, EndRule::Pad), (1872, EndRule::Unread)]);
952        let one = NoteList::new(vec![note(0.0, 2.0, 60.0)]);
953        let finished = finish(&input(&one, &facts[..1], 120.0));
954        let ends: Vec<_> = finished.notes.iter().map(|n| (n.end, n.end_rule)).collect();
955        assert_eq!(ends, [(960, EndRule::Knee)]);
956    }
957
958    /// The track's frame step.
959    fn hop() -> f64 {
960        cast::usize_to_f64(TRACK_HOP) / MODEL_RATE.hz().get()
961    }
962
963    /// A contour on the track's frames that holds the key in the note's
964    /// first half and `cents` off it in the second.
965    fn bent(onset: f64, offset: f64, key: f64, cents: f64) -> Note {
966        let f0 = |c: f64| Hz::from(MidiPitch::new(key + c / 100.0).unwrap());
967        let middle = f64::midpoint(onset, offset);
968        let n = cast::floor_to_usize((offset - onset) / hop()).unwrap_or(0);
969        let points = (0..n)
970            .map(|i| {
971                let t = onset + hop() * cast::usize_to_f64(i);
972                ContourPoint {
973                    time: s(t),
974                    f0: f0(if t >= middle { cents } else { 0.0 }),
975                }
976            })
977            .collect();
978        note(onset, offset, key).with_contour(Contour::new(points).unwrap())
979    }
980
981    #[test]
982    fn only_unmodelled_motion_of_a_mono_line_writes_bends() {
983        let notes = NoteList::new(vec![bent(0.0, 1.0, 50.0, 300.0)]);
984        let facts = [knee(0.95)];
985        let on = input(&notes, &facts, 120.0);
986        let chords = FinishInput { mono: false, ..on };
987        assert_eq!(finish(&chords).bends, []);
988        let (_, bends) = parse(&finish(&on).to_midi(None).unwrap());
989        // The first frame off the key is frame 87 at 0.5050 s of track time,
990        // 0.4852 s of audio after the lag of 19.8 ms at 146.8 Hz, tick 466.
991        // Three semitones clamp to the top of the 2-semitone range, and the
992        // note's end centres the bend again.
993        assert_eq!(bends, [(466, 16_383), (912, 8192)]);
994        let glides = [GlideSpan {
995            start: s(0.0),
996            interval: Semitones::new(3.0).unwrap(),
997            time: s(1.0),
998        }];
999        let bends = finish(&FinishInput {
1000            glides: &glides,
1001            ..on
1002        })
1003        .bends;
1004        assert_eq!(bends, []);
1005        let small = NoteList::new(vec![bent(0.0, 1.0, 50.0, 30.0)]);
1006        let bends = finish(&FinishInput {
1007            notes: &small,
1008            ..on
1009        })
1010        .bends;
1011        assert_eq!(bends, []);
1012    }
1013
1014    #[test]
1015    fn the_frames_of_a_note_change_write_no_bend() {
1016        // The track still reads the note before over its first quarter
1017        // frame: a contour that starts 2 semitones low there, then holds the
1018        // key, bends nothing.
1019        let key = 45.0;
1020        let points = (0..25)
1021            .map(|i| {
1022                let t = 1.0 + 0.01 * f64::from(i);
1023                let cents = if t < 1.02 { -200.0 } else { 0.0 };
1024                ContourPoint {
1025                    time: s(t),
1026                    f0: Hz::from(MidiPitch::new(key + cents / 100.0).unwrap()),
1027                }
1028            })
1029            .collect();
1030        let notes = NoteList::new(vec![
1031            note(0.75, 1.0, 43.0),
1032            note(1.0, 1.25, key).with_contour(Contour::new(points).unwrap()),
1033        ]);
1034        let facts = [kneeless(), kneeless()];
1035        assert_eq!(finish(&input(&notes, &facts, 120.0)).bends, []);
1036    }
1037
1038    /// A 49 Hz note (key 31) whose track jitters within ±35 cents, with a
1039    /// spike past 50 cents shorter than the tracker's reach every 0.1 s,
1040    /// then, when `held` is set, a bend of +60 cents from 0.6 to 0.9 s.
1041    fn sub_line(held: bool) -> Note {
1042        let reach_frames = cast::floor_to_usize(
1043            (cast::usize_to_f64(TRACK_FRAME / 4) / MODEL_RATE.hz().get() + 1.0 / 49.0) / hop(),
1044        )
1045        .unwrap();
1046        let n = cast::floor_to_usize(1.0 / hop()).unwrap();
1047        let points = (0..n)
1048            .map(|i| {
1049                let t = 0.1 + hop() * cast::usize_to_f64(i);
1050                let jitter = 35.0 * (cast::usize_to_f64(i) * 1.7).sin();
1051                // A spike of half the reach, as at a hit inside a merged note.
1052                let spike = if i % 17 < reach_frames / 2 {
1053                    70.0
1054                } else {
1055                    jitter
1056                };
1057                let cents = if held && (0.6..0.9).contains(&t) {
1058                    60.0
1059                } else {
1060                    spike
1061                };
1062                ContourPoint {
1063                    time: s(t),
1064                    f0: Hz::from(MidiPitch::new(31.0 + cents / 100.0).unwrap()),
1065                }
1066            })
1067            .collect();
1068        note(0.1, 1.1, 31.0).with_contour(Contour::new(points).unwrap())
1069    }
1070
1071    #[test]
1072    fn a_steady_sub_line_with_jitter_writes_no_bend() {
1073        let notes = NoteList::new(vec![sub_line(false)]);
1074        let facts = [kneeless()];
1075        assert_eq!(finish(&input(&notes, &facts, 120.0)).bends, []);
1076    }
1077
1078    #[test]
1079    fn a_held_bend_of_60_cents_writes_bends() {
1080        let notes = NoteList::new(vec![sub_line(true)]);
1081        let facts = [kneeless()];
1082        let bends = finish(&input(&notes, &facts, 120.0)).bends;
1083        // +60 cents of a 2-semitone range: 8192 + 0.3 * 8191.
1084        let at = |t: f64| in_force_at(&bends, tick_of(t, 960.0)).1;
1085        assert_eq!(at(0.75), 10_649);
1086        assert_eq!(at(1.2), PITCH_BEND_CENTRE);
1087    }
1088
1089    #[test]
1090    fn a_note_inside_another_reads_no_bend_from_the_shared_track() {
1091        // An octave ghost inside a held note: the one-voice track reads the
1092        // held note, 12 semitones under the ghost, so neither note bends.
1093        let held = contoured(0.5, 1.5, 47, 0.0);
1094        let points = (0..6)
1095            .map(|i| ContourPoint {
1096                time: s(0.9 + 0.01 * f64::from(i)),
1097                f0: Hz::from(MidiPitch::new(47.0).unwrap()),
1098            })
1099            .collect();
1100        let ghost = note(0.9, 0.96, 59.0).with_contour(Contour::new(points).unwrap());
1101        let notes = NoteList::new(vec![held, ghost]);
1102        let facts = [kneeless(), kneeless()];
1103        let finished = finish(&FinishInput {
1104            mono: true,
1105            ..input(&notes, &facts, 120.0)
1106        });
1107        assert_eq!(finished.bends, []);
1108    }
1109
1110    #[test]
1111    fn a_glide_after_the_later_onset_keeps_that_onset() {
1112        // The later note starts at 1.0 s and the glide at 1.04 s: the note
1113        // keeps 1.0 s, 960 ticks, and the earlier one ends 5 ticks later.
1114        let notes = NoteList::new(vec![note(0.5, 1.0, 45.0), note(1.0, 1.08, 50.0)]);
1115        let glides = [GlideSpan {
1116            start: s(1.04),
1117            interval: Semitones::new(5.0).unwrap(),
1118            time: s(0.03),
1119        }];
1120        let facts = [kneeless(), kneeless()];
1121        let finished = finish(&FinishInput {
1122            glides: &glides,
1123            ..input(&notes, &facts, 120.0)
1124        });
1125        let spans: Vec<_> = finished.notes.iter().map(|n| (n.start, n.end)).collect();
1126        assert_eq!(spans, [(480, 965), (960, 1016)]);
1127    }
1128
1129    #[test]
1130    fn of_two_notes_on_one_tick_a_mono_line_keeps_the_longer() {
1131        let notes = NoteList::new(vec![note(1.0, 1.5, 40.0), note(1.0, 1.4, 52.0)]);
1132        // Levels do not rank twins: the louder of the facts still goes.
1133        let loud = |db| NoteFacts {
1134            level: Some(Decibels::new(db).unwrap()),
1135            ..kneeless()
1136        };
1137        let facts = [loud(-20.0), loud(-6.0)];
1138        let finished = finish(&input(&notes, &facts, 120.0));
1139        assert_eq!(finished.dropped, 1);
1140        assert_eq!(finished.notes.len(), 1);
1141        assert_eq!(finished.notes[0].key, 40);
1142        // Equal lengths keep the lower key.
1143        let notes = NoteList::new(vec![note(1.0, 1.5, 52.0), note(1.0, 1.5, 40.0)]);
1144        assert_eq!(finish(&input(&notes, &facts, 120.0)).notes[0].key, 40);
1145    }
1146
1147    #[test]
1148    fn of_two_notes_on_one_tick_the_one_without_an_octave_flag_stays() {
1149        let notes = NoteList::new(vec![
1150            note(1.0, 1.5, 40.0).with_octave_uncertain(true),
1151            note(1.0, 1.4, 52.0).with_octave_uncertain(false),
1152        ]);
1153        let facts = [kneeless(), kneeless()];
1154        let finished = finish(&input(&notes, &facts, 120.0));
1155        assert_eq!(finished.notes.len(), 1);
1156        assert_eq!(finished.notes[0].key, 52);
1157    }
1158
1159    #[test]
1160    fn a_missing_level_of_a_mono_line_reads_no_further_than_the_next_onset() {
1161        // A short quiet note without a level, 5 ms before a loud one: its
1162        // written span runs to the minimum, past the next onset, but its
1163        // level stops at that onset.
1164        let notes = NoteList::new(vec![note(2.0, 2.04, 50.0), note(2.005, 2.8, 52.0)]);
1165        let facts = [
1166            kneeless(),
1167            NoteFacts {
1168                level: Decibels::new(-6.0).ok(),
1169                ..kneeless()
1170            },
1171        ];
1172        let next = tick_of(2.005, ticks_per_second(bpm(120.0)));
1173        let reader = |from: Seconds, to: Seconds, _| {
1174            assert_eq!(from, s(2.0));
1175            assert!(
1176                to <= seconds_of(next, ticks_per_second(bpm(120.0))),
1177                "{to:?}"
1178            );
1179            Decibels::new(-24.0).ok()
1180        };
1181        let finished = finish(&FinishInput {
1182            level: &reader,
1183            level_velocity: true,
1184            ..input(&notes, &facts, 120.0)
1185        });
1186        // 127 * 10^(-18 / 40) = 45.0.
1187        let velocities: Vec<_> = finished.notes.iter().map(|n| n.velocity.get()).collect();
1188        assert_eq!(velocities, [45, 127]);
1189        // On a polyphonic line the span is the written one.
1190        let reader = |_, to: Seconds, _| {
1191            assert!(to > s(2.03), "{to:?}");
1192            Decibels::new(-24.0).ok()
1193        };
1194        let finished = finish(&FinishInput {
1195            level: &reader,
1196            mono: false,
1197            ..input(&notes, &facts, 120.0)
1198        });
1199        assert_eq!(finished.notes.len(), 2);
1200    }
1201
1202    #[test]
1203    fn a_dropped_twin_still_keeps_its_span_from_the_kept_note_s_bends() {
1204        // The track follows the dropped twin at 52 for the kept note's whole
1205        // length; the twin still sounds, so those frames bend nothing.
1206        let n = cast::floor_to_usize(0.4 / hop()).unwrap();
1207        let points = (0..n)
1208            .map(|i| ContourPoint {
1209                time: s(1.0 + hop() * cast::usize_to_f64(i)),
1210                f0: Hz::from(MidiPitch::new(52.0).unwrap()),
1211            })
1212            .collect();
1213        let kept = note(1.0, 1.4, 40.0).with_contour(Contour::new(points).unwrap());
1214        let ghost = note(1.0, 1.5, 52.0).with_octave_uncertain(true);
1215        let notes = NoteList::new(vec![kept, ghost]);
1216        let facts = [kneeless(), kneeless()];
1217        let finished = finish(&input(&notes, &facts, 120.0));
1218        assert_eq!((finished.notes.len(), finished.notes[0].key), (1, 40));
1219        assert_eq!(finished.bends, []);
1220    }
1221
1222    #[test]
1223    fn a_merged_drop_takes_its_read_flag_with_its_knee() {
1224        // The later note ends the merged one and the envelope skipped it, so
1225        // the merged note is unread, not without a knee.
1226        let notes = NoteList::new(vec![note(1.0, 1.04, 43.0), note(1.03, 1.6, 42.0)]);
1227        let facts = [kneeless(), NoteFacts::default()];
1228        let f_end = Hz::from(MidiPitch::new(41.0).unwrap());
1229        let drops = [DropSpan {
1230            onset: s(1.0),
1231            f_end,
1232            df_hz: f_end.get() * (2f64.powf(0.25) - 1.0),
1233            settled: s(2.5),
1234        }];
1235        let finished = finish(&FinishInput {
1236            drops: &drops,
1237            ..input(&notes, &facts, 120.0)
1238        });
1239        let rules: Vec<_> = finished.notes.iter().map(|n| n.end_rule).collect();
1240        assert_eq!(rules, [EndRule::Unread]);
1241        // And the other way round.
1242        let facts = [NoteFacts::default(), kneeless()];
1243        let finished = finish(&FinishInput {
1244            drops: &drops,
1245            ..input(&notes, &facts, 120.0)
1246        });
1247        let rules: Vec<_> = finished.notes.iter().map(|n| n.end_rule).collect();
1248        assert_eq!(rules, [EndRule::NoKnee]);
1249    }
1250
1251    #[test]
1252    fn a_note_no_rule_read_is_unread_and_a_missing_level_is_filled() {
1253        let notes = NoteList::new(vec![note(0.0, 0.5, 40.0), note(1.0, 1.5, 40.0)]);
1254        let facts = [
1255            NoteFacts {
1256                level: Some(Decibels::new(-6.0).unwrap()),
1257                ..NoteFacts::default()
1258            },
1259            kneeless(),
1260        ];
1261        let finished = finish(&FinishInput {
1262            level_velocity: true,
1263            ..input(&notes, &facts, 120.0)
1264        });
1265        let rules: Vec<_> = finished.notes.iter().map(|n| n.end_rule).collect();
1266        assert_eq!(rules, [EndRule::Unread, EndRule::NoKnee]);
1267        assert_eq!(finished.notes[1].velocity, DEFAULT_VELOCITY);
1268        // The written span of the second note fills its level.
1269        let written = |from: Seconds, to: Seconds, _| {
1270            assert_eq!((from, to), (s(1.0), s(1.45)));
1271            Decibels::new(-12.0).ok()
1272        };
1273        let finished = finish(&FinishInput {
1274            level_velocity: true,
1275            level: &written,
1276            ..input(&notes, &facts, 120.0)
1277        });
1278        // 127 * 10^(-6 / 40) = 89.9.
1279        let velocities: Vec<_> = finished.notes.iter().map(|n| n.velocity.get()).collect();
1280        assert_eq!(velocities, [127, 90]);
1281    }
1282
1283    /// The last bend at or before a tick, which the file plays before a
1284    /// note-on of that tick, and its tick.
1285    fn in_force_at(bends: &[(u64, u16)], at: u64) -> (u64, u16) {
1286        bends
1287            .iter()
1288            .rev()
1289            .find(|&&(t, _)| t <= at)
1290            .map_or((0, PITCH_BEND_CENTRE), |&b| b)
1291    }
1292
1293    /// One note of a generated mono line: the gap to the next onset, the
1294    /// length, whether it overlaps the next note in the list, the key, the
1295    /// knee as a share of its length, whether the envelope read it, its
1296    /// level in dB, its contour's offset in cents, a drop on it, and a glide
1297    /// into the next note with its start against that onset.
1298    #[derive(Clone, Debug)]
1299    struct Spec {
1300        gap: f64,
1301        length: f64,
1302        overlaps: bool,
1303        key: u8,
1304        knee: Option<f64>,
1305        read: bool,
1306        level: Option<f64>,
1307        cents: f64,
1308        drop: bool,
1309        glide: Option<f64>,
1310    }
1311
1312    fn spec() -> impl Strategy<Value = Spec> {
1313        (
1314            (
1315                0.2f64..1.0,
1316                0.03f64..0.8,
1317                prop::bool::weighted(0.2),
1318                30u8..90,
1319            ),
1320            (
1321                prop::option::of(0.0f64..1.0),
1322                prop::bool::weighted(0.8),
1323                prop::option::of(-30.0f64..0.0),
1324            ),
1325            (
1326                prop::sample::select(vec![0.0, 30.0, -30.0, 80.0, -150.0]),
1327                prop::bool::weighted(0.1),
1328                prop::option::weighted(0.3, -0.04f64..0.04),
1329            ),
1330        )
1331            .prop_map(
1332                |((gap, length, overlaps, key), (knee, read, level), (cents, drop, glide))| Spec {
1333                    gap,
1334                    length,
1335                    overlaps,
1336                    key,
1337                    knee: knee.filter(|_| read),
1338                    read,
1339                    level,
1340                    cents,
1341                    drop,
1342                    glide,
1343                },
1344            )
1345    }
1346
1347    /// A contour on the track's frames over the note, `cents` off its key.
1348    fn contoured(onset: f64, offset: f64, key: u8, cents: f64) -> Note {
1349        let pitch = f64::from(key);
1350        let n = cast::floor_to_usize((offset - onset) / hop())
1351            .unwrap_or(0)
1352            .max(1);
1353        let points = (0..n)
1354            .map(|i| ContourPoint {
1355                time: s(onset + hop() * cast::usize_to_f64(i)),
1356                f0: Hz::from(MidiPitch::new(pitch + cents / 100.0).unwrap()),
1357            })
1358            .collect();
1359        note(onset, offset, pitch).with_contour(Contour::new(points).unwrap())
1360    }
1361
1362    proptest! {
1363        #[test]
1364        fn a_mono_line_keeps_its_rules(
1365            specs in prop::collection::vec(spec(), 2..12),
1366            tempo in 4.0f64..1000.0,
1367            level in proptest::bool::ANY,
1368        ) {
1369            let mut onsets = Vec::new();
1370            let mut onset = 0.1;
1371            for spec in &specs {
1372                onsets.push(onset);
1373                onset += spec.length.max(spec.gap) + 0.01;
1374            }
1375            let ends: Vec<f64> = specs
1376                .iter()
1377                .enumerate()
1378                .map(|(i, spec)| match onsets.get(i + 1) {
1379                    Some(&next) if spec.overlaps => next + 0.05,
1380                    _ => onsets[i] + spec.length,
1381                })
1382                .collect();
1383            let notes = NoteList::new(
1384                specs
1385                    .iter()
1386                    .enumerate()
1387                    .map(|(i, spec)| contoured(onsets[i], ends[i], spec.key, spec.cents))
1388                    .collect(),
1389            );
1390            let facts: Vec<NoteFacts> = specs
1391                .iter()
1392                .enumerate()
1393                .map(|(i, spec)| NoteFacts {
1394                    knee: spec.knee.map(|k| s(onsets[i] + k * (ends[i] - onsets[i]))),
1395                    pad_end: None,
1396                    level: spec.level.map(|l| Decibels::new(l).unwrap()),
1397                    read: spec.read,
1398                })
1399                .collect();
1400            let drops: Vec<DropSpan> = specs
1401                .iter()
1402                .enumerate()
1403                .filter(|(_, spec)| spec.drop)
1404                .map(|(i, spec)| {
1405                    // From a semitone above the key down to two below it.
1406                    let f_end = Hz::from(MidiPitch::new(f64::from(spec.key) - 2.0).unwrap());
1407                    DropSpan {
1408                        onset: s(onsets[i]),
1409                        f_end,
1410                        df_hz: f_end.get() * (2f64.powf(0.25) - 1.0),
1411                        settled: s(onsets[i] + 0.1),
1412                    }
1413                })
1414                .collect();
1415            let joins: Vec<usize> = (0..specs.len() - 1)
1416                .filter(|&i| {
1417                    specs[i].glide.is_some()
1418                        && !specs[i].drop
1419                        && !specs[i + 1].drop
1420                        && specs[i].key != specs[i + 1].key
1421                })
1422                .collect();
1423            let glides: Vec<GlideSpan> = joins
1424                .iter()
1425                .map(|&i| GlideSpan {
1426                    start: s(onsets[i + 1] + specs[i].glide.unwrap_or(0.0)),
1427                    interval: Semitones::new(
1428                        f64::from(specs[i + 1].key) - f64::from(specs[i].key) + 0.3,
1429                    )
1430                    .unwrap(),
1431                    time: s(0.08),
1432                })
1433                .collect();
1434            let finished = finish(&FinishInput {
1435                glides: &glides,
1436                drops: &drops,
1437                level_velocity: level,
1438                ..input(&notes, &facts, tempo)
1439            });
1440            let tps = ticks_per_second(bpm(tempo));
1441            let tick = 1.0 / tps;
1442            let minimum = min_note_length(bpm(tempo)).get();
1443            let list = &finished.notes;
1444            prop_assert_eq!(list.len(), specs.len());
1445            prop_assert_eq!(finished.dropped, 0);
1446            for (i, (n, spec)) in list.iter().zip(&specs).enumerate() {
1447                prop_assert!(n.end > n.start);
1448                prop_assert!((1..=127).contains(&n.velocity.get()));
1449                if !level || n.level.is_none() {
1450                    prop_assert_eq!(n.velocity, DEFAULT_VELOCITY);
1451                }
1452                prop_assert_eq!(n.key, if spec.drop { spec.key - 2 } else { spec.key });
1453                let glided = i.checked_sub(1).is_some_and(|p| list[p].end_rule == EndRule::Glide);
1454                let joined = n.end_rule == EndRule::Glide || glided;
1455                let own_start = tick_of(onsets[i], tps);
1456                if glided {
1457                    // The later note starts at the glide's start or its own
1458                    // onset, whichever comes first.
1459                    let at = tick_of(onsets[i] + specs[i - 1].glide.unwrap_or(0.0), tps);
1460                    prop_assert_eq!(n.start, own_start.min(at));
1461                } else {
1462                    prop_assert_eq!(n.start, own_start);
1463                }
1464                match n.end_rule {
1465                    EndRule::Glide => {
1466                        let next = &list[i + 1];
1467                        let overlap =
1468                            finished.seconds(n.end).get() - finished.seconds(next.start).get();
1469                        prop_assert!(overlap >= GLIDE_OVERLAP.get() - 1e-9, "{overlap}");
1470                    }
1471                    rule => {
1472                        prop_assert!(n.end <= tick_of(ends[i], tps));
1473                        if let Some(next) = list.get(i + 1) {
1474                            prop_assert!(n.end <= next.start);
1475                        }
1476                        let expected = if spec.knee.is_some() {
1477                            EndRule::Knee
1478                        } else if spec.read {
1479                            EndRule::NoKnee
1480                        } else {
1481                            EndRule::Unread
1482                        };
1483                        prop_assert!(
1484                            [expected, EndRule::Minimum, EndRule::NextNote].contains(&rule),
1485                            "{rule:?} for {spec:?}"
1486                        );
1487                    }
1488                }
1489                if !joined && n.end_rule != EndRule::NextNote {
1490                    let written = finished.seconds(n.end).get() - finished.seconds(n.start).get();
1491                    if ends[i] - onsets[i] >= minimum {
1492                        prop_assert!(written >= minimum - tick - 1e-9);
1493                    } else {
1494                        // A note the list gives shorter keeps its length.
1495                        prop_assert_eq!(n.end, tick_of(ends[i], tps).max(n.start + 1));
1496                    }
1497                }
1498                if n.end_rule == EndRule::NextNote {
1499                    // Only a list note that runs into the next is cut there.
1500                    prop_assert!(spec.overlaps);
1501                    prop_assert_eq!(n.end, list[i + 1].start);
1502                }
1503            }
1504            // Bends: never before a note's note-on, centred again before the
1505            // next, within range, and present for an unmodelled long offset.
1506            for (i, n) in list.iter().enumerate() {
1507                let spec = &specs[i];
1508                // At a note-on the bend is centred, unless it already holds
1509                // the note's own bend value, as when the note before bent
1510                // by as much.
1511                let (t, v) = in_force_at(&finished.bends, n.start);
1512                let own = cast::round_to_usize(
1513                    f64::from(PITCH_BEND_CENTRE) + (spec.cents / 100.0).clamp(-2.0, 2.0) / 2.0 * f64::from(MAX_PITCH_BEND - PITCH_BEND_CENTRE),
1514                )
1515                .and_then(|v| u16::try_from(v).ok());
1516                prop_assert!(
1517                    v == PITCH_BEND_CENTRE || (spec.cents.abs() >= 50.0 && Some(v) == own),
1518                    "note {i}: {v} at {t}"
1519                );
1520                // A glide into the note models its start, which can leave
1521                // less than the tracker's reach before its end.
1522                let glided = i.checked_sub(1).is_some_and(|p| list[p].end_rule == EndRule::Glide);
1523                let clear = !spec.drop
1524                    && !glided
1525                    && spec.cents.abs() >= 80.0
1526                    && ends[i] - onsets[i] >= 0.2
1527                    && finished.seconds(n.end).get() - onsets[i] >= 0.15;
1528                if clear {
1529                    // A bend of the note's own, or one already in force at
1530                    // its note-on with the same value.
1531                    let bent = in_force_at(&finished.bends, n.start).1 != PITCH_BEND_CENTRE
1532                        || finished
1533                            .bends
1534                            .iter()
1535                            .any(|&(t, v)| (n.start..n.end).contains(&t) && v != PITCH_BEND_CENTRE);
1536                    prop_assert!(bent, "note {i}: {spec:?}");
1537                }
1538                if spec.cents.abs() < 50.0 || spec.drop {
1539                    let stop = list.get(i + 1).map_or(n.end, |m| n.end.min(m.start));
1540                    prop_assert!(
1541                        finished
1542                            .bends
1543                            .iter()
1544                            .all(|&(t, v)| !(n.start..stop).contains(&t) || v == PITCH_BEND_CENTRE),
1545                        "note {i}: {spec:?}"
1546                    );
1547                }
1548            }
1549            prop_assert!(finished.bends.iter().all(|&(_, v)| v <= 16_383));
1550            prop_assert!(finished.bends.windows(2).all(|w| w[0].0 < w[1].0 && w[0].1 != w[1].1));
1551            let (parsed, bends) = parse(&finished.to_midi(None).unwrap());
1552            prop_assert_eq!(&bends, &finished.bends);
1553            let mut written: Vec<_> =
1554                list.iter().map(|n| (n.start, n.end, n.key, n.velocity.get())).collect();
1555            written.sort_unstable();
1556            prop_assert_eq!(parsed, written);
1557        }
1558
1559        #[test]
1560        fn a_polyphonic_line_keeps_its_rules(
1561            raw in prop::collection::vec(
1562                (
1563                    0.0f64..3.0,
1564                    0.03f64..1.5,
1565                    30u8..90,
1566                    prop::option::of(0.0f64..1.2),
1567                    prop::option::of(0.0f64..1.2),
1568                    prop::bool::weighted(0.7),
1569                ),
1570                1..12,
1571            ),
1572            tempo in 4.0f64..1000.0,
1573        ) {
1574            let notes = NoteList::new(
1575                raw.iter()
1576                    .map(|&(on, length, key, ..)| contoured(on, on + length, key, 120.0))
1577                    .collect(),
1578            );
1579            // The list sorts by onset, then pitch: read the facts in its order.
1580            let facts: Vec<NoteFacts> = notes
1581                .notes()
1582                .iter()
1583                .map(|n| {
1584                    let &(on, length, _, knee, pad_end, read) = raw
1585                        .iter()
1586                        .find(|r| s(r.0) == n.onset() && key_of(n.pitch().get()) == r.2)
1587                        .unwrap();
1588                    NoteFacts {
1589                        knee: knee.filter(|_| read).map(|k| s(on + k * length)),
1590                        pad_end: pad_end.map(|p| s(on + p * length)),
1591                        level: None,
1592                        read,
1593                    }
1594                })
1595                .collect();
1596            let finished = finish(&FinishInput {
1597                mono: false,
1598                ..input(&notes, &facts, tempo)
1599            });
1600            let tps = ticks_per_second(bpm(tempo));
1601            let minimum = min_note_length(bpm(tempo)).get();
1602            let list = &finished.notes;
1603            prop_assert_eq!(list.len(), notes.notes().len());
1604            prop_assert_eq!(&finished.bends, &Vec::new());
1605            for n in list {
1606                let (i, listed) = notes
1607                    .notes()
1608                    .iter()
1609                    .enumerate()
1610                    .find(|(_, l)| l.onset() == n.list_onset && key_of(l.pitch().get()) == n.key)
1611                    .unwrap();
1612                let f = facts[i];
1613                prop_assert!(n.end > n.start);
1614                prop_assert_eq!(n.velocity, DEFAULT_VELOCITY);
1615                let at_list_end = tick_of(listed.offset().get(), tps).max(n.start + 1);
1616                prop_assert!(n.end <= at_list_end);
1617                if n.end_rule == EndRule::Unread {
1618                    let cut = listed.offset().get() - NO_KNEE_CUT.get();
1619                    prop_assert_eq!(n.end, tick_of(cut, tps).max(n.start + 1));
1620                }
1621                let expected = if f.pad_end.is_some() {
1622                    EndRule::Pad
1623                } else if f.knee.is_some() {
1624                    EndRule::Knee
1625                } else if f.read {
1626                    EndRule::NoKnee
1627                } else {
1628                    EndRule::Unread
1629                };
1630                prop_assert!([expected, EndRule::Minimum].contains(&n.end_rule), "{:?}", n.end_rule);
1631                let length = listed.offset().get() - listed.onset().get();
1632                if length < minimum {
1633                    // A note the list gives shorter keeps its length.
1634                    prop_assert_eq!(n.end, at_list_end);
1635                } else {
1636                    let written = finished.seconds(n.end).get() - finished.seconds(n.start).get();
1637                    prop_assert!(written >= minimum - 1.0 / tps - 1e-9);
1638                }
1639            }
1640        }
1641    }
1642}