///|
/// YIN algorithm configuration for fundamental frequency (F0) pitch tracking.
pub(all) struct YinConfig {
f_min : Double
f_max : Double
threshold : Double
frame_length : Int
hop_length : Int
} derive(Debug)
///|
/// Default YIN configuration (65 Hz to 2093 Hz, threshold 0.1, 2048 frame, 512 hop).
pub fn YinConfig::default() -> YinConfig {
{
f_min: 65.0,
f_max: 2093.0,
threshold: 0.1,
frame_length: 2048,
hop_length: 512,
}
}
///|
/// Output of pitch tracking analysis.
pub(all) struct PitchTrack {
f0 : Array[Double] // Fundamental frequency in Hz (0.0 indicates unvoiced)
voiced_prob : Array[Double] // Voiced confidence in range [0.0, 1.0]
periods : Array[Double] // Period in fractional samples
} derive(Debug)
///|
/// Compute difference function d_t(tau) for a frame of samples.
pub fn difference_function(
frame : Array[Double],
tau_max : Int,
) -> Array[Double] {
let len = frame.length()
if tau_max < 0 {
return []
}
let safe_tau = if tau_max < len {
tau_max
} else if len > 0 {
len - 1
} else {
0
}
let win_len = len - safe_tau
let d : Array[Double] = Array::make(tau_max + 1, 0.0)
for tau in 0..<=safe_tau {
let mut sum = 0.0
for j in 0.. Array[Double] {
let len = d.length()
let cmndf : Array[Double] = Array::make(len, 0.0)
if len == 0 {
return cmndf
}
cmndf[0] = 1.0
let mut running_sum = 0.0
for tau in 1.. (Double, Double, Double) {
if sample_rate <= 0 ||
!@types.is_finite(f_min) ||
!@types.is_finite(f_max) ||
f_min <= 0.0 ||
f_max <= f_min ||
!@types.is_finite(threshold) ||
threshold <= 0.0 ||
threshold >= 1.0 {
return (0.0, 0.0, 0.0)
}
let fs = sample_rate.to_double()
let tau_min_raw = (fs / f_max).to_int()
let tau_max_raw = (fs / f_min).to_int() + 1
let tau_min = if tau_min_raw >= 2 { tau_min_raw } else { 2 }
let max_possible = frame.length() / 2
let tau_max = if tau_max_raw < max_possible {
tau_max_raw
} else {
max_possible
}
if tau_max <= tau_min {
return (0.0, 0.0, 0.0)
}
let d = difference_function(frame, tau_max)
let cmndf = cumulative_mean_normalized_difference(d)
// Step 3: Absolute threshold search
let mut chosen_tau = -1
let mut tau = tau_min
while tau <= tau_max {
if cmndf[tau] < threshold {
while tau + 1 <= tau_max && cmndf[tau + 1] < cmndf[tau] {
tau = tau + 1
}
chosen_tau = tau
break
}
tau = tau + 1
}
// Fallback: Global minimum if threshold not reached
let mut is_voiced = true
if chosen_tau == -1 {
let mut min_val = cmndf[tau_min]
chosen_tau = tau_min
for t in (tau_min + 1)..<=tau_max {
if cmndf[t] < min_val {
min_val = cmndf[t]
chosen_tau = t
}
}
is_voiced = false
}
// Step 4: Parabolic interpolation
let mut refined_tau = chosen_tau.to_double()
if chosen_tau > 0 && chosen_tau < tau_max {
let y0 = cmndf[chosen_tau - 1]
let y1 = cmndf[chosen_tau]
let y2 = cmndf[chosen_tau + 1]
let denom = 2.0 * (y0 - 2.0 * y1 + y2)
if denom.abs() > 1.0e-12 {
let delta = (y0 - y2) / denom
if delta.abs() <= 1.0 {
refined_tau = refined_tau + delta
}
}
}
if refined_tau <= 0.0 {
return (0.0, 0.0, 0.0)
}
let f0 = fs / refined_tau
let raw_prob = 1.0 - cmndf[chosen_tau]
let voiced_prob = if raw_prob < 0.0 {
0.0
} else if raw_prob > 1.0 {
1.0
} else {
raw_prob
}
if f0 < f_min || f0 > f_max || !is_voiced {
(0.0, voiced_prob, refined_tau)
} else {
(f0, voiced_prob, refined_tau)
}
}
///|
/// Extract frame-wise pitch track from single-channel AudioBuffer using YIN.
pub fn pitch_yin(
audio : @types.AudioBuffer,
config? : YinConfig = YinConfig::default(),
) -> Result[PitchTrack, @types.MelError] {
let len = audio.length()
if len == 0 {
return Err(EmptyAudio)
}
let sr = audio.sample_rate
if sr <= 0 {
return Err(InvalidSampleRate(sr))
}
if !@types.is_finite(config.f_min) ||
!@types.is_finite(config.f_max) ||
config.f_min <= 0.0 ||
config.f_max <= config.f_min ||
config.f_max > sr.to_double() / 2.0 {
return Err(InvalidFrequencyRange(config.f_min, config.f_max))
}
if !@types.is_finite(config.threshold) ||
config.threshold <= 0.0 ||
config.threshold >= 1.0 {
return Err(DimensionMismatch("YIN threshold must be in (0, 1)"))
}
let frame_len = config.frame_length
let hop_len = config.hop_length
if frame_len <= 0 || hop_len <= 0 {
return Err(InvalidHopLength(hop_len))
}
let n_frames = if len >= frame_len {
(len - frame_len) / hop_len + 1
} else {
1
}
let f0_arr : Array[Double] = Array::make(n_frames, 0.0)
let prob_arr : Array[Double] = Array::make(n_frames, 0.0)
let period_arr : Array[Double] = Array::make(n_frames, 0.0)
let frame_buf : Array[Double] = Array::make(frame_len, 0.0)
for f in 0..