use std::num::{NonZeroU32, NonZeroUsize};
use non_empty_slice::NonEmptyVec;
use crate::AudioSamples;
use crate::operations::traits::AudioStatistics;
const DIAGONAL_LOADING_EPSILON: f64 = 1e-5;
const MIN_PREDICTION_ERROR: f64 = 1e-15;
pub const SILK_LPC_ORDER: usize = 16;
#[derive(Debug, Clone)]
pub struct LpcCoefficients {
pub coeffs: Vec<f32>,
pub prediction_error: f64,
}
#[must_use]
pub fn compute_autocorrelation(samples: &[f32], max_lag: usize) -> Vec<f64> {
let n = samples.len();
if n == 0 {
return vec![0.0; max_lag + 1];
}
let windowed: Vec<f32> = samples
.iter()
.enumerate()
.map(|(i, &x)| {
let w = if n > 1 {
let t = 2.0 * std::f64::consts::PI * i as f64 / (n - 1) as f64;
0.54 - 0.46 * t.cos()
} else {
1.0
};
(w * x as f64) as f32
})
.collect();
let effective_max = max_lag.min(n - 1);
let ne = NonEmptyVec::new(windowed).expect("n > 0 checked above");
let audio = AudioSamples::<'static, f32>::from_mono_vec(ne, NonZeroU32::new(1).expect("1 > 0"));
if let Some(lag_nz) = NonZeroUsize::new(effective_max) {
if let Some(normalized) = audio.autocorrelation(lag_nz) {
return (0..=effective_max)
.map(|lag| normalized[lag] * (n - lag) as f64)
.collect();
}
}
let w64: Vec<f64> = audio
.as_slice()
.expect("from_mono_vec is always contiguous")
.iter()
.map(|&x| x as f64)
.collect();
(0..=effective_max)
.map(|lag| (0..n - lag).map(|i| w64[i] * w64[i + lag]).sum::<f64>())
.collect()
}
#[must_use]
pub fn levinson_durbin(autocorr: &[f64], order: usize) -> Option<LpcCoefficients> {
if autocorr.len() < order + 1 || autocorr[0] < 1e-12 {
return None;
}
let mut r = autocorr.to_vec();
r[0] *= 1.0 + DIAGONAL_LOADING_EPSILON;
let mut a = vec![0.0f64; order];
let mut a_prev = vec![0.0f64; order];
let mut error = r[0];
for m in 0..order {
let mut num = r[m + 1];
for i in 0..m {
num += a_prev[i] * r[m - i];
}
let k = -num / error;
for i in 0..m {
a[i] = a_prev[i] + k * a_prev[m - 1 - i];
}
a[m] = k;
error = (error * (1.0 - k * k)).max(MIN_PREDICTION_ERROR);
std::mem::swap(&mut a, &mut a_prev);
}
let coeffs = a.iter().map(|&v| v as f32).collect();
Some(LpcCoefficients {
coeffs,
prediction_error: error,
})
}
#[must_use]
pub fn lpc_analysis(samples: &[f32], order: usize) -> LpcCoefficients {
let effective_order = order.min(samples.len() / 2).max(1);
let autocorr = compute_autocorrelation(samples, effective_order);
levinson_durbin(&autocorr, effective_order).unwrap_or_else(|| LpcCoefficients {
coeffs: vec![0.0; effective_order],
prediction_error: 1.0,
})
}
#[must_use]
pub fn lpc_residual(samples: &[f32], coeffs: &LpcCoefficients) -> Vec<f32> {
let order = coeffs.coeffs.len();
let mut residual = Vec::with_capacity(samples.len());
for n in 0..samples.len() {
let mut sum = 0.0f32;
for k in 0..order.min(n) {
sum += coeffs.coeffs[k] * samples[n - 1 - k];
}
residual.push(samples[n] + sum);
}
residual
}
#[must_use]
pub fn lpc_synthesis(residual: &[f32], coeffs: &LpcCoefficients) -> Vec<f32> {
let order = coeffs.coeffs.len();
let mut output = Vec::with_capacity(residual.len());
for n in 0..residual.len() {
let mut sum = 0.0f32;
for k in 0..order.min(n) {
sum += coeffs.coeffs[k] * output[n - 1 - k];
}
output.push(residual[n] - sum);
}
output
}
#[must_use]
pub fn lpc_residual_stateful(
samples: &[f32],
coeffs: &LpcCoefficients,
state: &mut Vec<f32>,
) -> Vec<f32> {
let order = coeffs.coeffs.len();
normalise_state(state, order);
let mut residual = Vec::with_capacity(samples.len());
for n in 0..samples.len() {
let mut sum = 0.0f32;
for k in 0..order {
let pos = n as isize - 1 - k as isize;
let x = if pos >= 0 {
samples[pos as usize]
} else {
state[(order as isize + pos) as usize]
};
sum += coeffs.coeffs[k] * x;
}
residual.push(samples[n] + sum);
}
update_state(state, samples, order);
residual
}
#[must_use]
pub fn lpc_synthesis_stateful(
residual: &[f32],
coeffs: &LpcCoefficients,
state: &mut Vec<f32>,
) -> Vec<f32> {
let order = coeffs.coeffs.len();
normalise_state(state, order);
let mut output = Vec::with_capacity(residual.len());
for n in 0..residual.len() {
let mut sum = 0.0f32;
for k in 0..order {
let pos = n as isize - 1 - k as isize;
let y = if pos >= 0 {
output[pos as usize]
} else {
state[(order as isize + pos) as usize]
};
sum += coeffs.coeffs[k] * y;
}
output.push(residual[n] - sum);
}
update_state(state, &output, order);
output
}
#[must_use]
pub fn estimate_pitch(samples: &[f32], sample_rate: u32) -> Option<(usize, f32)> {
let n = samples.len();
let fs = sample_rate as usize;
let t_min = (fs / 500).max(2);
let t_max = (fs / 50).min(n / 2);
if t_max <= t_min || n <= t_max {
return None;
}
let r0: f64 = samples.iter().map(|&x| (x as f64).powi(2)).sum();
if r0 < 1e-10 {
return None;
}
let (best_lag, best_r) = (t_min..=t_max)
.map(|lag| {
let r: f64 = (0..n - lag)
.map(|i| samples[i] as f64 * samples[i + lag] as f64)
.sum();
(lag, r / r0)
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))?;
if best_r < 0.3 {
return None;
}
Some((best_lag, (best_r as f32).clamp(0.0, 0.9)))
}
#[must_use]
pub fn ltp_residual(samples: &[f32], lag: usize, gain: f32) -> Vec<f32> {
samples
.iter()
.enumerate()
.map(|(n, &e)| e - gain * if n >= lag { samples[n - lag] } else { 0.0 })
.collect()
}
#[must_use]
pub fn ltp_synthesis(residual: &[f32], lag: usize, gain: f32) -> Vec<f32> {
let mut output = vec![0.0f32; residual.len()];
for n in 0..residual.len() {
let prev = if n >= lag { output[n - lag] } else { 0.0 };
output[n] = residual[n] + gain * prev;
}
output
}
fn normalise_state(state: &mut Vec<f32>, order: usize) {
while state.len() < order {
state.insert(0, 0.0);
}
if state.len() > order {
let excess = state.len() - order;
state.drain(0..excess);
}
}
fn update_state(state: &mut Vec<f32>, samples: &[f32], order: usize) {
let n = samples.len();
if n >= order {
state.clear();
state.extend_from_slice(&samples[n - order..]);
} else {
let tail: Vec<f32> = state[n..].to_vec();
state.clear();
state.extend_from_slice(&tail);
state.extend_from_slice(samples);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_no_quantization() {
let samples: Vec<f32> = (0..64)
.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin() * 0.5)
.collect();
let coeffs = lpc_analysis(&samples, SILK_LPC_ORDER);
let residual = lpc_residual(&samples, &coeffs);
let recovered = lpc_synthesis(&residual, &coeffs);
for (orig, rec) in samples.iter().zip(recovered.iter()) {
assert!(
(orig - rec).abs() < 1e-4,
"round-trip error {:.2e} > 1e-4",
(orig - rec).abs()
);
}
}
#[test]
fn levinson_silent_frame() {
let autocorr = vec![0.0f64; SILK_LPC_ORDER + 1];
assert!(levinson_durbin(&autocorr, SILK_LPC_ORDER).is_none());
}
#[test]
fn stateful_round_trip_single_frame() {
let samples: Vec<f32> = (0..64)
.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin() * 0.5)
.collect();
let coeffs = lpc_analysis(&samples, SILK_LPC_ORDER);
let mut enc_state = Vec::new();
let mut dec_state = Vec::new();
let residual = lpc_residual_stateful(&samples, &coeffs, &mut enc_state);
let recovered = lpc_synthesis_stateful(&residual, &coeffs, &mut dec_state);
for (o, r) in samples.iter().zip(recovered.iter()) {
assert!(
(o - r).abs() < 1e-4,
"stateful round-trip error {:.2e}",
(o - r).abs()
);
}
}
#[test]
fn stateful_cross_frame_continuity() {
let n = 64usize;
let all: Vec<f32> = (0..n * 2)
.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin() * 0.5)
.collect();
let coeffs0 = lpc_analysis(&all[..n], SILK_LPC_ORDER);
let coeffs1 = lpc_analysis(&all[n..], SILK_LPC_ORDER);
let mut enc_state = Vec::new();
let mut dec_state = Vec::new();
let res0 = lpc_residual_stateful(&all[..n], &coeffs0, &mut enc_state);
let res1 = lpc_residual_stateful(&all[n..], &coeffs1, &mut enc_state);
let rec0 = lpc_synthesis_stateful(&res0, &coeffs0, &mut dec_state);
let rec1 = lpc_synthesis_stateful(&res1, &coeffs1, &mut dec_state);
let mut recovered = rec0;
recovered.extend(rec1);
for (o, r) in all.iter().zip(recovered.iter()) {
assert!(
(o - r).abs() < 1e-3,
"cross-frame error {:.2e}",
(o - r).abs()
);
}
}
#[test]
fn ltp_round_trip() {
let samples: Vec<f32> = (0..200)
.map(|i| (2.0 * std::f32::consts::PI * 220.0 * i as f32 / 44100.0).sin() * 0.5)
.collect();
let lag = 100;
let gain = 0.7;
let residual = ltp_residual(&samples, lag, gain);
let recovered = ltp_synthesis(&residual, lag, gain);
for (o, r) in samples.iter().zip(recovered.iter()) {
assert!(
(o - r).abs() < 1e-4,
"LTP round-trip error {:.2e}",
(o - r).abs()
);
}
}
#[test]
fn pitch_detection_sine() {
let freq_hz = 220.0_f32;
let sample_rate = 44100_u32;
let expected_lag = (sample_rate as f32 / freq_hz).round() as usize; let samples: Vec<f32> = (0..882)
.map(|i| (2.0 * std::f32::consts::PI * freq_hz * i as f32 / sample_rate as f32).sin())
.collect();
let result = estimate_pitch(&samples, sample_rate);
assert!(result.is_some(), "pitch not detected for {freq_hz} Hz sine");
let (lag, gain) = result.unwrap();
assert!(
lag.abs_diff(expected_lag) <= 2,
"detected lag {lag} expected ~{expected_lag}"
);
assert!(gain > 0.3, "LTP gain {gain:.2} too low");
}
}