1use std::error::Error;
5use std::fmt;
6
7use basic_pitch::NoteParams;
8use basic_pitch::constants::{AUDIO_SAMPLE_RATE, FFT_HOP};
9
10use super::Voicing;
11use crate::cast;
12use crate::units::{Bpm, Hz, MidiPitch, Seconds};
13
14pub const LOWEST_MODEL_PITCH: u8 = 21;
16pub const HIGHEST_MODEL_PITCH: u8 = 108;
18
19pub const DEFAULT_MIN_NOTE_FRAMES: usize = 11;
22
23const RULE_FLOOR_MS: f64 = 58.0;
25const RULE_CEILING_MS: f64 = 127.7;
27const SIXTEENTH_FRACTION: f64 = 0.5;
29
30pub(super) fn frame_ms() -> f64 {
32 cast::usize_to_f64(FFT_HOP) / f64::from(AUDIO_SAMPLE_RATE) * 1000.0
33}
34
35#[must_use]
43pub fn min_note_frames(bpm: Bpm) -> usize {
44 let ms = (SIXTEENTH_FRACTION * 15_000.0 / bpm.get()).clamp(RULE_FLOOR_MS, RULE_CEILING_MS);
45 cast::floor_to_usize((ms / frame_ms()).round_ties_even()).unwrap_or(DEFAULT_MIN_NOTE_FRAMES)
46}
47
48#[must_use]
51pub fn min_note_length(bpm: Bpm) -> Seconds {
52 Seconds::clamped(cast::usize_to_f64(min_note_frames(bpm)) * frame_ms() / 1000.0)
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct PitchRange {
59 lowest: u8,
60 highest: u8,
61}
62
63impl PitchRange {
64 pub fn new(lowest: MidiPitch, highest: MidiPitch) -> Result<Self, RangeError> {
71 let whole = |pitch: MidiPitch| {
72 let value = pitch.get();
73 (value.fract() == 0.0
74 && (f64::from(LOWEST_MODEL_PITCH)..=f64::from(HIGHEST_MODEL_PITCH))
75 .contains(&value))
76 .then(|| cast::floor_to_usize(value).and_then(|n| u8::try_from(n).ok()))
77 .flatten()
78 .ok_or(RangeError::Pitch(pitch))
79 };
80 let (lowest, highest) = (whole(lowest)?, whole(highest)?);
81 if lowest > highest {
82 return Err(RangeError::Order { lowest, highest });
83 }
84 Ok(Self { lowest, highest })
85 }
86
87 const fn from_const(lowest: u8, highest: u8) -> Self {
90 Self { lowest, highest }
91 }
92
93 #[must_use]
95 pub fn lowest(self) -> MidiPitch {
96 MidiPitch::clamped(f64::from(self.lowest))
97 }
98
99 #[must_use]
101 pub fn highest(self) -> MidiPitch {
102 MidiPitch::clamped(f64::from(self.highest))
103 }
104
105 #[must_use]
108 pub fn minimum_frequency(self) -> Hz {
109 Hz::from(self.lowest())
110 }
111
112 #[must_use]
115 pub fn maximum_frequency(self) -> Hz {
116 Hz::from(MidiPitch::clamped(f64::from(self.highest) + 1.0))
117 }
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub struct DecodeSettings {
125 pub range: PitchRange,
127 pub min_note_frames: usize,
129 pub melodia: bool,
132}
133
134impl DecodeSettings {
135 #[must_use]
138 pub fn note_params(&self) -> NoteParams {
139 NoteParams {
140 minimum_note_length_ms: cast::usize_to_f64(self.min_note_frames) * frame_ms(),
141 minimum_frequency: Some(self.range.minimum_frequency().get()),
142 maximum_frequency: Some(self.range.maximum_frequency().get()),
143 melodia_trick: self.melodia,
144 include_pitch_bends: false,
145 ..NoteParams::default()
146 }
147 }
148}
149
150#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, clap::ValueEnum)]
152pub enum Profile {
153 Bass,
155 Lead,
157 Pad,
160}
161
162impl Profile {
163 pub const ALL: [Self; 3] = [Self::Bass, Self::Lead, Self::Pad];
165
166 #[must_use]
168 pub fn name(self) -> &'static str {
169 match self {
170 Self::Bass => "bass",
171 Self::Lead => "lead",
172 Self::Pad => "pad",
173 }
174 }
175
176 #[must_use]
178 pub fn range(self) -> PitchRange {
179 match self {
180 Self::Bass => PitchRange::from_const(23, 66),
183 Self::Lead | Self::Pad => PitchRange::from_const(36, 95),
184 }
185 }
186
187 #[must_use]
190 pub fn voicing(self) -> Voicing {
191 match self {
192 Self::Bass | Self::Lead => Voicing::Monophonic,
193 Self::Pad => Voicing::Polyphonic,
194 }
195 }
196
197 #[must_use]
199 pub fn settings(self, bpm: Bpm) -> DecodeSettings {
200 DecodeSettings {
201 range: self.range(),
202 min_note_frames: match self {
203 Self::Bass | Self::Lead => min_note_frames(bpm),
204 Self::Pad => DEFAULT_MIN_NOTE_FRAMES,
205 },
206 melodia: true,
207 }
208 }
209}
210
211impl fmt::Display for Profile {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 f.write_str(self.name())
214 }
215}
216
217#[derive(Clone, Copy, Debug, PartialEq)]
219pub enum RangeError {
220 Pitch(MidiPitch),
222 Order {
224 lowest: u8,
226 highest: u8,
228 },
229}
230
231impl fmt::Display for RangeError {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 match self {
234 Self::Pitch(pitch) => write!(
235 f,
236 "pitch {} is not a whole MIDI pitch from {LOWEST_MODEL_PITCH} to {HIGHEST_MODEL_PITCH}, the range of the transcription model",
237 pitch.get()
238 ),
239 Self::Order { lowest, highest } => write!(
240 f,
241 "the lowest pitch {lowest} lies above the highest pitch {highest}"
242 ),
243 }
244 }
245}
246
247impl Error for RangeError {}
248
249#[cfg(test)]
250mod tests {
251 use basic_pitch::notes::{constrain_frequency, model_output_to_notes};
252 use basic_pitch::{Posteriorgram, Posteriorgrams};
253
254 use super::*;
255
256 fn pitch(value: f64) -> MidiPitch {
257 MidiPitch::new(value).unwrap()
258 }
259
260 fn bpm(value: f64) -> Bpm {
261 Bpm::new(value).unwrap()
262 }
263
264 fn kept(range: PitchRange) -> Vec<u8> {
266 let ones = Posteriorgram::from_vec(vec![1.0; 88], 88).unwrap();
267 let (_, frames) = constrain_frequency(
268 &ones,
269 &ones,
270 Some(range.maximum_frequency().get()),
271 Some(range.minimum_frequency().get()),
272 )
273 .unwrap();
274 (0..88_u8)
275 .filter(|&bin| frames[(0, usize::from(bin))] > 0.0)
276 .map(|bin| bin + LOWEST_MODEL_PITCH)
277 .collect()
278 }
279
280 #[test]
281 fn every_range_keeps_exactly_its_pitches() {
282 for lowest in LOWEST_MODEL_PITCH..=HIGHEST_MODEL_PITCH {
283 for highest in lowest..=HIGHEST_MODEL_PITCH {
284 let range = PitchRange::new(pitch(lowest.into()), pitch(highest.into())).unwrap();
285 assert_eq!(
286 kept(range),
287 (lowest..=highest).collect::<Vec<_>>(),
288 "{lowest} to {highest}"
289 );
290 }
291 }
292 }
293
294 #[test]
295 fn every_profile_keeps_exactly_its_pitches() {
296 for (profile, lowest, highest) in [
297 (Profile::Bass, 23, 66),
298 (Profile::Lead, 36, 95),
299 (Profile::Pad, 36, 95),
300 ] {
301 let range = profile.range();
302 assert_eq!(
303 range,
304 PitchRange::new(pitch(lowest.into()), pitch(highest.into())).unwrap()
305 );
306 assert_eq!(kept(range), (lowest..=highest).collect::<Vec<_>>());
307 }
308 }
309
310 #[test]
311 fn a_400_hz_ceiling_keeps_f_sharp_4_and_drops_g4() {
312 let ones = Posteriorgram::from_vec(vec![1.0; 88], 88).unwrap();
313 let (_, frames) = constrain_frequency(&ones, &ones, Some(400.0), None).unwrap();
314 assert!(frames[(0, 66 - 21)] > 0.0);
315 assert_eq!(frames[(0, 67 - 21)], 0.0);
316 assert_eq!(
317 Profile::Bass.range().highest(),
318 pitch(66.0),
319 "the bass ceiling is the 400 Hz example"
320 );
321 }
322
323 #[test]
324 fn a_range_takes_whole_pitches_of_the_model_only() {
325 assert!(PitchRange::new(pitch(21.0), pitch(108.0)).is_ok());
326 for bad in [20.0, 109.0, 60.5, 0.0, 127.0] {
327 assert_eq!(
328 PitchRange::new(pitch(bad), pitch(108.0)),
329 Err(RangeError::Pitch(pitch(bad)))
330 );
331 }
332 assert_eq!(
333 PitchRange::new(pitch(60.0), pitch(59.0))
334 .unwrap_err()
335 .to_string(),
336 "the lowest pitch 60 lies above the highest pitch 59"
337 );
338 assert_eq!(
339 RangeError::Pitch(pitch(60.5)).to_string(),
340 "pitch 60.5 is not a whole MIDI pitch from 21 to 108, the range of the transcription model"
341 );
342 }
343
344 #[test]
345 fn the_bpm_rule_gives_the_issue_frame_counts() {
346 for (tempo, frames) in [(60.0, 11), (90.0, 7), (120.0, 5), (4.0, 11), (1000.0, 5)] {
347 assert_eq!(min_note_frames(bpm(tempo)), frames, "{tempo} BPM");
348 }
349 assert_eq!(min_note_frames(bpm(58.7)), 11);
351 assert_eq!(min_note_frames(bpm(129.4)), 5);
352 }
353
354 proptest::proptest! {
355 #[test]
356 fn the_bpm_rule_stays_in_range_and_never_grows_with_tempo(
357 a in 4.0..=1000.0_f64,
358 b in 4.0..=1000.0_f64,
359 ) {
360 let (slow, fast) = (a.min(b), a.max(b));
361 let frames = min_note_frames(bpm(slow));
362 proptest::prop_assert!((5..=11).contains(&frames));
363 proptest::prop_assert!(min_note_frames(bpm(fast)) <= frames);
364 for profile in Profile::ALL {
365 let settings = profile.settings(bpm(slow));
366 proptest::prop_assert_eq!(
367 settings.note_params().min_note_len_frames().unwrap(),
368 settings.min_note_frames
369 );
370 }
371 }
372 }
373
374 #[test]
375 fn profiles_turn_bends_off_and_keep_the_decoder_thresholds() {
376 let defaults = NoteParams::default();
377 for profile in Profile::ALL {
378 let params = profile.settings(bpm(120.0)).note_params();
379 assert!(!params.include_pitch_bends, "{profile}");
380 assert!(params.melodia_trick, "{profile}");
381 assert_eq!(params.onset_threshold, defaults.onset_threshold);
382 assert_eq!(params.frame_threshold, defaults.frame_threshold);
383 assert_eq!(params.energy_tolerance, defaults.energy_tolerance);
384 params.validate().unwrap();
385 }
386 assert_eq!(Profile::Pad.settings(bpm(200.0)).min_note_frames, 11);
387 assert_eq!(Profile::Bass.settings(bpm(200.0)).min_note_frames, 5);
388 assert_eq!(Profile::ALL.map(Profile::name), ["bass", "lead", "pad"]);
389 let plain = DecodeSettings {
390 melodia: false,
391 ..Profile::Lead.settings(bpm(120.0))
392 };
393 assert!(!plain.note_params().melodia_trick);
394 }
395
396 fn region(len: usize, onset: bool) -> Posteriorgrams {
399 let (frames, bin) = (60, 57 - 21);
400 let mut note = Posteriorgram::zeros(frames, 88);
401 let mut onsets = Posteriorgram::zeros(frames, 88);
402 for t in 10..10 + len {
403 note.as_mut_slice()[t * 88 + bin] = 0.8;
404 }
405 if onset {
406 onsets.as_mut_slice()[10 * 88 + bin] = 0.9;
407 }
408 Posteriorgrams::new(note, onsets, Posteriorgram::zeros(frames, 264)).unwrap()
409 }
410
411 #[test]
412 fn the_default_length_drops_11_frames_and_keeps_12_with_an_onset() {
413 let params = NoteParams {
414 infer_onsets: false,
415 melodia_trick: false,
416 ..NoteParams::default()
417 };
418 let lengths = |len| {
419 model_output_to_notes(®ion(len, true), ¶ms)
420 .unwrap()
421 .iter()
422 .map(|note| note.end_frame - note.start_frame)
423 .collect::<Vec<_>>()
424 };
425 assert_eq!(lengths(11), Vec::<usize>::new());
426 assert_eq!(lengths(12), vec![12]);
427 }
428
429 #[test]
430 fn the_melodia_pass_is_one_frame_stricter() {
431 let params = NoteParams {
432 infer_onsets: false,
433 ..NoteParams::default()
434 };
435 let lengths = |len| {
436 model_output_to_notes(®ion(len, false), ¶ms)
437 .unwrap()
438 .iter()
439 .map(|note| note.end_frame - note.start_frame)
440 .collect::<Vec<_>>()
441 };
442 assert_eq!(lengths(12), Vec::<usize>::new());
443 assert_eq!(lengths(13), vec![12]);
446 }
447}