1use std::error::Error;
12use std::fmt;
13
14use super::pyin::{Pyin, PyinError, Settings};
15use super::{Contour, ContourPoint, MODEL_RATE, Note, NoteList};
16use crate::audio::{ResampleError, SampleRate, resample};
17use crate::cast;
18use crate::dsp::{StftParams, frame_rms};
19use crate::units::{Decibels, Hz, MidiPitch, Seconds};
20
21pub const TRACK_FMIN: Hz = Hz::from_const(25.0);
23pub const TRACK_FMAX: Hz = Hz::from_const(4200.0);
27pub const TRACK_FRAME: usize = 2048;
29pub const TRACK_HOP: usize = 128;
31const BINS_PER_SEMITONE: usize = 10;
33const SILENT_LEVEL: Decibels = Decibels::from_const(-200.0);
35
36#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct TrackFrame {
39 pub time: Seconds,
41 pub f0: Option<Hz>,
43 pub level: Decibels,
46}
47
48#[derive(Clone, Debug, Default, PartialEq)]
50pub struct PitchTrack {
51 frames: Vec<TrackFrame>,
52}
53
54impl PitchTrack {
55 pub fn of(mono: &[f64], rate: SampleRate) -> Result<Self, TrackError> {
68 let mut samples = resample(mono, rate, MODEL_RATE).map_err(TrackError::Resample)?;
69 if let Some(index) = samples.iter().position(|x| !x.is_finite()) {
70 return Err(TrackError::NotFinite(index));
71 }
72 let peak = samples.iter().fold(0.0_f64, |peak, x| peak.max(x.abs()));
73 if samples.is_empty() || peak == 0.0 {
74 return Ok(Self::default());
75 }
76 for x in &mut samples {
77 *x /= peak;
78 }
79 let tracker = Pyin::new(Settings {
80 fmin: TRACK_FMIN.get(),
81 fmax: TRACK_FMAX.get(),
82 rate: MODEL_RATE.get(),
83 frame: TRACK_FRAME,
84 hop: TRACK_HOP,
85 bins_per_semitone: BINS_PER_SEMITONE,
86 })?;
87 let f0s = tracker.run(&samples)?;
88 let rms = frame_rms(
90 &samples,
91 StftParams::fixed(TRACK_FRAME)
93 .with_hop(TRACK_HOP)
94 .unwrap_or(StftParams::fixed(TRACK_FRAME)),
95 );
96 let loudest = rms.iter().copied().fold(0.0, f64::max);
97 let frames = f0s
98 .into_iter()
99 .enumerate()
100 .map(|(m, f0)| TrackFrame {
101 time: Seconds::clamped(
102 cast::usize_to_f64(m) * cast::usize_to_f64(TRACK_HOP)
103 / f64::from(MODEL_RATE.get()),
104 ),
105 f0: f0.filter(|hz| hz.is_finite() && *hz > 0.0).map(Hz::clamped),
106 level: rms
107 .get(m)
108 .and_then(|&value| Decibels::from_amplitude_gain(value / loudest).ok())
109 .map_or(SILENT_LEVEL, |level| {
110 Decibels::clamped(level.get().max(SILENT_LEVEL.get()))
111 }),
112 })
113 .collect();
114 Ok(Self { frames })
115 }
116
117 #[must_use]
119 pub fn from_frames(frames: Vec<TrackFrame>) -> Self {
120 Self { frames }
121 }
122
123 #[must_use]
125 pub fn frames(&self) -> &[TrackFrame] {
126 &self.frames
127 }
128
129 pub fn voiced_between(
132 &self,
133 onset: Seconds,
134 offset: Seconds,
135 ) -> impl Iterator<Item = (Seconds, Hz)> + '_ {
136 let start = self.frames.partition_point(|frame| frame.time < onset);
137 self.frames[start..]
138 .iter()
139 .take_while(move |frame| frame.time < offset)
140 .filter_map(|frame| frame.f0.map(|f0| (frame.time, f0)))
141 }
142
143 #[must_use]
146 pub fn voiced_pitches(&self) -> Vec<MidiPitch> {
147 self.frames
148 .iter()
149 .filter_map(|frame| frame.f0)
150 .filter_map(|f0| MidiPitch::try_from(f0).ok())
151 .collect()
152 }
153
154 #[must_use]
157 pub fn contour(&self, onset: Seconds, offset: Seconds) -> Option<Contour> {
158 let points: Vec<ContourPoint> = self
159 .voiced_between(onset, offset)
160 .map(|(time, f0)| ContourPoint { time, f0 })
161 .collect();
162 Contour::new(points).ok()
163 }
164
165 #[must_use]
167 pub fn with_contours(&self, notes: &NoteList) -> NoteList {
168 NoteList::new(
169 notes
170 .notes()
171 .iter()
172 .map(|note| match self.contour(note.onset(), note.offset()) {
173 Some(contour) => note.clone().with_contour(contour),
174 None => note.clone(),
175 })
176 .collect::<Vec<Note>>(),
177 )
178 }
179}
180
181#[derive(Debug)]
183pub enum TrackError {
184 Resample(ResampleError),
186 NotFinite(usize),
188 Tracker(&'static str),
190}
191
192impl fmt::Display for TrackError {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 Self::Resample(source) => {
196 write!(f, "cannot prepare the stem for the f0 track: {source}")
197 }
198 Self::NotFinite(index) => write!(f, "cannot track f0: sample {index} is not finite"),
199 Self::Tracker(reason) => write!(f, "cannot track f0: {reason}"),
200 }
201 }
202}
203
204impl From<PyinError> for TrackError {
205 fn from(PyinError(reason): PyinError) -> Self {
206 Self::Tracker(reason)
207 }
208}
209
210impl Error for TrackError {
211 fn source(&self) -> Option<&(dyn Error + 'static)> {
212 match self {
213 Self::Resample(source) => Some(source),
214 Self::NotFinite(_) | Self::Tracker(_) => None,
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::cast;
223
224 fn frame(time: f64, f0: Option<f64>) -> TrackFrame {
225 TrackFrame {
226 time: Seconds::new(time).unwrap(),
227 f0: f0.map(|hz| Hz::new(hz).unwrap()),
228 level: Decibels::new(0.0).unwrap(),
229 }
230 }
231
232 #[test]
233 fn voiced_frames_run_from_the_onset_up_to_the_offset() {
234 let track = PitchTrack::from_frames(vec![
235 frame(0.1, Some(110.0)),
236 frame(0.2, None),
237 frame(0.3, Some(220.0)),
238 frame(0.4, Some(440.0)),
239 ]);
240 let s = |value| Seconds::new(value).unwrap();
241 let times: Vec<f64> = track
242 .voiced_between(s(0.1), s(0.4))
243 .map(|(time, _)| time.get())
244 .collect();
245 assert_eq!(times, [0.1, 0.3]);
246 let pitches: Vec<f64> = track
247 .voiced_pitches()
248 .iter()
249 .map(|p| p.get().round())
250 .collect();
251 assert_eq!(pitches, [45.0, 57.0, 69.0]);
252 }
253
254 #[test]
255 fn errors_name_their_cause_and_keep_their_source() {
256 let not_finite = TrackError::NotFinite(7);
257 assert_eq!(
258 not_finite.to_string(),
259 "cannot track f0: sample 7 is not finite"
260 );
261 assert!(not_finite.source().is_none());
262 let native = SampleRate::new(48_000).unwrap();
263 let resample = TrackError::Resample(resample(&[f64::NAN], native, MODEL_RATE).unwrap_err());
264 assert!(
265 resample
266 .to_string()
267 .starts_with("cannot prepare the stem for the f0 track: ")
268 );
269 assert!(resample.source().is_some());
270 let tracker = TrackError::from(PyinError("the frame is too short"));
271 assert_eq!(
272 tracker.to_string(),
273 "cannot track f0: the frame is too short"
274 );
275 assert!(tracker.source().is_none());
276 }
277
278 #[test]
282 fn the_settings_meet_every_rule_of_the_tracker() {
283 let rate = f64::from(MODEL_RATE.get());
284 let (fmin, fmax) = (TRACK_FMIN.get(), TRACK_FMAX.get());
285 assert!(0.0 < fmin && fmin < fmax && fmax <= rate / 2.0);
286 let window = TRACK_FRAME / 2;
287 let longest = cast::ceil_to_usize(rate / fmin).unwrap();
290 assert!(longest < TRACK_FRAME - window - 1);
291 let shortest = cast::floor_to_usize(rate / fmax).unwrap().max(1);
292 assert!(longest - shortest >= 2);
293 let bins = cast::floor_to_usize(
296 12.0 * cast::usize_to_f64(BINS_PER_SEMITONE) * (fmax / fmin).log2(),
297 )
298 .unwrap()
299 + 1;
300 let per_frame =
301 cast::round_to_usize(35.92 * 12.0 * cast::usize_to_f64(TRACK_HOP) / rate).unwrap();
302 assert!(bins > per_frame * 10);
303 assert!(TRACK_HOP > 0 && window > 0);
304 }
305}