Skip to main content

pitch_core/
lib.rs

1//! Streaming pitch (f0) tracker. Pure-DSP backends only — neural
2//! backends live in the companion crate `pitch-core-onnx`.
3//!
4//! # Quick start
5//!
6//! ```no_run
7//! use pitch_core::{PitchTracker, SwipeEstimator};
8//!
9//! # fn main() -> Result<(), pitch_core::EstimatorError> {
10//! let est = SwipeEstimator::new()?;
11//! let mut tracker = PitchTracker::new(est, 48_000, 1024)?;
12//!
13//! # let chunk = vec![0.0f32; 4800];
14//! for frame in tracker.process(&chunk)? {
15//!     if frame.confidence > 0.3 {
16//!         println!("{:.3}s  {:.1} Hz", frame.time_s, frame.pitch_hz);
17//!     }
18//! }
19//! # Ok(()) }
20//! ```
21//!
22//! # Adding ONNX backends
23//!
24//! Add `pitch-core-onnx` as a dependency and pass any of its estimators
25//! to the same [`PitchTracker::new`]. The trait surface is identical:
26//!
27//! ```ignore
28//! use pitch_core::PitchTracker;
29//! use pitch_core_onnx::{SwiftF0Estimator, Mode};
30//!
31//! let est = SwiftF0Estimator::new("path/to/swift_f0.onnx", Mode::Balanced)?;
32//! let mut tracker = PitchTracker::new(est, 48_000, 1024)?;
33//! ```
34
35pub mod estimator;
36pub mod praat_ac;
37pub mod pyin_est;
38pub mod resample;
39pub mod swipe;
40
41pub use estimator::{calibrate_confidence, EstimatorError, PitchEstimator, PitchFrame, Result};
42pub use praat_ac::PraatAcEstimator;
43pub use pyin_est::PyinEstimator;
44pub use swipe::SwipeEstimator;
45
46/// High-level streaming pitch tracker. Combines any [`PitchEstimator`]
47/// with a linear resampler that converts the host's sample rate to the
48/// estimator's target rate.
49///
50/// Feed it mono `f32` audio at `input_sample_rate` via [`process`](Self::process).
51/// The tracker buffers internally and returns frames as they become
52/// available.
53pub struct PitchTracker {
54    estimator: Box<dyn PitchEstimator>,
55    resampler: resample::LinearResampler,
56    input_sr: u32,
57    target_sr: u32,
58}
59
60impl PitchTracker {
61    /// Build a tracker from any concrete [`PitchEstimator`].
62    pub fn new<E: PitchEstimator + 'static>(
63        estimator: E,
64        input_sample_rate: u32,
65        resample_chunk: usize,
66    ) -> Result<Self> {
67        Self::from_boxed(Box::new(estimator), input_sample_rate, resample_chunk)
68    }
69
70    /// Build a tracker from an already-boxed estimator. Useful when the
71    /// estimator type is decided at runtime (e.g. from a CLI flag).
72    pub fn from_boxed(
73        estimator: Box<dyn PitchEstimator>,
74        input_sample_rate: u32,
75        resample_chunk: usize,
76    ) -> Result<Self> {
77        let target_sr = estimator.target_sample_rate();
78        let resampler =
79            resample::LinearResampler::new(input_sample_rate, target_sr, resample_chunk)?;
80        Ok(Self {
81            estimator,
82            resampler,
83            input_sr: input_sample_rate,
84            target_sr,
85        })
86    }
87
88    pub fn algorithm(&self) -> &str {
89        self.estimator.name()
90    }
91
92    pub fn input_sample_rate(&self) -> u32 {
93        self.input_sr
94    }
95
96    pub fn target_sample_rate(&self) -> u32 {
97        self.target_sr
98    }
99
100    pub fn reset(&mut self) {
101        self.resampler.reset();
102        self.estimator.reset();
103    }
104
105    /// Push mono `f32` audio at `input_sample_rate`. Returns whatever
106    /// frames the estimator produced after consuming this chunk. Empty
107    /// at the start of the stream and after [`reset`](Self::reset)
108    /// while the estimator's internal buffer fills.
109    pub fn process(&mut self, audio: &[f32]) -> Result<Vec<PitchFrame>> {
110        let resampled = self.resampler.push(audio)?;
111        if resampled.is_empty() {
112            return Ok(Vec::new());
113        }
114        self.estimator.process(&resampled)
115    }
116}