1use crate::units::{Hz, MidiPitch, Seconds};
22
23mod cleanup;
24mod detect;
25mod finish;
26mod mask;
27mod midi;
28mod profile;
29mod pyin;
30mod quiet;
31mod stem;
32mod track;
33mod transcribe;
34mod write;
35pub(crate) use write::ticks_per_second;
36
37pub use cleanup::{
38 ATTACK_AFTER, ATTACK_BEFORE, ATTACK_DIP, COPY_SLACK, FUNDAMENTAL_DIP, HARMONIC_DIP,
39 HOLD_BEFORE, HOLD_RATIO, HOLD_SEARCH, HOLD_SPAN, OCTAVE_AGREEMENT, OCTAVE_FIX_CEILING,
40 OCTAVE_MIN_FRAMES, OCTAVE_TOLERANCE, OctaveCheck, RISE_LATEST, RISE_READ, RISE_SLACK,
41 RISE_SPAN, RISE_STEP, RISE_WINDOW, SPLIT_GAP, SPLIT_LOWEST, SPLIT_ONSET, check_octaves,
42 join_splits, join_splits_with, merge_overlaps, split_at_attacks,
43};
44pub use detect::{
45 CHORD_OVERLAP, CHORD_SHARE, Detection, HOLD_WINDOW, REFERENCE_PERCENTILE, REGISTER_MARGIN,
46 REGISTER_PERCENTILES, REGISTER_WINDOW, RegisterSource, Voicing, detect, detect_with_window,
47 probe_settings, voicing_of_notes,
48};
49pub use finish::{
50 BEND_MIN, BEND_RANGE, DROP_SLACK, DropSpan, EndRule, FinalNote, FinalNotes, FinishInput,
51 GLIDE_OVERLAP, GLIDE_SLACK, GlideSpan, LevelReader, NO_KNEE_CUT, NoteFacts, PAD_FALL,
52 VELOCITY_LAW_DB, VibratoSpan, finish,
53};
54pub use mask::{FrameMask, MaskParams, Masker, out_of_mask_fraction};
55pub use midi::{
56 BPM_TOLERANCE, DRUM_CHANNEL, MidiError, read_midi, read_midi_bytes, read_midi_near,
57};
58pub use profile::{
59 DEFAULT_MIN_NOTE_FRAMES, DecodeSettings, HIGHEST_MODEL_PITCH, LOWEST_MODEL_PITCH, PitchRange,
60 Profile, RangeError, min_note_frames, min_note_length,
61};
62pub use quiet::{HARMONICS, QUIET_FRAME, QUIET_GATE, StemLevels, drop_quiet, drop_quiet_with};
63#[doc(inline)]
64pub use sound_to_patch_core::notes::{NoteError, Velocity};
65pub use stem::{
66 CleanupSteps, NotesError, TRANSCRIBER_VERSION, Transcription, TranscriptionRecord, clean_up,
67 transcribe_stem,
68};
69pub use track::{
70 PitchTrack, TRACK_FMAX, TRACK_FMIN, TRACK_FRAME, TRACK_HOP, TrackError, TrackFrame,
71};
72pub use transcribe::{
73 Backend, MODEL_RATE, MODEL_SHA256, ModelRun, Tracker, TranscribeError, Transcriber,
74};
75pub use write::{
76 DEFAULT_VELOCITY, MIN_NOTE_LENGTH, MidiWriteError, TICKS_PER_BEAT, Velocities, midi_track_name,
77 write_midi, write_named_midi,
78};
79
80#[derive(Clone, Copy, Debug, PartialEq)]
82pub struct ContourPoint {
83 pub time: Seconds,
85 pub f0: Hz,
87}
88
89#[derive(Clone, Debug, PartialEq)]
93pub struct Contour {
94 points: Vec<ContourPoint>,
95}
96
97impl Contour {
98 pub fn new(mut points: Vec<ContourPoint>) -> Result<Self, NoteError> {
105 if points.is_empty() {
106 return Err(NoteError::EmptyContour);
107 }
108 if points.iter().any(|point| point.f0.get() <= 0.0) {
109 return Err(NoteError::ZeroF0);
110 }
111 points.sort_by(|a, b| a.time.get().total_cmp(&b.time.get()));
112 if points.windows(2).any(|pair| pair[0].time == pair[1].time) {
113 return Err(NoteError::DuplicateTime);
114 }
115 Ok(Self { points })
116 }
117
118 #[must_use]
120 pub fn points(&self) -> &[ContourPoint] {
121 &self.points
122 }
123
124 #[must_use]
126 pub fn f0_at(&self, time: Seconds) -> Hz {
127 let t = time.get();
128 let after = self.points.partition_point(|point| point.time.get() <= t);
129 match (
130 after.checked_sub(1).and_then(|i| self.points.get(i)),
131 self.points.get(after),
132 ) {
133 (Some(before), Some(next)) if before.time.get() < t => {
134 let span = next.time.get() - before.time.get();
135 let fraction = (t - before.time.get()) / span;
136 let log = before.f0.get().ln() * (1.0 - fraction) + next.f0.get().ln() * fraction;
137 Hz::clamped(log.exp())
138 }
139 (Some(only), _) | (None, Some(only)) => only.f0,
140 (None, None) => Hz::clamped(0.0),
141 }
142 }
143}
144
145#[derive(Clone, Debug, PartialEq)]
147pub struct Note {
148 onset: Seconds,
149 offset: Seconds,
150 pitch: MidiPitch,
151 contour: Option<Contour>,
152 octave_uncertain: Option<bool>,
153 velocity: Option<Velocity>,
154 overlap: Option<bool>,
155}
156
157impl Note {
158 pub fn new(onset: Seconds, offset: Seconds, pitch: MidiPitch) -> Result<Self, NoteError> {
164 if offset <= onset {
165 return Err(NoteError::OffsetNotAfterOnset { onset, offset });
166 }
167 Ok(Self {
168 onset,
169 offset,
170 pitch,
171 contour: None,
172 octave_uncertain: None,
173 velocity: None,
174 overlap: None,
175 })
176 }
177
178 #[must_use]
180 pub fn with_contour(self, contour: Contour) -> Self {
181 Self {
182 contour: Some(contour),
183 ..self
184 }
185 }
186
187 #[must_use]
189 pub fn with_octave_uncertain(self, flag: bool) -> Self {
190 Self {
191 octave_uncertain: Some(flag),
192 ..self
193 }
194 }
195
196 #[must_use]
198 pub fn with_velocity(self, velocity: Velocity) -> Self {
199 Self {
200 velocity: Some(velocity),
201 ..self
202 }
203 }
204
205 #[must_use]
207 pub fn with_overlap(self, flag: bool) -> Self {
208 Self {
209 overlap: Some(flag),
210 ..self
211 }
212 }
213
214 #[must_use]
216 pub fn onset(&self) -> Seconds {
217 self.onset
218 }
219
220 #[must_use]
222 pub fn offset(&self) -> Seconds {
223 self.offset
224 }
225
226 #[must_use]
228 pub fn pitch(&self) -> MidiPitch {
229 self.pitch
230 }
231
232 #[must_use]
234 pub fn contour(&self) -> Option<&Contour> {
235 self.contour.as_ref()
236 }
237
238 #[must_use]
240 pub fn octave_uncertain(&self) -> Option<bool> {
241 self.octave_uncertain
242 }
243
244 #[must_use]
246 pub fn velocity(&self) -> Option<Velocity> {
247 self.velocity
248 }
249
250 #[must_use]
252 pub fn overlap(&self) -> Option<bool> {
253 self.overlap
254 }
255
256 #[must_use]
259 pub fn sounds_at(&self, time: Seconds) -> bool {
260 self.onset <= time && time < self.offset
261 }
262
263 #[must_use]
266 pub fn f0_at(&self, time: Seconds) -> Hz {
267 self.contour
268 .as_ref()
269 .map_or_else(|| Hz::from(self.pitch), |contour| contour.f0_at(time))
270 }
271}
272
273#[derive(Clone, Debug, Default, PartialEq)]
275pub struct NoteList {
276 notes: Vec<Note>,
277}
278
279impl NoteList {
280 #[must_use]
282 pub fn new(mut notes: Vec<Note>) -> Self {
283 notes.sort_by(|a, b| {
284 a.onset
285 .get()
286 .total_cmp(&b.onset.get())
287 .then(a.pitch.get().total_cmp(&b.pitch.get()))
288 });
289 Self { notes }
290 }
291
292 #[must_use]
294 pub fn notes(&self) -> &[Note] {
295 &self.notes
296 }
297
298 #[must_use]
300 pub fn is_empty(&self) -> bool {
301 self.notes.is_empty()
302 }
303
304 #[must_use]
307 pub fn with_overlaps(self) -> Self {
308 let mut latest_offset = f64::NEG_INFINITY;
312 let flags: Vec<bool> = self
313 .notes
314 .iter()
315 .enumerate()
316 .map(|(i, note)| {
317 let before = latest_offset > note.onset.get();
318 let after = self
319 .notes
320 .get(i + 1)
321 .is_some_and(|next| next.onset < note.offset);
322 latest_offset = latest_offset.max(note.offset.get());
323 before || after
324 })
325 .collect();
326 Self {
327 notes: self
328 .notes
329 .into_iter()
330 .zip(flags)
331 .map(|(note, flag)| note.with_overlap(flag))
332 .collect(),
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 fn s(value: f64) -> Seconds {
342 Seconds::new(value).unwrap()
343 }
344
345 fn note(onset: f64, offset: f64, pitch: f64) -> Note {
346 Note::new(s(onset), s(offset), MidiPitch::new(pitch).unwrap()).unwrap()
347 }
348
349 proptest::proptest! {
350 #[test]
351 fn overlap_flags_match_every_pair(
352 spans in proptest::collection::vec((0u8..40, 1u8..10), 0..30)
353 ) {
354 let list = NoteList::new(
355 spans
356 .iter()
357 .map(|&(start, len)| {
358 let onset = f64::from(start) / 4.0;
359 note(onset, onset + f64::from(len) / 4.0, 60.0)
360 })
361 .collect(),
362 )
363 .with_overlaps();
364 let notes = list.notes();
365 for (i, a) in notes.iter().enumerate() {
366 let expected = notes.iter().enumerate().any(|(j, b)| {
367 i != j && b.onset() < a.offset() && a.onset() < b.offset()
368 });
369 proptest::prop_assert_eq!(a.overlap(), Some(expected));
370 }
371 }
372 }
373
374 #[test]
375 fn a_new_note_reads_its_optional_fields_as_unset() {
376 let note = note(0.0, 1.0, 45.0);
377 assert_eq!(note.octave_uncertain(), None);
378 assert_eq!(note.velocity(), None);
379 assert_eq!(note.overlap(), None);
380 assert_eq!(note.contour(), None);
381 let set = note
382 .with_octave_uncertain(true)
383 .with_velocity(Velocity::new(100).unwrap())
384 .with_overlap(false);
385 assert_eq!(set.octave_uncertain(), Some(true));
386 assert_eq!(set.velocity().map(Velocity::get), Some(100));
387 assert_eq!(set.overlap(), Some(false));
388 }
389
390 #[test]
391 fn a_note_needs_its_offset_after_its_onset() {
392 let error = Note::new(s(1.0), s(1.0), MidiPitch::new(60.0).unwrap()).unwrap_err();
393 assert_eq!(
394 error.to_string(),
395 "a note ends at 1 s but starts at 1 s; the offset must come after the onset"
396 );
397 assert!(Velocity::new(0).is_err());
398 assert!(Velocity::new(128).is_err());
399 }
400
401 #[test]
402 fn without_a_contour_the_f0_is_the_pitch() {
403 let a2 = note(0.0, 1.0, 45.0);
404 assert!((a2.f0_at(s(0.5)).get() - 110.0).abs() < 1e-9);
405 }
406
407 #[test]
408 fn a_contour_moves_in_log_frequency_and_holds_its_ends() {
409 let point = |time, f0| ContourPoint {
410 time: s(time),
411 f0: Hz::new(f0).unwrap(),
412 };
413 let contour = Contour::new(vec![point(1.0, 220.0), point(0.0, 110.0)]).unwrap();
414 assert!((contour.f0_at(s(0.5)).get() - 110.0 * 2.0_f64.sqrt()).abs() < 1e-9);
415 assert_eq!(contour.f0_at(s(2.0)).get(), 220.0);
416 assert_eq!(contour.f0_at(s(0.0)).get(), 110.0);
417 let later = Contour::new(vec![point(1.0, 110.0), point(3.0, 220.0)]).unwrap();
418 assert!((later.f0_at(s(1.5)).get() - 110.0 * 2.0_f64.powf(0.25)).abs() < 1e-9);
419 assert!(Contour::new(vec![]).is_err());
420 assert!(Contour::new(vec![point(0.0, 110.0), point(0.0, 220.0)]).is_err());
421 }
422
423 #[test]
424 fn overlaps_mark_notes_that_share_time() {
425 let list = NoteList::new(vec![
426 note(1.0, 2.0, 60.0),
427 note(0.0, 1.5, 48.0),
428 note(3.0, 4.0, 50.0),
429 ])
430 .with_overlaps();
431 let flags: Vec<_> = list.notes().iter().map(Note::overlap).collect();
432 assert_eq!(flags, vec![Some(true), Some(true), Some(false)]);
433 assert_eq!(list.notes()[0].onset(), s(0.0));
434 }
435}