pitch-core 0.1.0

Streaming pitch (f0) tracker — pure-DSP backends (SWIPE', pYIN, Praat-AC). No model files, no GPU.
Documentation
use crate::estimator::{EstimatorError, Result};
use rubato::{
    Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction,
};

pub struct LinearResampler {
    inner: Option<SincFixedIn<f32>>,
    chunk_in: usize,
    pending: Vec<f32>,
}

impl LinearResampler {
    pub fn new(in_sr: u32, out_sr: u32, chunk_in: usize) -> Result<Self> {
        if in_sr == out_sr {
            return Ok(Self {
                inner: None,
                chunk_in,
                pending: Vec::new(),
            });
        }
        let params = SincInterpolationParameters {
            sinc_len: 128,
            f_cutoff: 0.95,
            interpolation: SincInterpolationType::Linear,
            oversampling_factor: 128,
            window: WindowFunction::BlackmanHarris2,
        };
        let ratio = out_sr as f64 / in_sr as f64;
        let inner = SincFixedIn::<f32>::new(ratio, 1.0, params, chunk_in, 1)
            .map_err(|e| EstimatorError::Resample(e.to_string()))?;
        Ok(Self {
            inner: Some(inner),
            chunk_in,
            pending: Vec::with_capacity(chunk_in * 2),
        })
    }

    pub fn push(&mut self, audio: &[f32]) -> Result<Vec<f32>> {
        if self.inner.is_none() {
            return Ok(audio.to_vec());
        }
        self.pending.extend_from_slice(audio);
        let mut out: Vec<f32> = Vec::new();
        let inner = self.inner.as_mut().unwrap();
        while self.pending.len() >= self.chunk_in {
            let chunk: Vec<f32> = self.pending.drain(..self.chunk_in).collect();
            let resampled = inner
                .process(&[chunk], None)
                .map_err(|e| EstimatorError::Resample(e.to_string()))?;
            out.extend_from_slice(&resampled[0]);
        }
        Ok(out)
    }

    pub fn reset(&mut self) {
        self.pending.clear();
        if let Some(inner) = self.inner.as_mut() {
            inner.reset();
        }
    }
}