1use super::{MODEL_RATE, ModelRun, Note, NoteList, PitchTrack, QUIET_FRAME, StemLevels};
16use crate::cast;
17use crate::units::Hz;
18use crate::units::{Decibels, MidiPitch, Seconds, Semitones};
19
20pub const OCTAVE_AGREEMENT: Semitones = Semitones::from_const(1.0);
22pub const OCTAVE_TOLERANCE: Semitones = Semitones::from_const(0.5);
24pub const OCTAVE_FIX_CEILING: MidiPitch = MidiPitch::from_const(45.0);
28pub const COPY_SLACK: Seconds = Seconds::from_const(2.0 * 256.0 / 22_050.0);
36pub const OCTAVE_MIN_FRAMES: usize = 3;
38pub const SPLIT_ONSET: f64 = 0.68;
44pub const SPLIT_GAP: Seconds = Seconds::from_const(2.0 * 256.0 / 22_050.0);
46pub const SPLIT_LOWEST: MidiPitch = MidiPitch::from_const(33.0);
56pub const ATTACK_BEFORE: Seconds = Seconds::from_const(0.075);
63pub const ATTACK_AFTER: Seconds = Seconds::from_const(0.05);
67pub const ATTACK_DIP: Decibels = Decibels::from_const(19.0);
75pub const FUNDAMENTAL_DIP: Decibels = Decibels::from_const(23.0);
91pub const HARMONIC_DIP: Decibels = Decibels::from_const(22.0);
99pub const RISE_WINDOW: Seconds = Seconds::from_const(0.004);
105pub const RISE_STEP: Seconds = Seconds::from_const(0.001);
107pub const RISE_SPAN: (f64, f64) = (0.1, 0.9);
110pub const RISE_LATEST: Seconds = Seconds::from_const(0.02);
113pub const RISE_READ: f64 = 3.0;
121pub const RISE_SLACK: Seconds = Seconds::from_const(0.007);
129pub const HOLD_BEFORE: Seconds = Seconds::from_const(0.06);
139pub const HOLD_SPAN: (f64, f64) = (0.5, 0.6);
145pub const HOLD_SEARCH: Seconds = Seconds::from_const(0.4);
157pub const HOLD_RATIO: f64 = 1.5;
170
171#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
173pub struct OctaveCheck {
174 pub moved: usize,
176 pub flagged: usize,
178 pub unchecked: usize,
180}
181
182#[must_use]
193pub fn check_octaves(notes: &NoteList, track: &PitchTrack) -> (NoteList, OctaveCheck) {
194 let mut check = OctaveCheck::default();
195 let checked = notes
196 .notes()
197 .iter()
198 .map(|note| {
199 let Some(median) = median_pitch(track, note) else {
200 check.unchecked += 1;
201 return note.clone();
202 };
203 let gap = note.pitch().get() - median;
204 if gap.abs() <= OCTAVE_AGREEMENT.get() {
205 return note.clone().with_octave_uncertain(false);
206 }
207 if (gap - 12.0).abs() <= OCTAVE_TOLERANCE.get()
208 && median <= OCTAVE_FIX_CEILING.get() + OCTAVE_TOLERANCE.get()
209 && let Some(lower) = lower_octave(notes, note)
210 && let Some(moved) = moved_down(note, lower)
211 {
212 check.moved += 1;
213 return moved;
214 }
215 check.flagged += 1;
216 note.clone().with_octave_uncertain(true)
217 })
218 .collect();
219 (NoteList::new(checked), check)
220}
221
222fn lower_octave<'a>(notes: &'a NoteList, note: &Note) -> Option<&'a Note> {
228 let slack = COPY_SLACK.get();
229 notes.notes().iter().find(|other| {
230 (note.pitch().get() - other.pitch().get() - 12.0).abs() < 0.5
231 && other.onset().get() <= note.onset().get() + slack
232 && note.offset().get() <= other.offset().get() + slack
233 && note
234 .velocity()
235 .zip(other.velocity())
236 .is_some_and(|(copy, lower)| copy < lower)
237 })
238}
239
240fn median_pitch(track: &PitchTrack, note: &Note) -> Option<f64> {
243 let mut pitches: Vec<f64> = track
244 .voiced_between(note.onset(), note.offset())
245 .filter_map(|(_, f0)| MidiPitch::try_from(f0).ok())
246 .map(MidiPitch::get)
247 .collect();
248 if pitches.len() < OCTAVE_MIN_FRAMES {
249 return None;
250 }
251 pitches.sort_by(f64::total_cmp);
252 let middle = pitches.len() / 2;
253 Some(if pitches.len().is_multiple_of(2) {
254 f64::midpoint(pitches[middle - 1], pitches[middle])
255 } else {
256 pitches[middle]
257 })
258}
259
260fn moved_down(note: &Note, lower: &Note) -> Option<Note> {
264 let mut moved = Note::new(lower.onset(), lower.offset(), lower.pitch())
265 .ok()?
266 .with_octave_uncertain(false);
267 if let Some(velocity) = note.velocity() {
268 moved = moved.with_velocity(velocity);
269 }
270 Some(moved)
271}
272
273#[must_use]
284pub fn merge_overlaps(notes: &NoteList) -> NoteList {
285 let mut merged: Vec<Note> = Vec::with_capacity(notes.notes().len());
286 for note in notes.notes() {
287 let same_key = merged
288 .iter_mut()
289 .rev()
290 .find(|kept| same_key(kept, note) && note.onset() < kept.offset());
291 match same_key {
292 Some(kept) => *kept = joined(kept, note),
293 None => merged.push(note.clone()),
294 }
295 }
296 NoteList::new(merged)
297}
298
299#[must_use]
303pub fn join_splits(notes: &NoteList, run: &ModelRun) -> NoteList {
304 join_splits_with(notes, run, &StemLevels::new(&[], MODEL_RATE))
305}
306
307#[must_use]
321pub fn join_splits_with(notes: &NoteList, run: &ModelRun, levels: &StemLevels<'_>) -> NoteList {
322 let mut joined_notes: Vec<Note> = Vec::with_capacity(notes.notes().len());
323 for note in notes.notes() {
324 let weak = run
325 .onset_strength(note.onset(), note.pitch())
326 .is_some_and(|strength| strength < SPLIT_ONSET);
327 let earlier = joined_notes.iter_mut().rev().find(|kept| {
328 same_key(kept, note)
329 && kept.offset() <= note.onset()
330 && note.onset().get() - kept.offset().get() <= SPLIT_GAP.get()
331 && note.velocity() <= kept.velocity()
332 });
333 match earlier {
334 Some(kept) if weak && !attacks(levels, kept, note) => {
335 *kept = joined(kept, note);
336 }
337 _ => joined_notes.push(note.clone()),
338 }
339 }
340 NoteList::new(joined_notes)
341}
342
343#[must_use]
367pub fn split_at_attacks(notes: &NoteList, levels: &StemLevels<'_>) -> NoteList {
368 let decoded = notes.notes();
369 let mut starts: Vec<Option<Seconds>> = vec![None; decoded.len()];
370 let mut pieces: Vec<Note> = Vec::with_capacity(decoded.len());
371 for (index, found) in decoded.iter().enumerate() {
372 let note = starts[index]
373 .and_then(|onset| piece(found, onset, found.offset()))
374 .unwrap_or_else(|| found.clone());
375 if note.pitch() < SPLIT_LOWEST {
376 pieces.push(note);
377 continue;
378 }
379 let frames = levels.frames_between(note.onset(), note.offset());
380 let reach = levels.frames_between(
381 note.onset(),
382 Seconds::clamped(note.offset().get() + ATTACK_AFTER.get()),
383 );
384 let first = frames.start.saturating_sub(1);
387 let cached: Vec<f64> = (first..=reach.end).map(|m| levels.ac_rms(m)).collect();
388 let level = |m: usize| cached[m - first];
389 let mut rest = note.clone();
390 let (mut seen, mut before) = (frames.start, 0.0_f64);
391 for frame in frames {
392 let hold_end = Seconds::clamped(levels.frame_time(frame).get() - HOLD_BEFORE.get());
393 while seen < levels.frames_between(rest.onset(), hold_end).end {
394 before = before.max(level(seen));
395 seen += 1;
396 }
397 let Some(onset) = split_point(levels, frame, note.pitch(), &level, before)
398 .filter(|onset| rest.offset().get() - onset.get() >= ATTACK_AFTER.get())
399 else {
400 continue;
401 };
402 if let (Some(earlier), Some(after)) = (
403 piece(&rest, rest.onset(), onset),
404 piece(&rest, onset, rest.offset()),
405 ) && attacks(levels, &earlier, &after)
406 {
407 pieces.push(earlier);
408 rest = after;
409 seen = levels.frames_between(rest.onset(), rest.offset()).start;
410 before = 0.0;
411 }
412 }
413 let next = (rest.onset() > note.onset())
414 .then(|| {
415 decoded[index + 1..]
416 .iter()
417 .position(|later| same_key(later, &rest) && later.onset() >= rest.offset())
418 })
419 .flatten()
420 .map(|offset| index + 1 + offset)
421 .filter(|&later| {
422 rest.offset().get() - rest.onset().get() <= 2.0 * ATTACK_AFTER.get()
423 && decoded[later].onset().get() - rest.offset().get() <= SPLIT_GAP.get()
424 && !attacks(levels, &rest, &decoded[later])
425 });
426 match next {
427 Some(later) => starts[later] = Some(rest.onset()),
428 None => pieces.push(rest),
429 }
430 }
431 NoteList::new(pieces)
432}
433
434fn split_point(
439 levels: &StemLevels<'_>,
440 frame: usize,
441 pitch: MidiPitch,
442 level: &dyn Fn(usize) -> f64,
443 before: f64,
444) -> Option<Seconds> {
445 let centre = levels.frame_time(frame);
446 let after = Seconds::clamped(centre.get() + ATTACK_AFTER.get());
447 let loud = levels
448 .frames_between(centre, after)
449 .map(level)
450 .fold(0.0, f64::max);
451 if frame == 0
454 || level(frame - 1) < level(frame)
455 || level(frame + 1) <= level(frame)
456 || 20.0 * (loud / level(frame)).log10() < ATTACK_DIP.get()
457 || before < HOLD_SPAN.1 * loud
458 {
459 return None;
460 }
461 let (from, to) = RISE_SPAN;
462 let step = RISE_STEP.get();
463 let start = centre.get() - QUIET_FRAME.get() / 2.0;
464 let last = cast::floor_to_usize((after.get() - start) / step)?;
465 let readings = readings(
466 levels,
467 (0..=last).map(|k| start + cast::usize_to_f64(k) * step),
468 rise_window(pitch),
469 );
470 let peak = peak_from(&readings, centre.get());
471 let dip_readings = cast::round_to_usize(QUIET_FRAME.get() / step)? + 1;
476 let trough = readings
477 .iter()
478 .take(dip_readings)
479 .enumerate()
480 .min_by(|a, b| a.1.1.total_cmp(&b.1.1))
481 .map(|(k, _)| k)?;
482 let rise = trough
483 + readings[trough..]
484 .iter()
485 .position(|&(_, level)| level >= to * peak)?;
486 let onset = readings[..rise]
487 .iter()
488 .rev()
489 .find(|&&(_, level)| level <= from * peak)
490 .map(|&(time, _)| time)?;
491 fundamental_dips(levels, &readings, onset, pitch).then(|| Seconds::clamped(onset))
492}
493
494fn fundamental_dips(
498 levels: &StemLevels<'_>,
499 readings: &[(f64, f64)],
500 onset: f64,
501 pitch: MidiPitch,
502) -> bool {
503 let level = |time: f64| levels.fundamental_level(Seconds::clamped(time), pitch);
504 let (low, high) = readings
505 .iter()
506 .fold((f64::INFINITY, 0.0_f64), |(low, high), &(time, _)| {
507 if time < onset {
508 (low.min(level(time)), high)
509 } else {
510 (low, high.max(level(time)))
511 }
512 });
513 20.0 * (high / low).log10() >= FUNDAMENTAL_DIP.get()
514}
515
516fn rise_window(pitch: MidiPitch) -> f64 {
519 (1.0 / Hz::from(pitch).get()).max(RISE_WINDOW.get())
520}
521
522fn readings(
524 levels: &StemLevels<'_>,
525 times: impl Iterator<Item = f64>,
526 window: f64,
527) -> Vec<(f64, f64)> {
528 times
529 .map(|time| {
530 (
531 time,
532 levels.envelope(Seconds::clamped(time), Seconds::clamped(window)),
533 )
534 })
535 .collect()
536}
537
538fn peak_from(readings: &[(f64, f64)], time: f64) -> f64 {
540 readings
541 .iter()
542 .filter(|(at, _)| *at >= time)
543 .map(|&(_, level)| level)
544 .fold(0.0, f64::max)
545}
546
547fn piece(note: &Note, onset: Seconds, offset: Seconds) -> Option<Note> {
550 let mut piece = Note::new(onset, offset, note.pitch()).ok()?;
551 if let Some(flag) = note.octave_uncertain() {
552 piece = piece.with_octave_uncertain(flag);
553 }
554 if let Some(velocity) = note.velocity() {
555 piece = piece.with_velocity(velocity);
556 }
557 Some(piece)
558}
559
560fn attacks(levels: &StemLevels<'_>, kept: &Note, note: &Note) -> bool {
571 let start =
572 Seconds::clamped((note.onset().get() - ATTACK_BEFORE.get()).max(kept.onset().get()));
573 let end = Seconds::clamped((note.onset().get() + ATTACK_AFTER.get()).min(note.offset().get()));
574 let quiet = levels.frames_between(start, note.onset());
575 let loud = levels.frames_between(note.onset(), end);
576 let dips = |level: &dyn Fn(usize) -> f64, dip: Decibels| {
577 let low = quiet.clone().map(level).fold(f64::INFINITY, f64::min);
578 let high = loud.clone().map(level).fold(0.0, f64::max);
579 20.0 * (high / low).log10() >= dip.get()
581 };
582 dips(&|m| levels.ac_rms(m), ATTACK_DIP)
583 && dips(&|m| levels.harmonic_level(m, note.pitch()), HARMONIC_DIP)
584 && !rises_slowly(levels, note, start)
585}
586
587fn rises_slowly(levels: &StemLevels<'_>, note: &Note, start: Seconds) -> bool {
602 let onset = note.onset().get();
603 let window = rise_window(note.pitch());
604 let (from, to) = RISE_SPAN;
605 let limit = (to * to - from * from) * window + RISE_SLACK.get();
606 let end = onset + RISE_LATEST.get() + RISE_READ * limit;
607 let step = RISE_STEP.get();
608 let first = ((start.get() - onset) / step).ceil();
609 let Some(last) = cast::floor_to_usize((end - onset) / step - first) else {
610 return true;
611 };
612 let readings = readings(
613 levels,
614 (0..=last).map(|k| onset + (first + cast::usize_to_f64(k)) * step),
615 window,
616 );
617 let peak = peak_from(&readings, onset);
618 let mut low = None;
619 let mut fastest: Option<f64> = None;
620 for &(time, level) in &readings {
621 if level <= from * peak && time <= onset + RISE_LATEST.get() {
622 low = Some(time);
623 }
624 if let Some(since) = low
625 && level >= to * peak
626 {
627 let rose = readings
628 .iter()
629 .find(|&&(after, level)| after > since && level >= HOLD_SPAN.1 * peak)
630 .map_or(time, |&(after, _)| after);
631 if held(levels, since, rose, peak, window) {
632 fastest = Some(fastest.map_or(time - since, |rise| rise.min(time - since)));
633 }
634 low = None;
635 }
636 }
637 fastest.is_none_or(|rise| rise > limit)
638}
639
640fn held(levels: &StemLevels<'_>, low: f64, rose: f64, peak: f64, window: f64) -> bool {
649 let (stay, end) = HOLD_SPAN;
650 let step = RISE_STEP.get();
651 let level = |time: f64| levels.envelope(Seconds::clamped(time), Seconds::clamped(window));
652 let back = |from: f64| (0..).map(move |k| from - cast::usize_to_f64(k) * step);
653 let Some(fell) = back(low)
654 .take_while(|&time| time >= 0.0 && time >= low - HOLD_SEARCH.get())
655 .find(|&time| level(time) >= end * peak)
656 else {
657 return low >= HOLD_SEARCH.get();
658 };
659 let hold = HOLD_BEFORE.get().max(HOLD_RATIO * (rose - fell));
660 fell >= hold
661 && back(fell)
662 .take_while(|&time| time > fell - hold)
663 .all(|time| level(time) >= stay * peak)
664}
665
666fn same_key(a: &Note, b: &Note) -> bool {
667 (a.pitch().get() - b.pitch().get()).abs() < 0.5
668}
669
670fn joined(first: &Note, second: &Note) -> Note {
673 let offset = if second.offset() > first.offset() {
674 second.offset()
675 } else {
676 first.offset()
677 };
678 let Some(note) = piece(first, first.onset(), offset) else {
679 return first.clone();
680 };
681 match first.velocity().max(second.velocity()) {
682 Some(velocity) => note.with_velocity(velocity),
683 None => note,
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use crate::audio::SampleRate;
691 use crate::notes::{TrackFrame, Velocity};
692 use crate::units::{Decibels, Hz};
693
694 fn s(value: f64) -> Seconds {
695 Seconds::new(value).unwrap()
696 }
697
698 fn note(onset: f64, offset: f64, pitch: f64) -> Note {
699 Note::new(s(onset), s(offset), MidiPitch::new(pitch).unwrap()).unwrap()
700 }
701
702 fn heard(onset: f64, offset: f64, pitch: f64, velocity: u8) -> Note {
704 note(onset, offset, pitch).with_velocity(Velocity::new(velocity).unwrap())
705 }
706
707 fn track_at(pitch: f64) -> PitchTrack {
709 PitchTrack::from_frames(
710 (0..400)
711 .map(|i| TrackFrame {
712 time: s(f64::from(i) * 0.005),
713 f0: Some(Hz::from(MidiPitch::new(pitch).unwrap())),
714 level: Decibels::new(-3.0).unwrap(),
715 })
716 .collect(),
717 )
718 }
719
720 fn pitches(list: &NoteList) -> Vec<f64> {
721 list.notes().iter().map(|n| n.pitch().get()).collect()
722 }
723
724 #[test]
725 fn a_note_an_octave_above_the_track_moves_down() {
726 let notes = NoteList::new(vec![heard(0.5, 1.0, 45.0, 90), heard(0.5, 0.6, 57.0, 37)]);
727 let (checked, check) = check_octaves(¬es, &track_at(45.0));
728 assert_eq!(pitches(&checked), vec![45.0, 45.0]);
729 assert_eq!(
730 check,
731 OctaveCheck {
732 moved: 1,
733 flagged: 0,
734 unchecked: 0
735 }
736 );
737 assert!(
738 checked
739 .notes()
740 .iter()
741 .all(|n| n.octave_uncertain() == Some(false))
742 );
743 let merged = merge_overlaps(&checked);
745 assert_eq!(
746 merged.notes(),
747 &[heard(0.5, 1.0, 45.0, 90).with_octave_uncertain(false)]
748 );
749 }
750
751 #[test]
755 fn an_octave_bass_keeps_its_true_upper_notes() {
756 let notes = NoteList::new(vec![
757 heard(0.755, 0.859, 45.0, 94),
758 heard(0.755, 0.871, 57.0, 46),
759 heard(0.859, 0.998, 45.0, 48),
760 heard(0.871, 0.998, 57.0, 100),
761 ]);
762 let (checked, check) = check_octaves(¬es, &track_at(45.0));
763 assert_eq!(check.moved, 1);
764 let merged = merge_overlaps(&checked);
765 let kept: Vec<(f64, f64)> = merged
766 .notes()
767 .iter()
768 .map(|n| (n.onset().get(), n.pitch().get()))
769 .collect();
770 assert_eq!(kept, vec![(0.755, 45.0), (0.859, 45.0), (0.871, 57.0)]);
772 }
773
774 #[test]
775 fn a_copy_moves_only_inside_its_lower_note() {
776 let moved = |lower: Note| {
777 let notes = NoteList::new(vec![lower, heard(0.5, 0.6, 57.0, 37)]);
778 check_octaves(¬es, &track_at(45.0)).1.moved
779 };
780 let slack = COPY_SLACK.get();
781 assert_eq!(moved(heard(0.5 + slack, 0.6 - slack, 45.0, 90)), 1);
782 assert_eq!(moved(heard(0.5 + slack + 1e-3, 1.0, 45.0, 90)), 0);
783 assert_eq!(moved(heard(0.4, 0.6 - slack - 1e-3, 45.0, 90)), 0);
784 assert_eq!(moved(heard(0.5, 1.0, 45.0, 37)), 0);
786 assert_eq!(moved(note(0.5, 1.0, 45.0)), 0);
787 }
788
789 #[test]
790 fn a_lone_note_an_octave_above_the_track_keeps_its_pitch() {
791 let notes = NoteList::new(vec![note(0.5, 1.0, 57.0), note(0.8, 1.2, 45.0)]);
792 let (checked, check) = check_octaves(¬es, &track_at(45.0));
793 assert_eq!(pitches(&checked), vec![57.0, 45.0]);
794 assert_eq!(checked.notes()[0].octave_uncertain(), Some(true));
795 assert_eq!((check.moved, check.flagged), (0, 1));
796 }
797
798 #[test]
799 fn a_note_three_semitones_off_keeps_its_pitch_and_the_flag() {
800 let notes = NoteList::new(vec![note(0.5, 1.0, 48.0)]);
801 let (checked, check) = check_octaves(¬es, &track_at(45.0));
802 assert_eq!(pitches(&checked), vec![48.0]);
803 assert_eq!(checked.notes()[0].octave_uncertain(), Some(true));
804 assert_eq!(check.flagged, 1);
805 }
806
807 #[test]
808 fn the_gap_rules_hold_at_their_bounds() {
809 let flag = |pitch: f64, track: f64| {
812 let notes = NoteList::new(vec![
813 heard(0.45, 1.0, pitch - 12.0, 90),
814 heard(0.5, 1.0, pitch, 40),
815 ]);
816 let (checked, _) = check_octaves(¬es, &track_at(track));
817 let last = checked.notes().last().unwrap();
818 (last.pitch().get(), last.octave_uncertain())
819 };
820 assert_eq!(flag(46.0, 45.0), (46.0, Some(false)));
822 assert_eq!(flag(46.0, 44.9), (46.0, Some(true)));
823 assert_eq!(flag(57.0, 45.5), (45.0, Some(false)));
825 assert_eq!(flag(57.0, 44.4), (57.0, Some(true)));
826 assert_eq!(flag(58.0, 46.0), (58.0, Some(true)));
828 assert_eq!(flag(33.0, 45.0), (33.0, Some(true)));
830 }
831
832 #[test]
833 fn an_even_count_of_frames_takes_the_middle_two() {
834 let track = PitchTrack::from_frames(
837 [44.0, 44.0, 46.0, 46.0]
838 .iter()
839 .enumerate()
840 .map(|(i, &pitch)| TrackFrame {
841 time: s(0.5 + 0.005 * f64::from(u32::try_from(i).unwrap())),
842 f0: Some(Hz::from(MidiPitch::new(pitch).unwrap())),
843 level: Decibels::new(-3.0).unwrap(),
844 })
845 .collect(),
846 );
847 let notes = NoteList::new(vec![heard(0.49, 1.0, 45.0, 90), heard(0.5, 0.6, 57.0, 37)]);
848 assert_eq!(check_octaves(¬es, &track).1.moved, 1);
849 }
850
851 #[test]
852 fn a_note_with_too_few_voiced_frames_stays_unchecked() {
853 for (frames, unchecked) in [(2, 1), (3, 0)] {
854 let track = PitchTrack::from_frames(
855 (0..frames)
856 .map(|i| TrackFrame {
857 time: s(0.5 + f64::from(i) * 0.005),
858 f0: Some(Hz::from(MidiPitch::new(45.0).unwrap())),
859 level: Decibels::new(-3.0).unwrap(),
860 })
861 .collect(),
862 );
863 let notes = NoteList::new(vec![note(0.5, 1.0, 57.0)]);
864 let (checked, check) = check_octaves(¬es, &track);
865 assert_eq!(check.unchecked, unchecked, "{frames} frames");
866 assert_eq!(
867 checked.notes()[0].octave_uncertain().is_none(),
868 unchecked == 1
869 );
870 }
871 }
872
873 fn run_with_onset(strength: f32) -> (ModelRun, impl Fn(usize) -> f64) {
876 run_with_onset_at(strength, 30)
877 }
878
879 fn run_with_onset_at(strength: f32, frame: usize) -> (ModelRun, impl Fn(usize) -> f64) {
882 use basic_pitch::{Posteriorgram, Posteriorgrams};
883 let (frames, bin) = (frame + 50, 45 - 21);
884 let mut onsets = Posteriorgram::zeros(frames, 88);
885 onsets.as_mut_slice()[frame * 88 + bin] = strength;
886 let grams = Posteriorgrams::new(
887 Posteriorgram::zeros(frames, 88),
888 onsets,
889 Posteriorgram::zeros(frames, 264),
890 )
891 .unwrap();
892 (
893 ModelRun::from_posteriorgrams(grams),
894 basic_pitch::notes::frame_to_time,
895 )
896 }
897
898 #[test]
901 fn the_decoder_gives_a_repeat_of_one_key_no_overlap() {
902 use basic_pitch::{Posteriorgram, Posteriorgrams};
903 let (frames, bin) = (80, 45 - 21);
904 let mut held = Posteriorgram::zeros(frames, 88);
905 let mut onsets = Posteriorgram::zeros(frames, 88);
906 for frame in 10..60 {
907 held.as_mut_slice()[frame * 88 + bin] = 0.8;
908 }
909 for frame in [10, 30] {
910 onsets.as_mut_slice()[frame * 88 + bin] = 0.9;
911 }
912 let run = ModelRun::from_posteriorgrams(
913 Posteriorgrams::new(held, onsets, Posteriorgram::zeros(frames, 264)).unwrap(),
914 );
915 let settings = crate::notes::Profile::Bass.settings(crate::units::Bpm::new(120.0).unwrap());
916 let decoded = run.decode(&settings).unwrap();
917 let a2: Vec<&Note> = decoded
918 .notes()
919 .iter()
920 .filter(|decoded| (decoded.pitch().get() - 45.0).abs() < 0.5)
921 .collect();
922 assert_eq!(a2.len(), 2);
923 assert!(a2[0].offset() <= a2[1].onset());
924 assert_eq!(
925 merge_overlaps(&decoded).notes().len(),
926 decoded.notes().len()
927 );
928 }
929
930 #[test]
931 fn the_onset_strength_reads_one_frame_either_side() {
932 let (run, time) = run_with_onset(0.9);
933 let a2 = MidiPitch::new(45.0).unwrap();
934 let at = |t| run.onset_strength(s(t), a2).unwrap();
935 assert!((at(time(30) + 0.01) - 0.9).abs() < 1e-6);
936 assert!(at(time(33)).abs() < 1e-6);
937 assert_eq!(
938 run.onset_strength(s(time(30)), MidiPitch::new(120.0).unwrap()),
939 None
940 );
941 }
942
943 #[test]
944 fn a_split_on_a_weak_onset_joins_and_a_repeat_stays() {
945 for (strength, expected) in [(0.6, 1), (0.67, 1), (0.69, 2), (0.9, 2)] {
946 let (run, time) = run_with_onset(strength);
947 let notes = NoteList::new(vec![
948 note(time(10), time(30), 45.0),
949 note(time(30), time(45), 45.0),
950 ]);
951 let joined = join_splits(¬es, &run);
952 assert_eq!(joined.notes().len(), expected, "onset {strength}");
953 if expected == 1 {
954 assert_eq!(joined.notes()[0], note(time(10), time(45), 45.0));
955 }
956 }
957 }
958
959 #[test]
960 fn a_split_joins_within_its_gap_and_only_as_a_tail() {
961 let (run, time) = run_with_onset(0.0);
962 let gap = SPLIT_GAP.get();
963 let count = |first: (f64, f64), second: (f64, f64)| {
964 let notes = NoteList::new(vec![
965 note(first.0, first.1, 45.0),
966 note(second.0, second.1, 45.0),
967 ]);
968 join_splits(¬es, &run).notes().len()
969 };
970 let (start, end) = (time(30), time(40));
971 assert_eq!(count((0.1, start - gap), (start, end)), 1);
972 assert_eq!(count((0.1, start - gap - 1e-3), (start, end)), 2);
973 assert_eq!(count((0.1, start - 0.023), (start, end)), 1);
975 assert_eq!(count((0.1, start - 0.024), (start, end)), 2);
976 }
977
978 #[test]
979 fn a_weaker_piece_never_takes_a_stronger_note() {
980 let (run, time) = run_with_onset(0.0);
981 let joined = |first: u8, second: u8| {
982 let notes = NoteList::new(vec![
983 heard(time(10), time(30), 45.0, first),
984 heard(time(30), time(60), 45.0, second),
985 ]);
986 join_splits(¬es, &run).notes().len()
987 };
988 assert_eq!(joined(90, 50), 1);
989 assert_eq!(joined(90, 90), 1);
990 assert_eq!(joined(40, 90), 2);
993 }
994
995 fn rate() -> SampleRate {
996 SampleRate::new(22_050).unwrap()
997 }
998
999 const LEAD_FRAMES: usize = 137;
1001
1002 const LEAD_SAMPLES: i32 = 35_072;
1006
1007 fn stem_at(f0: f64, onset: f64, level: impl Fn(f64) -> (f64, f64)) -> Vec<f64> {
1011 (-LEAD_SAMPLES..22_050)
1012 .map(|index| {
1013 let t = f64::from(index) / 22_050.0;
1014 let (note, high) = level(t - onset);
1015 note * (std::f64::consts::TAU * f0 * t).sin()
1016 + high * (std::f64::consts::TAU * 1500.0 * t).sin()
1017 })
1018 .collect()
1019 }
1020
1021 fn stem(onset: f64, level: impl Fn(f64) -> (f64, f64)) -> Vec<f64> {
1023 stem_at(110.0, onset, level)
1024 }
1025
1026 fn joined_at(pitch: f64, mono: &[f64]) -> usize {
1029 let (run, time) = run_with_onset_at(0.0, LEAD_FRAMES + 30);
1030 let notes = NoteList::new(vec![
1031 note(time(LEAD_FRAMES + 10), time(LEAD_FRAMES + 30), pitch),
1032 note(time(LEAD_FRAMES + 30), time(LEAD_FRAMES + 45), pitch),
1033 ]);
1034 join_splits_with(¬es, &run, &StemLevels::new(mono, rate()))
1035 .notes()
1036 .len()
1037 }
1038
1039 fn joined_a2(mono: &[f64]) -> usize {
1040 joined_at(45.0, mono)
1041 }
1042
1043 fn onset() -> f64 {
1044 basic_pitch::notes::frame_to_time(30)
1045 }
1046
1047 #[test]
1048 fn a_repeat_after_a_gap_of_silence_stays_apart() {
1049 let gap = stem(onset(), |t| {
1050 (if (-0.05..0.0).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1051 });
1052 assert_eq!(joined_a2(&gap), 2);
1053 assert_eq!(joined_a2(&stem(onset(), |_| (0.5, 0.0))), 1);
1054 assert_eq!(joined_a2(&vec![0.0; 22_050]), 1);
1056 }
1057
1058 #[test]
1059 fn the_stem_dips_by_attack_dip() {
1060 for (high, expected) in [(0.07, 2), (0.079, 1)] {
1064 let mono = stem(onset(), |t| {
1065 if t >= 0.0 {
1066 (0.5, high)
1067 } else if t > -0.01 {
1068 (0.0, 0.0)
1069 } else {
1070 (0.001, high)
1071 }
1072 });
1073 assert_eq!(joined_a2(&mono), expected, "1500 Hz at {high}");
1074 }
1075 }
1076
1077 #[test]
1078 fn the_harmonics_dip_by_harmonic_dip() {
1079 for (low, expected) in [(0.0334, 2), (0.0472, 1)] {
1082 let mono = stem(onset(), |t| if t < 0.0 { (low, 0.0) } else { (0.5, 0.5) });
1083 assert_eq!(joined_a2(&mono), expected, "A2 at {low} before");
1084 }
1085 }
1086
1087 #[test]
1088 fn a_dc_offset_counts_for_nothing() {
1089 let gap = |offset: f64| {
1090 stem(onset(), |t| {
1091 (if (-0.05..0.0).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1092 })
1093 .into_iter()
1094 .map(|x| x + offset)
1095 .collect::<Vec<_>>()
1096 };
1097 assert_eq!(joined_a2(&gap(0.2)), 2);
1098 }
1099
1100 #[test]
1101 fn a_slow_rise_is_no_attack() {
1102 for (ramp, expected) in [(0.013, 2), (0.014, 1)] {
1106 let mono = stem(onset(), |t| (0.5 * (t / ramp).clamp(0.0, 1.0), 0.0));
1107 assert_eq!(joined_a2(&mono), expected, "ramp {ramp} s");
1108 }
1109 }
1110
1111 #[test]
1112 fn a_high_note_reads_through_rise_window() {
1113 for (ramp, expected) in [(0.01, 2), (0.011, 1)] {
1116 let mono = stem_at(880.0, onset(), |t| (0.5 * (t / ramp).clamp(0.0, 1.0), 0.0));
1117 assert_eq!(joined_at(81.0, &mono), expected, "ramp {ramp} s");
1118 }
1119 }
1120
1121 #[test]
1122 fn a_rise_starts_by_rise_latest() {
1123 for (end, expected) in [(0.03, 2), (0.035, 1)] {
1127 let mono = stem(onset(), |t| {
1128 (if (-0.05..end).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1129 });
1130 assert_eq!(joined_a2(&mono), expected, "silence to {end} s");
1131 }
1132 }
1133
1134 #[test]
1135 fn a_low_note_reads_past_attack_after() {
1136 for (end, expected) in [(0.046, 2), (0.054, 1)] {
1140 let mono = stem_at(30.868, onset(), |t| {
1141 (if (-0.05..end).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1142 });
1143 assert_eq!(joined_at(23.0, &mono), expected, "silence to {end} s");
1144 }
1145 }
1146
1147 fn joined_without_lead(mono: &[f64]) -> usize {
1150 let (run, time) = run_with_onset(0.0);
1151 let notes = NoteList::new(vec![
1152 note(time(10), time(30), 45.0),
1153 note(time(30), time(45), 45.0),
1154 ]);
1155 let lead = usize::try_from(LEAD_SAMPLES).unwrap();
1156 join_splits_with(¬es, &run, &StemLevels::new(&mono[lead..], rate()))
1157 .notes()
1158 .len()
1159 }
1160
1161 fn note_before_gap(hold: f64, gap: f64) -> Vec<f64> {
1163 stem(onset(), |t| {
1164 let on = (-gap - hold..-gap).contains(&t) || t >= 0.0;
1165 (if on { 0.5 } else { 0.0 }, 0.0)
1166 })
1167 }
1168
1169 #[test]
1170 fn a_rise_needs_a_hold_before_its_gap() {
1171 for (hold, expected) in [(0.059, 2), (0.058, 1)] {
1176 assert_eq!(
1177 joined_a2(¬e_before_gap(hold, 0.03)),
1178 expected,
1179 "held {hold} s"
1180 );
1181 }
1182 }
1183
1184 #[test]
1185 fn a_hold_lasts_hold_ratio_times_the_dip() {
1186 for (hold, expected) in [(0.147, 2), (0.146, 1)] {
1191 assert_eq!(
1192 joined_a2(¬e_before_gap(hold, 0.1)),
1193 expected,
1194 "held {hold} s"
1195 );
1196 }
1197 }
1198
1199 #[test]
1200 fn a_hold_cut_by_the_stem_start_is_too_short() {
1201 let mono = stem(onset(), |t| {
1204 (if t < -0.15 || t >= 0.0 { 0.5 } else { 0.0 }, 0.0)
1205 });
1206 assert_eq!(joined_a2(&mono), 2);
1207 assert_eq!(joined_without_lead(&mono), 1);
1208 }
1209
1210 #[test]
1211 fn a_dip_cut_by_the_stem_start_is_no_gap() {
1212 let mono = stem(onset(), |t| (if t < 0.0 { 0.0 } else { 0.5 }, 0.0));
1215 assert_eq!(joined_a2(&mono), 2);
1216 assert_eq!(joined_without_lead(&mono), 1);
1217 }
1218
1219 #[test]
1220 fn the_rise_reads_rise_read_limits_past_rise_latest() {
1221 for (late, expected) in [(0.065, 1), (0.066, 2)] {
1227 let mono = stem(onset(), |t| {
1228 let level = if t < -0.05 {
1229 0.5
1230 } else if t < 0.0 {
1231 0.0
1232 } else if t < late {
1233 0.4
1234 } else {
1235 1.0
1236 };
1237 (level, 0.0)
1238 });
1239 assert_eq!(joined_a2(&mono), expected, "step to 1 at {late} s");
1240 }
1241 }
1242
1243 #[test]
1244 fn the_hold_stays_above_the_first_share_of_hold_span() {
1245 for (hold, expected) in [(0.055, 2), (0.054, 1)] {
1251 let mono = stem(onset(), |t| {
1252 let start = -0.09 - hold;
1253 let level = if t >= 0.0 {
1254 0.5
1255 } else if t >= -0.05 {
1256 0.0
1257 } else {
1258 0.5 * ((t - start) / 0.04).clamp(0.0, 1.0)
1259 };
1260 (level, 0.0)
1261 });
1262 assert_eq!(joined_a2(&mono), expected, "held {hold} s");
1263 }
1264 }
1265
1266 #[test]
1267 fn the_hold_ends_within_hold_search() {
1268 for (before, expected) in [(0.356, 1), (0.357, 2)] {
1275 let mono = stem(onset(), |t| {
1276 let level = if t >= 0.0 {
1277 0.5
1278 } else if t >= -0.05 {
1279 0.0
1280 } else if (-0.07 - before..-0.05 - before).contains(&t) {
1281 0.5
1282 } else {
1283 0.2
1284 };
1285 (level, 0.0)
1286 });
1287 assert_eq!(joined_a2(&mono), expected, "burst {before} s before");
1288 }
1289 }
1290
1291 #[test]
1292 fn the_fastest_rise_counts() {
1293 let mono = stem(onset(), |t| {
1296 let level = if t < -0.06 {
1297 0.4
1298 } else if t < -0.035 {
1299 0.0
1300 } else if t < -0.015 {
1301 0.4
1302 } else if t < 0.0 {
1303 0.0
1304 } else {
1305 0.4 * (t / 0.045).min(1.0)
1306 };
1307 (level, 0.0)
1308 });
1309 assert_eq!(joined_a2(&mono), 2);
1310 }
1311
1312 #[test]
1313 fn the_fastest_held_rise_counts() {
1314 let mono = stem_at(880.0, onset(), |t| {
1319 let level = if t < -0.09 {
1320 0.5
1321 } else if t < -0.06 {
1322 0.0
1323 } else if t < -0.04 {
1324 25.0 * (t + 0.06)
1325 } else if (0.013..0.019).contains(&t) {
1326 0.0
1327 } else {
1328 0.5
1329 };
1330 (level, 0.0)
1331 });
1332 assert_eq!(joined_at(81.0, &mono), 2);
1333 }
1334
1335 #[test]
1336 fn the_loudest_frame_lies_within_attack_after() {
1337 for (late, expected) in [(0.04, 1), (0.1, 2)] {
1340 let mono = stem(onset(), |t| {
1341 let level = if t < 0.0 {
1342 0.0
1343 } else if t < late {
1344 0.4
1345 } else {
1346 1.0
1347 };
1348 (level, 0.0)
1349 });
1350 assert_eq!(joined_a2(&mono), expected, "step to 1 at {late} s");
1351 }
1352 }
1353
1354 #[test]
1355 fn the_quiet_frames_lie_within_attack_before() {
1356 for (end, expected) in [(0.035, 2), (0.08, 1)] {
1358 let mono = stem(onset(), |t| {
1359 (
1360 if (-end - 0.03..-end).contains(&t) {
1361 0.0
1362 } else {
1363 0.5
1364 },
1365 0.0,
1366 )
1367 });
1368 assert_eq!(joined_a2(&mono), expected, "gap ending {end} s before");
1369 }
1370 }
1371
1372 #[test]
1373 fn a_note_at_either_end_of_the_stem_reads_only_frames_inside_it() {
1374 let (run, _) = run_with_onset(0.0);
1375 let mono = &stem(0.0, |_| (0.5, 0.0))[..11_025];
1376 let levels = StemLevels::new(mono, rate());
1377 for (first, second) in [((0.0, 0.01), (0.01, 0.2)), ((0.3, 0.497), (0.497, 0.6))] {
1378 let notes = NoteList::new(vec![
1379 note(first.0, first.1, 45.0),
1380 note(second.0, second.1, 45.0),
1381 ]);
1382 assert_eq!(join_splits_with(¬es, &run, &levels).notes().len(), 1);
1383 }
1384 }
1385
1386 #[test]
1387 fn the_quiet_before_the_held_note_never_counts() {
1388 let (run, time) = run_with_onset_at(0.0, LEAD_FRAMES + 30);
1389 let mono = stem(time(26), |t| (if t < 0.0 { 0.0 } else { 0.5 }, 0.0));
1392 let notes = NoteList::new(vec![
1393 note(time(LEAD_FRAMES + 26), time(LEAD_FRAMES + 30), 45.0),
1394 note(time(LEAD_FRAMES + 30), time(LEAD_FRAMES + 45), 45.0),
1395 ]);
1396 assert_eq!(
1397 join_splits_with(¬es, &run, &StemLevels::new(&mono, rate()))
1398 .notes()
1399 .len(),
1400 1
1401 );
1402 }
1403
1404 fn split_at(pitch: f64, mono: &[f64], from: f64, to: f64) -> Vec<f64> {
1408 let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1409 let notes = NoteList::new(vec![note(start + from, start + to, pitch)]);
1410 split_at_attacks(¬es, &StemLevels::new(mono, rate()))
1411 .notes()
1412 .iter()
1413 .map(|piece| piece.onset().get() - start)
1414 .collect()
1415 }
1416
1417 fn gap_at(f0: f64, before: f64) -> Vec<f64> {
1420 stem_at(f0, onset(), |t| {
1421 let level = if t < -0.05 {
1422 before
1423 } else if t < 0.0 {
1424 0.0
1425 } else {
1426 0.5
1427 };
1428 (level, 0.0)
1429 })
1430 }
1431
1432 #[test]
1433 fn a_note_splits_at_the_last_tenth_before_the_climb() {
1434 let pieces = split_at(45.0, &gap_at(110.0, 0.5), -0.3, 0.3);
1438 assert_eq!(pieces.len(), 2);
1439 assert!(
1440 (pieces[1] + 0.0053).abs() < 0.0005,
1441 "split at {}",
1442 pieces[1]
1443 );
1444 assert_eq!(
1445 split_at(45.0, &stem(onset(), |_| (0.5, 0.0)), -0.3, 0.3).len(),
1446 1
1447 );
1448 }
1449
1450 #[test]
1451 fn the_piece_after_a_split_lasts_attack_after() {
1452 for (end, expected) in [(0.0452, 2), (0.0442, 1)] {
1454 assert_eq!(
1455 split_at(45.0, &gap_at(110.0, 0.5), -0.3, end).len(),
1456 expected,
1457 "note to {end} s"
1458 );
1459 }
1460 }
1461
1462 #[test]
1463 fn the_piece_before_a_split_reaches_the_second_share_of_hold_span() {
1464 for (before, expected) in [(0.305, 2), (0.295, 1)] {
1468 assert_eq!(
1469 split_at(45.0, &gap_at(110.0, before), -0.3, 0.3).len(),
1470 expected,
1471 "{before} before the gap"
1472 );
1473 }
1474 }
1475
1476 #[test]
1477 fn the_piece_before_a_split_is_loud_hold_before_ahead_of_the_dip() {
1478 for (from, expected) in [(-0.075, 2), (-0.07, 1)] {
1482 assert_eq!(
1483 split_at(45.0, &gap_at(110.0, 0.5), from, 0.3).len(),
1484 expected,
1485 "note from {from} s"
1486 );
1487 }
1488 }
1489
1490 #[test]
1491 fn a_split_needs_an_attack() {
1492 for (ramp, expected) in [(0.012, 2), (0.013, 1)] {
1495 let mono = stem(onset(), |t| {
1496 let level = if t < -0.05 {
1497 0.5
1498 } else {
1499 0.5 * (t / ramp).clamp(0.0, 1.0)
1500 };
1501 (level, 0.0)
1502 });
1503 assert_eq!(
1504 split_at(45.0, &mono, -0.3, 0.3).len(),
1505 expected,
1506 "ramp {ramp} s"
1507 );
1508 }
1509 }
1510
1511 #[test]
1512 fn a_quieter_repeat_splits() {
1513 let mono = stem(onset(), |t| {
1517 let level = if t < -0.025 {
1518 1.0
1519 } else if t < 0.0 {
1520 0.0
1521 } else {
1522 0.5
1523 };
1524 (level, 0.0)
1525 });
1526 let pieces = split_at(45.0, &mono, -0.3, 0.3);
1527 assert_eq!(pieces.len(), 2);
1528 assert!(
1529 (pieces[1] + 0.0053).abs() < 0.0005,
1530 "split at {}",
1531 pieces[1]
1532 );
1533 }
1534
1535 fn harmonic_stem(level: impl Fn(f64) -> (f64, f64)) -> Vec<f64> {
1538 (-LEAD_SAMPLES..22_050)
1539 .map(|index| {
1540 let t = f64::from(index) / 22_050.0;
1541 let (low, high) = level(t - onset());
1542 low * (std::f64::consts::TAU * 110.0 * t).sin()
1543 + high * (std::f64::consts::TAU * 220.0 * t).sin()
1544 })
1545 .collect()
1546 }
1547
1548 #[test]
1549 fn the_fundamental_dips_by_fundamental_dip() {
1550 for (floor, expected) in [(0.0375, 2), (0.04, 1)] {
1556 let mono = harmonic_stem(|t| {
1557 if (-0.05..0.0).contains(&t) {
1558 (floor, 0.0)
1559 } else {
1560 (0.5, 2.0)
1561 }
1562 });
1563 assert_eq!(
1564 split_at(45.0, &mono, -0.3, 0.3).len(),
1565 expected,
1566 "A2 at {floor} in the gap"
1567 );
1568 }
1569 }
1570
1571 #[test]
1572 fn a_last_piece_starts_the_next_note_within_split_gap() {
1573 let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1577 let levels_mono = gap_at(110.0, 0.5);
1578 let levels = StemLevels::new(&levels_mono, rate());
1579 for (gap, expected) in [(0.023, 2), (0.024, 3)] {
1580 let notes = NoteList::new(vec![
1581 heard(start - 0.3, start + 0.058, 45.0, 80),
1582 heard(start + 0.058 + gap, start + 0.3, 45.0, 90).with_octave_uncertain(true),
1583 ]);
1584 let pieces = split_at_attacks(¬es, &levels);
1585 assert_eq!(pieces.notes().len(), expected, "gap {gap} s");
1586 let last = &pieces.notes()[expected - 1];
1587 if expected == 2 {
1588 assert!((last.onset().get() - start + 0.0053).abs() < 0.0005);
1589 assert_eq!(last.velocity(), Some(Velocity::new(90).unwrap()));
1590 assert_eq!(last.octave_uncertain(), Some(true));
1591 }
1592 }
1593 let steady = stem(onset(), |_| (0.5, 0.0));
1595 let notes = NoteList::new(vec![
1596 heard(start - 0.3, start + 0.058, 45.0, 80),
1597 heard(start + 0.07, start + 0.3, 45.0, 90),
1598 ]);
1599 assert_eq!(
1600 split_at_attacks(¬es, &StemLevels::new(&steady, rate()))
1601 .notes()
1602 .len(),
1603 2
1604 );
1605 let notes = NoteList::new(vec![
1607 heard(start - 0.3, start + 0.058, 45.0, 80),
1608 heard(start + 0.07, start + 0.3, 47.0, 90),
1609 ]);
1610 assert_eq!(split_at_attacks(¬es, &levels).notes().len(), 3);
1611 }
1612
1613 #[test]
1614 fn a_short_last_piece_stays_before_a_next_note_with_its_own_attack() {
1615 let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1621 let mono = stem(onset(), |t| {
1622 let gap = (-0.05..0.0).contains(&t) || (0.06..0.09).contains(&t);
1623 (if gap { 0.0 } else { 0.5 }, 0.0)
1624 });
1625 let notes = NoteList::new(vec![
1626 heard(start - 0.3, start + 0.085, 45.0, 80),
1627 heard(start + 0.095, start + 0.4, 45.0, 90),
1628 ]);
1629 let pieces = split_at_attacks(¬es, &StemLevels::new(&mono, rate()));
1630 let onsets: Vec<f64> = pieces
1631 .notes()
1632 .iter()
1633 .map(|piece| piece.onset().get() - start)
1634 .collect();
1635 assert_eq!(onsets.len(), 3, "{onsets:?}");
1636 assert!((onsets[1] + 0.0053).abs() < 0.0005, "{onsets:?}");
1637 assert!((onsets[2] - 0.095).abs() < 1e-9, "{onsets:?}");
1638 }
1639
1640 #[test]
1641 fn a_long_last_piece_stays_before_a_next_note_with_its_own_attack() {
1642 let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1647 let mono = stem(onset(), |t| {
1648 let gap = (-0.05..0.0).contains(&t) || (0.1..0.14).contains(&t);
1649 (if gap { 0.0 } else { 0.5 }, 0.0)
1650 });
1651 let notes = NoteList::new(vec![
1652 heard(start - 0.3, start + 0.125, 45.0, 80),
1653 heard(start + 0.145, start + 0.4, 45.0, 90),
1654 ]);
1655 let pieces = split_at_attacks(¬es, &StemLevels::new(&mono, rate()));
1656 let onsets: Vec<f64> = pieces
1657 .notes()
1658 .iter()
1659 .map(|piece| piece.onset().get() - start)
1660 .collect();
1661 assert_eq!(onsets.len(), 3, "{onsets:?}");
1662 assert!((onsets[1] + 0.0053).abs() < 0.0005, "{onsets:?}");
1663 assert!((onsets[2] - 0.145).abs() < 1e-9, "{onsets:?}");
1664 }
1665
1666 #[test]
1667 fn the_trough_comes_from_the_dip_frame_s_26_readings() {
1668 let frame = 100;
1676 let len = 44_100;
1677 let centre = StemLevels::new(&vec![0.0; len], rate())
1678 .frame_time(frame)
1679 .get();
1680 for (repeat, late) in [(0.01, true), (0.011, false)] {
1681 let mono: Vec<f64> = (0..len)
1682 .map(|index| {
1683 let t = f64::from(u32::try_from(index).unwrap()) / 22_050.0 - centre;
1684 let level = if (-0.02..0.0).contains(&t) {
1685 0.002
1686 } else if (repeat..repeat + 0.025).contains(&t) {
1687 0.0
1688 } else {
1689 0.5
1690 };
1691 level * (std::f64::consts::TAU * 440.0 * (t + centre)).sin()
1692 })
1693 .collect();
1694 let levels = StemLevels::new(&mono, rate());
1695 let level = |m: usize| if m == frame { 0.001 } else { 1.0 };
1696 let onset = split_point(&levels, frame, MidiPitch::new(69.0).unwrap(), &level, 1.0)
1697 .map(|onset| onset.get() - centre);
1698 let cut = onset.unwrap();
1699 if late {
1700 assert!(cut > repeat + 0.02, "repeat {repeat} s: cut {cut}");
1701 } else {
1702 assert!(cut < 0.0, "repeat {repeat} s: cut {cut}");
1703 }
1704 }
1705 }
1706
1707 #[test]
1708 fn the_climb_starts_in_the_dip_frame() {
1709 for floor in [0.002, 0.0] {
1714 let mono = stem(onset(), |t| {
1715 let level = if (0.0..0.035).contains(&t) {
1716 floor
1717 } else if (0.05..0.09).contains(&t) {
1718 0.0
1719 } else {
1720 0.5
1721 };
1722 (level, 0.0)
1723 });
1724 let onsets = split_at(45.0, &mono, -0.3, 0.3);
1725 assert_eq!(onsets.len(), 2, "floor {floor}: {onsets:?}");
1726 assert!(
1727 (onsets[1] - 0.03).abs() < 0.001,
1728 "floor {floor}: {onsets:?}"
1729 );
1730 }
1731 }
1732
1733 #[test]
1734 fn a_short_repeat_after_a_short_gap_takes_the_cut_piece() {
1735 let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1743 for (length, expected) in [(0.08, 2), (0.1, 3)] {
1744 let mono = stem(onset(), |t| {
1745 let open =
1746 t < -0.1 || (0.0..length).contains(&t) || (length + 0.015..0.45).contains(&t);
1747 (if open { 0.5 } else { 0.0 }, 0.0)
1748 });
1749 let notes = NoteList::new(vec![
1750 heard(start - 0.3, start + length, 45.0, 80),
1751 heard(start + length + 0.01, start + 0.45, 45.0, 90),
1752 ]);
1753 let pieces = split_at_attacks(¬es, &StemLevels::new(&mono, rate()));
1754 let onsets: Vec<f64> = pieces
1755 .notes()
1756 .iter()
1757 .map(|piece| piece.onset().get() - start)
1758 .collect();
1759 assert_eq!(onsets.len(), expected, "gate {length} s: {onsets:?}");
1760 let last = onsets[expected - 1];
1761 let onset = if expected == 2 {
1762 -0.0053
1763 } else {
1764 length + 0.01
1765 };
1766 assert!((last - onset).abs() < 0.0005, "gate {length} s: {onsets:?}");
1767 }
1768 }
1769
1770 #[test]
1771 fn a_last_piece_starts_the_next_note_up_to_twice_attack_after() {
1772 let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1775 let mono = gap_at(110.0, 0.5);
1776 let levels = StemLevels::new(&mono, rate());
1777 for (end, expected) in [(0.094, 2), (0.096, 3)] {
1778 let notes = NoteList::new(vec![
1779 heard(start - 0.3, start + end, 45.0, 80),
1780 heard(start + end + 0.02, start + 0.4, 45.0, 90),
1781 ]);
1782 let pieces = split_at_attacks(¬es, &levels);
1783 assert_eq!(pieces.notes().len(), expected, "end {end} s");
1784 }
1785 }
1786
1787 #[test]
1788 fn a_long_last_piece_stays_before_a_repeat_after_a_short_gap() {
1789 let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1795 for gap in [0.02, 0.015, 0.01] {
1796 let mono = stem(onset(), |t| {
1797 let open = t < -0.1 || (0.0..0.2).contains(&t) || (0.2 + gap..0.45).contains(&t);
1798 (if open { 0.5 } else { 0.0 }, 0.0)
1799 });
1800 let notes = NoteList::new(vec![
1801 heard(start - 0.3, start + 0.2, 45.0, 80),
1802 heard(start + 0.2 + gap - 0.005, start + 0.45, 45.0, 90),
1803 ]);
1804 let pieces = split_at_attacks(¬es, &StemLevels::new(&mono, rate()));
1805 let onsets: Vec<f64> = pieces
1806 .notes()
1807 .iter()
1808 .map(|piece| piece.onset().get() - start)
1809 .collect();
1810 assert_eq!(onsets.len(), 3, "gap {gap} s: {onsets:?}");
1811 assert!((onsets[2] - (0.195 + gap)).abs() < 1e-9, "{onsets:?}");
1812 }
1813 }
1814
1815 #[test]
1816 fn a_note_from_the_stem_start_reads_only_the_stem() {
1817 let lead = usize::try_from(LEAD_SAMPLES).unwrap();
1820 for mono in [
1821 stem(onset(), |_| (0.5, 0.0)),
1822 stem(0.0, |t| {
1823 (if (0.02..0.07).contains(&t) { 0.0 } else { 0.5 }, 0.0)
1824 }),
1825 ] {
1826 let levels = StemLevels::new(&mono[lead..], rate());
1827 let notes = NoteList::new(vec![note(0.0, 0.5, 45.0)]);
1828 assert_eq!(split_at_attacks(¬es, &levels).notes().len(), 1);
1829 }
1830 }
1831
1832 #[test]
1833 fn every_piece_keeps_the_velocity_and_the_octave_flag() {
1834 let start = f64::from(LEAD_SAMPLES) / 22_050.0 + onset();
1835 let mono = gap_at(110.0, 0.5);
1836 let notes = NoteList::new(vec![
1837 heard(start - 0.3, start + 0.3, 45.0, 77).with_octave_uncertain(true),
1838 ]);
1839 let pieces = split_at_attacks(¬es, &StemLevels::new(&mono, rate()));
1840 assert_eq!(pieces.notes().len(), 2);
1841 for piece in pieces.notes() {
1842 assert_eq!(piece.velocity(), Some(Velocity::new(77).unwrap()));
1843 assert_eq!(piece.octave_uncertain(), Some(true));
1844 }
1845 let [first, last] = pieces.notes() else {
1847 unreachable!()
1848 };
1849 assert_eq!(first.onset(), notes.notes()[0].onset());
1850 assert_eq!(first.offset(), last.onset());
1851 assert_eq!(last.offset(), notes.notes()[0].offset());
1852 }
1853
1854 #[test]
1855 fn the_rise_window_spans_a_period_and_at_least_rise_window() {
1856 let at = |pitch: f64| rise_window(MidiPitch::new(pitch).unwrap());
1857 assert!((at(45.0) - 1.0 / 110.0).abs() < 1e-12);
1858 assert_eq!(at(81.0), RISE_WINDOW.get());
1860 }
1861
1862 #[test]
1863 fn notes_split_from_split_lowest() {
1864 assert_eq!(split_at(33.0, &gap_at(55.0, 0.5), -0.3, 0.3).len(), 2);
1865 assert_eq!(split_at(32.0, &gap_at(51.913, 0.5), -0.3, 0.3).len(), 1);
1866 assert_eq!(split_at(33.0, &gap_at(51.913, 0.5), -0.3, 0.3).len(), 2);
1868 }
1869
1870 #[test]
1871 fn two_repeated_notes_with_a_clear_gap_stay_two() {
1872 let (run, time) = run_with_onset(0.0);
1873 let notes = NoteList::new(vec![
1874 note(time(10), time(25), 45.0),
1875 note(time(30), time(50), 45.0),
1876 ]);
1877 assert_eq!(join_splits(¬es, &run).notes().len(), 2);
1878 }
1879
1880 #[test]
1881 fn a_merged_note_takes_the_higher_velocity() {
1882 let notes = NoteList::new(vec![heard(0.0, 0.5, 45.0, 40), heard(0.2, 0.8, 45.0, 90)]);
1883 assert_eq!(merge_overlaps(¬es).notes(), &[heard(0.0, 0.8, 45.0, 90)]);
1884 }
1885
1886 #[test]
1887 fn notes_of_one_key_merge_only_when_they_overlap() {
1888 let notes = NoteList::new(vec![
1889 note(0.0, 0.5, 45.0),
1890 note(0.2, 0.8, 45.0),
1891 note(0.3, 0.4, 52.0),
1892 note(1.0, 1.5, 45.0),
1893 ]);
1894 assert_eq!(
1895 merge_overlaps(¬es).notes(),
1896 &[
1897 note(0.0, 0.8, 45.0),
1898 note(0.3, 0.4, 52.0),
1899 note(1.0, 1.5, 45.0)
1900 ]
1901 );
1902 }
1903}