1use 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
22pub const GLIDE_OVERLAP: Seconds = Seconds::from_const(0.005);
25pub const NO_KNEE_CUT: Seconds = Seconds::from_const(0.050);
28pub const PAD_FALL: Decibels = Decibels::from_const(20.0);
30pub const GLIDE_SLACK: Seconds = Seconds::from_const(0.050);
33pub const DROP_SLACK: Seconds = Seconds::from_const(0.030);
36pub const BEND_MIN: Semitones = Semitones::from_const(0.5);
39pub const BEND_RANGE: Semitones = Semitones::from_const(2.0);
42pub const VELOCITY_LAW_DB: f64 = 40.0;
45
46#[derive(Clone, Copy, Debug, Default, PartialEq)]
48pub struct NoteFacts {
49 pub knee: Option<Seconds>,
51 pub pad_end: Option<Seconds>,
56 pub level: Option<Decibels>,
58 pub read: bool,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq)]
65pub struct GlideSpan {
66 pub start: Seconds,
68 pub interval: Semitones,
70 pub time: Seconds,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq)]
76pub struct DropSpan {
77 pub onset: Seconds,
79 pub f_end: Hz,
81 pub df_hz: f64,
83 pub settled: Seconds,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq)]
89pub struct VibratoSpan {
90 pub start: Seconds,
92 pub end: Seconds,
94 pub pitch: Hz,
96}
97
98pub type LevelReader<'a> = &'a dyn Fn(Seconds, Seconds, Hz) -> Option<Decibels>;
101
102#[derive(Clone, Copy)]
104pub struct FinishInput<'a> {
105 pub notes: &'a NoteList,
107 pub facts: &'a [NoteFacts],
110 pub bpm: Bpm,
112 pub mono: bool,
116 pub level_velocity: bool,
118 pub glides: &'a [GlideSpan],
120 pub drops: &'a [DropSpan],
122 pub vibrato: &'a [VibratoSpan],
124 pub level: LevelReader<'a>,
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum EndRule {
132 Knee,
134 NoKnee,
136 Pad,
138 Minimum,
140 NextNote,
142 Glide,
144 Unread,
148}
149
150impl EndRule {
151 #[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#[derive(Clone, Copy, Debug, PartialEq)]
168pub struct FinalNote {
169 pub start: u64,
171 pub end: u64,
173 pub key: u8,
175 pub velocity: Velocity,
177 pub end_rule: EndRule,
179 pub list_onset: Seconds,
181 pub list_end: Seconds,
183 pub level: Option<Decibels>,
185 pub octave_uncertain: Option<bool>,
187}
188
189#[derive(Clone, Debug, PartialEq)]
191pub struct FinalNotes {
192 pub notes: Vec<FinalNote>,
194 pub bends: Vec<(u64, u16)>,
196 pub bpm: Bpm,
198 pub level_velocity: bool,
200 pub mono: bool,
202 pub dropped: usize,
205}
206
207impl FinalNotes {
208 #[must_use]
210 pub fn seconds(&self, tick: u64) -> Seconds {
211 seconds_of(tick, ticks_per_second(self.bpm))
212 }
213
214 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#[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#[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 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 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(¬es, &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
379fn seconds_of(tick: u64, tps: f64) -> Seconds {
381 Seconds::clamped(cast::u64_to_f64(tick) / tps)
382}
383
384fn 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
391fn 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
398fn pitch_of(f0: Hz) -> Option<f64> {
400 MidiPitch::try_from(f0).ok().map(MidiPitch::get)
401}
402
403fn 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(¬e.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 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
461fn 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
475fn 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) = (¬es[i].0, ¬es[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 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
536fn 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 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
566fn 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
586fn model_frame() -> f64 {
588 profile::frame_ms() / 1000.0
589}
590
591fn 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
600fn 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 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 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
688fn persists(points: &[(u64, f64, f64)], reach: f64) -> bool {
694 let hop = cast::usize_to_f64(TRACK_HOP) / MODEL_RATE.hz().get();
695 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 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
716fn in_force(mut events: Vec<(u64, u16)>) -> Vec<(u64, u16)> {
720 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 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 #[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 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(¬es, &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 (480, 917, 45, EndRule::Glide, Some(false)),
839 (912, 1440, 50, EndRule::Knee, Some(true)),
840 (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(¬es, &facts, 120.0)
868 })
869 .notes
870 .iter()
871 .map(|n| n.velocity.get())
872 .collect::<Vec<_>>()
873 };
874 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 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(¬es, &[], 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(¬es, &[], 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(¬es, &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 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(¬es, &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 fn hop() -> f64 {
960 cast::usize_to_f64(TRACK_HOP) / MODEL_RATE.hz().get()
961 }
962
963 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(¬es, &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 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 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(¬es, &facts, 120.0)).bends, []);
1036 }
1037
1038 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 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(¬es, &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(¬es, &facts, 120.0)).bends;
1083 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 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(¬es, &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 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(¬es, &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 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(¬es, &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 let notes = NoteList::new(vec![note(1.0, 1.5, 52.0), note(1.0, 1.5, 40.0)]);
1144 assert_eq!(finish(&input(¬es, &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(¬es, &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 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(¬es, &facts, 120.0)
1185 });
1186 let velocities: Vec<_> = finished.notes.iter().map(|n| n.velocity.get()).collect();
1188 assert_eq!(velocities, [45, 127]);
1189 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(¬es, &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 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(¬es, &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 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(¬es, &facts, 120.0)
1238 });
1239 let rules: Vec<_> = finished.notes.iter().map(|n| n.end_rule).collect();
1240 assert_eq!(rules, [EndRule::Unread]);
1241 let facts = [NoteFacts::default(), kneeless()];
1243 let finished = finish(&FinishInput {
1244 drops: &drops,
1245 ..input(¬es, &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(¬es, &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 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(¬es, &facts, 120.0)
1277 });
1278 let velocities: Vec<_> = finished.notes.iter().map(|n| n.velocity.get()).collect();
1280 assert_eq!(velocities, [127, 90]);
1281 }
1282
1283 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 #[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 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 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(¬es, &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 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 prop_assert_eq!(n.end, tick_of(ends[i], tps).max(n.start + 1));
1496 }
1497 }
1498 if n.end_rule == EndRule::NextNote {
1499 prop_assert!(spec.overlaps);
1501 prop_assert_eq!(n.end, list[i + 1].start);
1502 }
1503 }
1504 for (i, n) in list.iter().enumerate() {
1507 let spec = &specs[i];
1508 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 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 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 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(¬es, &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 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}