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}