use crate::matrix::Matrix;
pub struct YinParams {
pub sr: f64,
pub fmin: f64,
pub fmax: f64,
pub frame_length: usize,
pub hop_length: Option<usize>,
pub trough_threshold: f64,
pub center: bool,
}
impl YinParams {
pub fn new(fmin: f64, fmax: f64) -> Self {
Self {
sr: 22050.0,
fmin,
fmax,
frame_length: 2048,
hop_length: None,
trough_threshold: 0.1,
center: true,
}
}
fn hop(&self) -> usize {
self.hop_length.unwrap_or(self.frame_length / 4)
}
}
pub struct PyinParams {
pub sr: f64,
pub fmin: f64,
pub fmax: f64,
pub frame_length: usize,
pub hop_length: Option<usize>,
pub n_thresholds: usize,
pub beta_parameters: (f64, f64),
pub boltzmann_parameter: f64,
pub resolution: f64,
pub max_transition_rate: f64,
pub switch_prob: f64,
pub no_trough_prob: f64,
pub fill_na: Option<f64>,
pub center: bool,
}
impl PyinParams {
pub fn new(fmin: f64, fmax: f64) -> Self {
Self {
sr: 22050.0,
fmin,
fmax,
frame_length: 2048,
hop_length: None,
n_thresholds: 100,
beta_parameters: (2.0, 18.0),
boltzmann_parameter: 2.0,
resolution: 0.1,
max_transition_rate: 35.92,
switch_prob: 0.01,
no_trough_prob: 0.01,
fill_na: Some(f64::NAN),
center: true,
}
}
fn hop(&self) -> usize {
self.hop_length.unwrap_or(self.frame_length / 4)
}
}
pub struct PyinResult {
pub f0: Vec<f64>,
pub voiced_flag: Vec<bool>,
pub voiced_prob: Vec<f64>,
}
fn parabolic_interpolation(x: &[f64]) -> Vec<f64> {
let n = x.len();
let mut shifts = vec![0.0; n];
for k in 1..n.saturating_sub(1) {
let a = x[k - 1] + x[k + 1] - 2.0 * x[k];
let b = (x[k + 1] - x[k - 1]) / 2.0;
if b.abs() < a.abs() {
shifts[k] = -b / a;
}
}
shifts
}
fn cumulative_mean_normalized_difference(
frame: &[f64],
min_period: usize,
max_period: usize,
) -> Vec<f64> {
let acf = crate::autocorrelate::autocorrelate(frame, Some(max_period + 1));
let energy_sq: Vec<f64> = frame.iter().map(|x| x * x).collect();
let mut cumsum_sq = vec![0.0; energy_sq.len()];
let mut running = 0.0;
for (i, &v) in energy_sq.iter().enumerate() {
running += v;
cumsum_sq[i] = running;
}
let n_tau = max_period + 1;
let mut yin_frame = vec![0.0; n_tau];
yin_frame[0] = 0.0;
for tau in 1..n_tau {
let diff = 2.0 * (acf[0] - acf[tau]);
yin_frame[tau] = diff;
}
let mut cumsum_d = vec![0.0; n_tau];
let mut running_d = 0.0;
for tau in 1..n_tau {
running_d += yin_frame[tau];
cumsum_d[tau] = running_d;
}
let mut cmnd = vec![0.0; n_tau];
cmnd[0] = 1.0; for tau in 1..n_tau {
if cumsum_d[tau] > 0.0 {
cmnd[tau] = yin_frame[tau] * tau as f64 / cumsum_d[tau];
} else {
cmnd[tau] = 1.0;
}
}
cmnd[min_period..=max_period].to_vec()
}
fn find_troughs(x: &[f64]) -> Vec<bool> {
let n = x.len();
if n == 0 {
return vec![];
}
let mut troughs = vec![false; n];
if n >= 2 {
troughs[0] = x[0] < x[1];
}
for k in 1..n.saturating_sub(1) {
troughs[k] = x[k] < x[k - 1] && x[k] <= x[k + 1];
}
troughs
}
pub fn yin(y: &[f64], params: &YinParams) -> Vec<f64> {
let hop_length = params.hop();
let frame_length = params.frame_length;
let padded: Vec<f64>;
let signal = if params.center {
let pad = frame_length / 2;
padded = std::iter::repeat_n(0.0, pad)
.chain(y.iter().copied())
.chain(std::iter::repeat_n(0.0, pad))
.collect();
&padded
} else {
y
};
if signal.len() < frame_length {
return vec![];
}
let n_frames = 1 + (signal.len() - frame_length) / hop_length;
let min_period = (params.sr / params.fmax).floor() as usize;
let max_period = ((params.sr / params.fmin).ceil() as usize).min(frame_length - 1);
let mut f0 = Vec::with_capacity(n_frames);
for i in 0..n_frames {
let start = i * hop_length;
let frame = &signal[start..start + frame_length];
let cmnd = cumulative_mean_normalized_difference(frame, min_period, max_period);
let shifts = parabolic_interpolation(&cmnd);
let troughs = find_troughs(&cmnd);
let mut selected = None;
for k in 0..cmnd.len() {
if troughs[k] && cmnd[k] < params.trough_threshold {
selected = Some(k);
break;
}
}
let selected = selected.unwrap_or_else(|| {
cmnd.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.total_cmp(b))
.map(|(idx, _)| idx)
.unwrap_or(0)
});
let period = min_period as f64 + selected as f64 + shifts[selected];
f0.push(params.sr / period);
}
f0
}
pub fn pyin(y: &[f64], params: &PyinParams) -> PyinResult {
use statrs::distribution::{Beta, ContinuousCDF};
let hop_length = params.hop();
let frame_length = params.frame_length;
let padded: Vec<f64>;
let signal = if params.center {
let pad = frame_length / 2;
padded = std::iter::repeat_n(0.0, pad)
.chain(y.iter().copied())
.chain(std::iter::repeat_n(0.0, pad))
.collect();
&padded
} else {
y
};
if signal.len() < frame_length {
return PyinResult {
f0: vec![],
voiced_flag: vec![],
voiced_prob: vec![],
};
}
let n_frames = 1 + (signal.len() - frame_length) / hop_length;
let sr = params.sr;
let min_period = (sr / params.fmax).floor() as usize;
let max_period = ((sr / params.fmin).ceil() as usize).min(frame_length - 1);
let n_periods = max_period - min_period + 1;
let n_bins_per_semitone = (1.0 / params.resolution).ceil() as usize;
let n_pitch_bins = (12.0 * n_bins_per_semitone as f64 * (params.fmax / params.fmin).log2())
.floor() as usize
+ 1;
let beta_dist = Beta::new(params.beta_parameters.0, params.beta_parameters.1)
.expect("invalid beta_parameters: both values must be > 0");
let thresholds: Vec<f64> = (0..=params.n_thresholds)
.map(|i| i as f64 / params.n_thresholds as f64)
.collect();
let beta_cdf: Vec<f64> = thresholds.iter().map(|&t| beta_dist.cdf(t)).collect();
let beta_probs: Vec<f64> = beta_cdf.windows(2).map(|w| w[1] - w[0]).collect();
let n_total_states = 2 * n_pitch_bins;
let mut observation_probs = Matrix::zeros(n_total_states, n_frames);
let mut voiced_prob_per_frame = vec![0.0; n_frames];
for frame_idx in 0..n_frames {
let start = frame_idx * hop_length;
let frame = &signal[start..start + frame_length];
let cmnd = cumulative_mean_normalized_difference(frame, min_period, max_period);
let shifts = parabolic_interpolation(&cmnd);
let troughs = find_troughs(&cmnd);
let trough_indices: Vec<usize> = (0..cmnd.len()).filter(|&k| troughs[k]).collect();
if trough_indices.is_empty() {
let unvoiced_prob = 1.0 / n_pitch_bins as f64;
for b in n_pitch_bins..n_total_states {
*observation_probs.get_mut(b, frame_idx) = unvoiced_prob;
}
continue;
}
let trough_heights: Vec<f64> = trough_indices.iter().map(|&k| cmnd[k]).collect();
let mut yin_probs = vec![0.0; n_periods];
for (thresh_idx, &threshold) in thresholds[1..].iter().enumerate() {
let qualifying: Vec<usize> = (0..trough_indices.len())
.filter(|&ti| trough_heights[ti] < threshold)
.collect();
let n_qualifying = qualifying.len();
if n_qualifying == 0 {
continue;
}
let lambda = params.boltzmann_parameter;
let boltz_unnorm: Vec<f64> = (0..n_qualifying)
.map(|k| (-(k as f64) / lambda).exp())
.collect();
let boltz_sum: f64 = boltz_unnorm.iter().sum();
for (rank, &ti) in qualifying.iter().enumerate() {
let boltz_prob = boltz_unnorm[rank] / boltz_sum;
yin_probs[trough_indices[ti]] += boltz_prob * beta_probs[thresh_idx];
}
}
let global_min_idx = trough_heights
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.total_cmp(b))
.map(|(i, _)| i)
.unwrap_or(0);
let n_below = thresholds[1..]
.iter()
.filter(|&&t| t <= trough_heights[global_min_idx])
.count();
if n_below > 0 {
let fallback_prob: f64 = beta_probs[..n_below].iter().sum::<f64>();
yin_probs[trough_indices[global_min_idx]] += params.no_trough_prob * fallback_prob;
}
for (period_offset, &prob) in yin_probs.iter().enumerate() {
if prob <= 0.0 {
continue;
}
let period = min_period as f64 + period_offset as f64 + shifts[period_offset];
if period <= 0.0 {
continue;
}
let f0_candidate = sr / period;
if f0_candidate < params.fmin || f0_candidate > params.fmax {
continue;
}
let bin_float = 12.0 * n_bins_per_semitone as f64 * (f0_candidate / params.fmin).log2();
let bin = bin_float.round().max(0.0).min(n_pitch_bins as f64 - 1.0) as usize;
*observation_probs.get_mut(bin, frame_idx) += prob;
}
let vp: f64 = (0..n_pitch_bins)
.map(|b| observation_probs.get(b, frame_idx))
.sum::<f64>()
.clamp(0.0, 1.0);
voiced_prob_per_frame[frame_idx] = vp;
let unvoiced_val = (1.0 - vp) / n_pitch_bins as f64;
for b in n_pitch_bins..n_total_states {
*observation_probs.get_mut(b, frame_idx) = unvoiced_val;
}
}
let max_semitones_per_frame =
(params.max_transition_rate * 12.0 * hop_length as f64 / sr).round() as usize;
let transition_width = max_semitones_per_frame * n_bins_per_semitone + 1;
let pitch_trans =
crate::sequence::transition_local(n_pitch_bins, transition_width, "triangle", false);
let switch_trans = crate::sequence::transition_loop(2, &[1.0 - params.switch_prob]);
let mut full_trans = Matrix::zeros(n_total_states, n_total_states);
for si in 0..2 {
for sj in 0..2 {
let switch_val = switch_trans.get(si, sj);
for pi in 0..n_pitch_bins {
for pj in 0..n_pitch_bins {
let pitch_val = pitch_trans.get(pi, pj);
*full_trans.get_mut(si * n_pitch_bins + pi, sj * n_pitch_bins + pj) =
switch_val * pitch_val;
}
}
}
}
let p_init = vec![1.0 / n_total_states as f64; n_total_states];
let states = crate::sequence::viterbi(&observation_probs, &full_trans, Some(&p_init));
let freqs: Vec<f64> = (0..n_pitch_bins)
.map(|b| params.fmin * 2.0_f64.powf(b as f64 / (12.0 * n_bins_per_semitone as f64)))
.collect();
let mut f0 = Vec::with_capacity(n_frames);
let mut voiced_flag = Vec::with_capacity(n_frames);
for &state in &states {
let pitch_bin = state % n_pitch_bins;
let is_voiced = state < n_pitch_bins;
voiced_flag.push(is_voiced);
if is_voiced {
f0.push(freqs[pitch_bin]);
} else {
f0.push(params.fill_na.unwrap_or(freqs[pitch_bin]));
}
}
PyinResult {
f0,
voiced_flag,
voiced_prob: voiced_prob_per_frame,
}
}