Skip to main content

sound_to_patch/notes/
transcribe.rs

1//! The transcription model: one run per stem, then any number of decodes.
2//!
3//! The model reads mono `f32` samples at 22,050 Hz and returns three
4//! activation matrices, the posteriorgrams, with one frame every 256
5//! samples. Running it is the expensive step. Decoding notes from the
6//! posteriorgrams is cheap, so every profile decodes the same run.
7
8use std::error::Error;
9use std::fmt;
10use std::num::NonZeroUsize;
11
12use basic_pitch::notes::{NoteEvent, frame_to_time, model_output_to_notes};
13use basic_pitch::{BasicPitch, Posteriorgrams};
14
15pub use basic_pitch::model::{Backend, MODEL_SHA256};
16
17use super::{DecodeSettings, LOWEST_MODEL_PITCH, Note, NoteList, PitchTrack, TrackError, Velocity};
18use crate::audio::{ResampleError, SampleRate, resample};
19use crate::cast;
20use crate::units::{MidiPitch, Seconds};
21
22/// Samples between two model frames.
23const MODEL_HOP: u16 = 256;
24
25/// The input rate of the model.
26pub const MODEL_RATE: SampleRate = SampleRate::from_const(22_050);
27
28/// The f0 tracker that the notes step runs beside the model:
29/// [`PitchTrack::of`] unless [`Transcriber::with_tracker`] names another.
30pub type Tracker = fn(&[f64], SampleRate) -> Result<PitchTrack, TrackError>;
31
32/// The transcription model with its inference engine, built once and used
33/// for every stem, and the f0 tracker of the notes step.
34#[derive(Debug)]
35pub struct Transcriber {
36    model: BasicPitch,
37    tracker: Tracker,
38}
39
40impl Transcriber {
41    /// Loads the bundled model on the pure-Rust engine and spreads the
42    /// windows over every core.
43    ///
44    /// # Errors
45    ///
46    /// Returns a [`TranscribeError`] when the engine cannot load the model.
47    pub fn new() -> Result<Self, TranscribeError> {
48        let threads = std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN);
49        Ok(Self {
50            model: BasicPitch::new()
51                .map_err(TranscribeError::Model)?
52                .with_threads(threads),
53            tracker: PitchTrack::of,
54        })
55    }
56
57    /// Spreads the windows over this many threads. The thread count never
58    /// changes the result.
59    #[must_use]
60    pub fn with_threads(self, threads: NonZeroUsize) -> Self {
61        Self {
62            model: self.model.with_threads(threads),
63            ..self
64        }
65    }
66
67    /// Runs the model on another engine, such as one that counts its calls
68    /// in a test.
69    #[must_use]
70    pub fn with_backend(backend: impl Backend + 'static) -> Self {
71        Self {
72            model: BasicPitch::with_backend(backend),
73            tracker: PitchTrack::of,
74        }
75    }
76
77    /// Runs another f0 tracker in the notes step, such as one that skips
78    /// the pYIN track in a test whose subject is not the track.
79    #[must_use]
80    pub fn with_tracker(self, tracker: Tracker) -> Self {
81        Self { tracker, ..self }
82    }
83
84    /// Runs the f0 tracker on a mono stem at any rate.
85    pub(crate) fn track(&self, mono: &[f64], rate: SampleRate) -> Result<PitchTrack, TrackError> {
86        (self.tracker)(mono, rate)
87    }
88
89    /// Runs the model once on a mono signal at any rate. The signal is
90    /// resampled to [`MODEL_RATE`] and cast to `f32`, with its gain
91    /// untouched: the model output depends on the absolute level.
92    ///
93    /// # Errors
94    ///
95    /// Returns a [`TranscribeError`] when resampling fails, a sample is
96    /// not finite or the engine fails.
97    pub fn run(&self, mono: &[f64], rate: SampleRate) -> Result<ModelRun, TranscribeError> {
98        let samples = resample(mono, rate, MODEL_RATE).map_err(TranscribeError::Resample)?;
99        let input: Vec<f32> = samples.into_iter().map(cast::f64_to_f32).collect();
100        let posteriorgrams = self.model.predict(&input).map_err(TranscribeError::Model)?;
101        Ok(ModelRun { posteriorgrams })
102    }
103}
104
105/// The posteriorgrams of one model run over a stem.
106#[derive(Clone, Debug)]
107pub struct ModelRun {
108    posteriorgrams: Posteriorgrams,
109}
110
111impl ModelRun {
112    /// Wraps hand-built posteriorgrams, as a test's.
113    #[cfg(test)]
114    pub(crate) fn from_posteriorgrams(posteriorgrams: Posteriorgrams) -> Self {
115        Self { posteriorgrams }
116    }
117
118    /// Returns the note, onset and contour activations.
119    #[must_use]
120    pub fn posteriorgrams(&self) -> &Posteriorgrams {
121        &self.posteriorgrams
122    }
123
124    /// Returns the strongest onset activation of a key, 0 to 1, over the
125    /// frames that start within one frame of `time`, or `None` outside the
126    /// run or the model's keys. The decoder starts a note on an onset peak,
127    /// or grows one from the note activations when no onset fires.
128    #[must_use]
129    pub fn onset_strength(&self, time: Seconds, pitch: MidiPitch) -> Option<f64> {
130        let onsets = &self.posteriorgrams.onset;
131        let bin = cast::round_to_usize(pitch.get() - f64::from(LOWEST_MODEL_PITCH))
132            .filter(|&bin| bin < onsets.n_bins())?;
133        let hop = f64::from(MODEL_HOP) / f64::from(MODEL_RATE.get());
134        (0..onsets.n_frames())
135            .filter(|&frame| (frame_to_time(frame) - time.get()).abs() <= hop)
136            .filter_map(|frame| onsets.get(frame, bin))
137            .map(f64::from)
138            .reduce(f64::max)
139    }
140
141    /// Decodes the notes with one set of settings, without running the
142    /// model again.
143    ///
144    /// Each note takes its times from the decoder's seconds, never from
145    /// frame indices, and its velocity from the decoder. That velocity is
146    /// the mean activation, a confidence and not a loudness. A note whose
147    /// end does not follow its start carries no sound and is left out.
148    ///
149    /// # Errors
150    ///
151    /// Returns a [`TranscribeError`] when the decoder rejects the settings.
152    pub fn decode(&self, settings: &DecodeSettings) -> Result<NoteList, TranscribeError> {
153        let events = model_output_to_notes(&self.posteriorgrams, &settings.note_params())
154            .map_err(TranscribeError::Model)?;
155        Ok(NoteList::new(events.iter().filter_map(note_of).collect()))
156    }
157}
158
159fn note_of(event: &NoteEvent) -> Option<Note> {
160    let note = Note::new(
161        Seconds::clamped(event.start_s),
162        Seconds::clamped(event.end_s),
163        MidiPitch::clamped(f64::from(event.pitch)),
164    )
165    .ok()?;
166    // MIDI reserves velocity 0 for a note-off, so the lowest confidence
167    // reads as 1.
168    Some(match Velocity::new(event.velocity().max(1)) {
169        Ok(velocity) => note.with_velocity(velocity),
170        Err(_) => note,
171    })
172}
173
174/// A model run or a decode that failed.
175#[derive(Debug)]
176pub enum TranscribeError {
177    /// The stem could not be resampled to the model rate.
178    Resample(ResampleError),
179    /// The model or its decoder failed, or a sample was not finite.
180    Model(basic_pitch::Error),
181}
182
183impl fmt::Display for TranscribeError {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        match self {
186            Self::Resample(source) => {
187                write!(
188                    f,
189                    "cannot prepare the stem for the transcription model: {source}"
190                )
191            }
192            Self::Model(source) => write!(f, "the transcription model failed: {source}"),
193        }
194    }
195}
196
197impl Error for TranscribeError {
198    fn source(&self) -> Option<&(dyn Error + 'static)> {
199        match self {
200            Self::Resample(source) => Some(source),
201            Self::Model(source) => Some(source),
202        }
203    }
204}