use super::{Complex64, DFT_SIZE, HarmonicBasis, packed_index};
pub const N_REFINE_CANDIDATES: usize = 10;
pub const REFINE_OFFSETS: [f64; N_REFINE_CANDIDATES] = [
-9.0 / 8.0,
-7.0 / 8.0,
-5.0 / 8.0,
-3.0 / 8.0,
-1.0 / 8.0,
1.0 / 8.0,
3.0 / 8.0,
5.0 / 8.0,
7.0 / 8.0,
9.0 / 8.0,
];
pub const L_HAT_MIN: u8 = 9;
pub const L_HAT_MAX: u8 = 56;
pub const B0_MAX: u8 = 207;
const E_R_LOWER_M: i32 = 50;
#[derive(Clone, Copy, Debug)]
pub struct PitchRefinement {
pub omega_hat: f64,
pub l_hat: u8,
pub b0: u8,
pub e_r: f64,
}
#[inline]
pub fn harmonic_count_for(omega_hat: f64) -> u8 {
let inner = (core::f64::consts::PI / omega_hat + 0.25).floor();
let raw = (0.9254 * inner).floor();
raw.clamp(f64::from(L_HAT_MIN), f64::from(L_HAT_MAX)) as u8
}
#[inline]
pub fn quantize_pitch_index(omega_hat: f64) -> Option<u8> {
let raw = (4.0 * core::f64::consts::PI / omega_hat - 39.0).floor();
if (0.0..=f64::from(B0_MAX)).contains(&raw) {
Some(raw as u8)
} else {
None
}
}
#[inline]
fn e_r_upper_m(omega0: f64) -> i32 {
(0.9254 * 128.0 - 64.0 * omega0 / core::f64::consts::PI).floor() as i32
}
pub(super) fn residual_e_r(
sw: &[Complex64; DFT_SIZE],
basis: &HarmonicBasis,
omega0: f64,
) -> f64 {
let m_max = e_r_upper_m(omega0);
if m_max < E_R_LOWER_M {
return 0.0;
}
let mut e_r = 0.0;
let mut l = 0u32;
loop {
let (m_lo, m_hi) = HarmonicBasis::bin_endpoints(l, omega0);
if m_lo > m_max {
break;
}
if m_hi <= E_R_LOWER_M {
l += 1;
continue;
}
let a_l = basis.harmonic_amplitude(sw, l, omega0);
let m_start = m_lo.max(E_R_LOWER_M);
let m_end = m_hi.min(m_max + 1);
for m in m_start..m_end {
let synth = basis.synthetic_bin(m, l, omega0, a_l);
let observed = sw[packed_index(m)];
let dre = observed.re - synth.re;
let dim = observed.im - synth.im;
e_r += dre * dre + dim * dim;
}
l += 1;
if l > u32::from(L_HAT_MAX) + 2 {
break;
}
}
e_r
}
pub fn refine_pitch(
sw: &[Complex64; DFT_SIZE],
basis: &HarmonicBasis,
p_hat_i: f64,
) -> PitchRefinement {
let mut best_e_r = f64::INFINITY;
let mut best_omega = 2.0 * core::f64::consts::PI / p_hat_i;
let mut best_abs_offset = f64::INFINITY;
for &offset in &REFINE_OFFSETS {
let p = p_hat_i + offset;
let omega0 = 2.0 * core::f64::consts::PI / p;
let e_r = residual_e_r(sw, basis, omega0);
let abs_off = offset.abs();
let take = e_r < best_e_r
|| (e_r == best_e_r && abs_off < best_abs_offset);
if take {
best_e_r = e_r;
best_omega = omega0;
best_abs_offset = abs_off;
}
}
let l_hat = harmonic_count_for(best_omega);
let b0 = quantize_pitch_index(best_omega).unwrap_or(B0_MAX);
PitchRefinement {
omega_hat: best_omega,
l_hat,
b0,
e_r: best_e_r,
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::{W_R_HALF, signal_spectrum};
#[test]
fn refine_offsets_are_symmetric_and_quarter_sample_spaced() {
assert!(!REFINE_OFFSETS.contains(&0.0));
for i in 1..N_REFINE_CANDIDATES {
let step = REFINE_OFFSETS[i] - REFINE_OFFSETS[i - 1];
assert!((step - 0.25).abs() < 1e-12, "step at i={i}: {step}");
}
let sum: f64 = REFINE_OFFSETS.iter().sum();
assert!(sum.abs() < 1e-12, "offsets not symmetric, sum = {sum}");
assert_eq!(REFINE_OFFSETS[0], -9.0 / 8.0);
assert_eq!(REFINE_OFFSETS[9], 9.0 / 8.0);
}
#[test]
fn harmonic_count_respects_admissible_range() {
let omega_lo = 2.0 * core::f64::consts::PI / 123.125;
assert_eq!(harmonic_count_for(omega_lo), 56);
let omega_hi = 2.0 * core::f64::consts::PI / 19.875;
assert_eq!(harmonic_count_for(omega_hi), 9);
let omega_mid = 2.0 * core::f64::consts::PI / 50.0;
assert_eq!(harmonic_count_for(omega_mid), 23);
}
#[test]
fn harmonic_count_applies_inner_floor() {
let omega_hat = core::f64::consts::PI / 13.8;
assert_eq!(harmonic_count_for(omega_hat), 12);
}
#[test]
fn quantize_pitch_index_covers_full_range() {
let omega_lo = 2.0 * core::f64::consts::PI / 123.125;
assert_eq!(quantize_pitch_index(omega_lo), Some(207));
let omega_hi = 2.0 * core::f64::consts::PI / 19.875;
assert_eq!(quantize_pitch_index(omega_hi), Some(0));
let omega_too_hi = 2.0 * core::f64::consts::PI / 10.0; assert_eq!(quantize_pitch_index(omega_too_hi), None);
let omega_too_lo = 2.0 * core::f64::consts::PI / 250.0; assert_eq!(quantize_pitch_index(omega_too_lo), None);
}
#[test]
fn quantize_pitch_index_roundtrips_through_decoder() {
use core::f64::consts::PI;
for p_times_2 in 42u32..=244 {
let p = f64::from(p_times_2) * 0.5;
let omega_hat = 2.0 * PI / p;
let b0 = quantize_pitch_index(omega_hat).expect("in range");
let omega_tilde = 4.0 * PI / (f64::from(b0) + 39.5);
let step = 4.0 * PI / (f64::from(b0) + 39.5).powi(2);
assert!(
(omega_hat - omega_tilde).abs() <= step,
"P={p}: ω̂={omega_hat:.6}, ω̃={omega_tilde:.6}, step={step:.6}"
);
}
}
fn broadband_periodic(period: f64, max_h: u32) -> [f64; (2 * W_R_HALF + 1) as usize] {
let mut signal = [0.0f64; (2 * W_R_HALF + 1) as usize];
let omega = 2.0 * core::f64::consts::PI / period;
for (idx, slot) in signal.iter_mut().enumerate() {
let n = idx as i32 - W_R_HALF;
let nf = f64::from(n);
let mut s = 0.0;
for h in 1..=max_h {
s += (f64::from(h) * omega * nf).cos() / f64::from(h);
}
*slot = s;
}
signal
}
#[test]
fn refine_pitch_returns_candidate_within_offset_range() {
use core::f64::consts::PI;
let basis = HarmonicBasis::new();
let signal = broadband_periodic(50.0, 20);
let sw = signal_spectrum(&signal);
let refined = refine_pitch(&sw, &basis, 50.0);
let p_refined = 2.0 * PI / refined.omega_hat;
assert!(
(p_refined - 50.0).abs() <= 9.0 / 8.0 + 1e-6,
"P refined = {p_refined}, out of ±1.125 around 50"
);
let matches_a_candidate = REFINE_OFFSETS
.iter()
.any(|&offset| (p_refined - (50.0 + offset)).abs() < 1e-9);
assert!(matches_a_candidate, "P refined = {p_refined} not a candidate");
}
#[test]
fn refine_pitch_picks_close_candidate_on_broadband_signal() {
use core::f64::consts::PI;
let basis = HarmonicBasis::new();
let signal = broadband_periodic(50.0, 20);
let sw = signal_spectrum(&signal);
let refined = refine_pitch(&sw, &basis, 50.0);
let p_refined = 2.0 * PI / refined.omega_hat;
assert!(
(p_refined - 50.0).abs() <= 3.0 / 8.0 + 1e-6,
"P refined = {p_refined}, expected within ±3/8 of 50"
);
}
#[test]
fn refine_pitch_derived_parameters_are_consistent() {
let basis = HarmonicBasis::new();
let signal = broadband_periodic(50.0, 20);
let sw = signal_spectrum(&signal);
let refined = refine_pitch(&sw, &basis, 50.0);
assert_eq!(refined.l_hat, harmonic_count_for(refined.omega_hat));
assert_eq!(refined.b0, quantize_pitch_index(refined.omega_hat).unwrap());
assert!((L_HAT_MIN..=L_HAT_MAX).contains(&refined.l_hat));
assert!(refined.b0 <= B0_MAX);
}
#[test]
#[ignore]
fn probe_dump_residual_e_r() {
use core::f64::consts::PI;
let basis = HarmonicBasis::new();
let omega_truth = 2.0 * PI / 50.0;
let mut signal = [0.0f64; (2 * W_R_HALF + 1) as usize];
for n in -W_R_HALF..=W_R_HALF {
signal[(n + W_R_HALF) as usize] = (omega_truth * f64::from(n)).cos();
}
let sw = signal_spectrum(&signal);
for &offset in &REFINE_OFFSETS {
let p = 50.0 + offset;
let omega0 = 2.0 * PI / p;
let e_r = residual_e_r(&sw, &basis, omega0);
println!(" P = {p:6.3} (offset {offset:+.3}) ω₀ = {omega0:.6} E_R = {e_r:.6e}");
}
}
#[test]
fn refine_pitch_residual_is_nonnegative() {
let basis = HarmonicBasis::new();
let signal = broadband_periodic(40.0, 15);
let sw = signal_spectrum(&signal);
let refined = refine_pitch(&sw, &basis, 40.0);
assert!(refined.e_r >= 0.0, "E_R = {}", refined.e_r);
assert!(refined.e_r.is_finite());
}
}