use alloc::vec::Vec;
use crate::MAX_LPC_ORDER;
pub const MAX_COEFFICIENT_SHIFT: u8 = 5;
pub struct LpcOrderView<'a> {
pub coefficients: &'a [i16],
pub shift: u8,
}
pub struct LpcLevels {
flat: [i16; (MAX_LPC_ORDER as usize) * (MAX_LPC_ORDER as usize)],
shifts: [u8; MAX_LPC_ORDER as usize],
max_order: u8,
}
impl LpcLevels {
#[inline]
pub fn new() -> Self {
Self {
flat: [0; (MAX_LPC_ORDER as usize) * (MAX_LPC_ORDER as usize)],
shifts: [0; MAX_LPC_ORDER as usize],
max_order: 0,
}
}
#[inline]
pub fn get(&self, m: u8) -> LpcOrderView<'_> {
debug_assert!(m >= 1 && m <= self.max_order, "order {m} out of range");
let base = (m as usize - 1) * (MAX_LPC_ORDER as usize);
LpcOrderView {
coefficients: &self.flat[base..base + m as usize],
shift: self.shifts[m as usize - 1],
}
}
}
impl Default for LpcLevels {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn autocorrelation_into(samples: &[i32], order: u8, out: &mut [i64]) {
let lags = order as usize + 1;
debug_assert!(
out.len() >= lags,
"autocorrelation_into: out.len()={} too small for order={order}",
out.len()
);
let n = samples.len();
for k in 0..lags {
let mut acc: i64 = 0;
for i in 0..n.saturating_sub(k) {
acc += (samples[i] as i64) * (samples[i + k] as i64);
}
out[k] = acc;
}
}
fn levinson_durbin_fill(r: &[i64], order: u8, levels: &mut LpcLevels) -> bool {
if r[0] == 0 {
return false;
}
let order_usize = order as usize;
let mut a = [0i64; (MAX_LPC_ORDER as usize) + 1];
let mut a_new = [0i64; (MAX_LPC_ORDER as usize) + 1];
let mut e: i64 = r[0];
let mut converged = true;
for m in 1..=order_usize {
let mut num: i128 = r[m] as i128;
for j in 1..m {
let prod = (a[j] as i128 * r[m - j] as i128 + (1i128 << 30)) >> 31;
num += prod;
}
if e <= 0 {
converged = false;
break;
}
let numerator = -num * (1i128 << 31);
let half_e = (e / 2) as i128;
let bias = if numerator >= 0 { half_e } else { -half_e };
let lambda_i128 = (numerator + bias) / e as i128;
let lambda = lambda_i128.clamp(-(1i128 << 31), (1i128 << 31) - 1) as i64;
a_new[..=m].copy_from_slice(&a[..=m]);
for j in 1..m {
let delta = (lambda as i128 * a[m - j] as i128 + (1i128 << 30)) >> 31;
a_new[j] += delta as i64;
}
a_new[m] = lambda;
a[..=m].copy_from_slice(&a_new[..=m]);
let shift = min_shift_for(&a[1..=m]);
levels.shifts[m - 1] = shift;
let base = (m - 1) * (MAX_LPC_ORDER as usize);
for (dst, &coeff) in levels.flat[base..base + m].iter_mut().zip(&a[1..=m]) {
*dst = q31_to_qn(coeff, shift);
}
let lambda_sq_q62 = lambda as i128 * lambda as i128;
let correction = (e as i128 * lambda_sq_q62 + (1i128 << 61)) >> 62;
e -= correction as i64;
}
if !converged {
}
levels.max_order = order;
converged
}
fn min_shift_for(coeffs_q31: &[i64]) -> u8 {
let max_abs: u64 = coeffs_q31
.iter()
.map(|&c| c.unsigned_abs())
.max()
.unwrap_or(0);
if max_abs < (1u64 << 31) {
return 0;
}
let shift = (64u32 - max_abs.leading_zeros()).saturating_sub(31) as u8;
shift.min(MAX_COEFFICIENT_SHIFT)
}
fn q31_to_qn(a_q31: i64, shift: u8) -> i16 {
let pred_q31 = -a_q31;
let bias = 1i64 << (15 + shift as u32);
let out = (pred_q31 + bias) >> (16 + shift as u32);
out.clamp(i16::MIN as i64, i16::MAX as i64) as i16
}
pub fn lpc_analyze_levels_into(samples: &[i32], max_order: u8, levels: &mut LpcLevels) -> bool {
debug_assert!(
max_order >= 1,
"max_order must be ≥ 1 for lpc_analyze_levels_into"
);
debug_assert!(
max_order <= MAX_LPC_ORDER,
"max_order={max_order} exceeds MAX_LPC_ORDER={MAX_LPC_ORDER}"
);
let mut r = [0i64; (MAX_LPC_ORDER as usize) + 1];
autocorrelation_into(samples, max_order, &mut r);
levinson_durbin_fill(&r[..=max_order as usize], max_order, levels)
}
#[cfg(test)]
pub(crate) fn lpc_analyze(samples: &[i32], order: u8) -> Option<(Vec<i16>, u8)> {
if order == 0 {
return Some((Vec::new(), 0));
}
let mut levels = LpcLevels::new();
if !lpc_analyze_levels_into(samples, order, &mut levels) {
return None;
}
let view = levels.get(order);
Some((view.coefficients.to_vec(), view.shift))
}
#[cfg(any(test, feature = "__internal-for-bench"))]
pub fn compute_residuals(samples: &[i32], coeffs: &[i16], shift: u8) -> Vec<i32> {
let mut out = Vec::with_capacity(samples.len());
compute_residuals_into(samples, coeffs, shift, &mut out);
out
}
pub fn compute_residuals_into(samples: &[i32], coeffs: &[i16], shift: u8, out: &mut Vec<i32>) {
out.clear();
out.reserve(samples.len());
let order = coeffs.len();
if order == 0 {
out.extend_from_slice(samples);
return;
}
let mut coeffs_rev_buf = [0i32; MAX_LPC_ORDER as usize];
for k in 0..order {
coeffs_rev_buf[k] = coeffs[order - 1 - k] as i32;
}
let coeffs_rev = &coeffs_rev_buf[..order];
let shift_amt = 15u32 - shift as u32;
let bias = 1i64 << (shift_amt - 1);
let n = samples.len();
let warm = order.min(n);
for i in 0..warm {
let mut sum: i64 = 0;
let crev = &coeffs_rev[order - i..];
let window = &samples[..i];
for k in 0..i {
sum += crev[k] as i64 * window[k] as i64;
}
let pred = if i == 0 { 0 } else { (sum + bias) >> shift_amt };
out.push(samples[i] - pred as i32);
}
for i in order..n {
let window = &samples[i - order..i];
let mut sum: i64 = 0;
for k in 0..order {
sum += coeffs_rev[k] as i64 * window[k] as i64;
}
let pred = (sum + bias) >> shift_amt;
out.push(samples[i] - pred as i32);
}
}
#[cfg(test)]
pub(crate) fn lpc_synthesize(residuals: &[i32], coeffs: &[i16], shift: u8) -> Vec<i32> {
let mut out = Vec::with_capacity(residuals.len());
lpc_synthesize_into(residuals, coeffs, shift, &mut out);
out
}
pub fn lpc_synthesize_into(residuals: &[i32], coeffs: &[i16], shift: u8, out: &mut Vec<i32>) {
out.clear();
out.reserve(residuals.len());
let order = coeffs.len();
let shift_amt = 15u32 - shift as u32;
let bias = 1i64 << (shift_amt - 1);
let samples = out;
for (i, &res) in residuals.iter().enumerate() {
let terms = i.min(order);
let pred = if terms == 0 {
0i64
} else {
let sum: i64 = (0..terms)
.map(|j| coeffs[j] as i64 * samples[i - j - 1] as i64)
.sum();
(sum + bias) >> shift_amt
};
samples.push(res.wrapping_add(pred as i32));
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
use crate::test_signals::{angular_step_q32, sin_q15, sine_samples as int_sine_samples};
fn sine_samples(n: usize, freq_cycles: u64, amplitude: i32) -> Vec<i32> {
let step = angular_step_q32(freq_cycles, n as u64);
int_sine_samples(n, step, amplitude)
}
#[test]
fn residuals_synthesize_roundtrip_all_orders() {
let samples = sine_samples(960, 4, 50_000);
let (coeffs, shift) = lpc_analyze(&samples, 0).unwrap();
assert!(coeffs.is_empty());
assert_eq!(shift, 0);
let residuals = compute_residuals(&samples, &coeffs, shift);
assert_eq!(residuals, samples);
assert_eq!(lpc_synthesize(&residuals, &coeffs, shift), samples);
for order in 1..=MAX_LPC_ORDER {
let (coeffs, shift) = lpc_analyze(&samples, order).unwrap();
assert_eq!(coeffs.len(), order as usize);
assert!(shift <= MAX_COEFFICIENT_SHIFT);
let residuals = compute_residuals(&samples, &coeffs, shift);
let reconstructed = lpc_synthesize(&residuals, &coeffs, shift);
assert_eq!(reconstructed, samples, "roundtrip failed at order {order}");
}
}
#[test]
fn autocorrelation_into_impulse() {
const C: i32 = 1_000_000; let mut samples = vec![0i32; 32];
samples[0] = C;
let order = 8u8;
let mut r = vec![0i64; order as usize + 1];
autocorrelation_into(&samples, order, &mut r);
assert_eq!(r[0], (C as i64) * (C as i64), "R[0] must equal c² exactly");
for (k, &r_k) in r.iter().enumerate().skip(1) {
assert_eq!(r_k, 0, "R[{k}] must be zero for an impulse input");
}
}
#[test]
fn autocorrelation_into_dc() {
const C: i32 = 5_000;
const N: usize = 64;
let samples = vec![C; N];
let order = 4u8;
let mut r = vec![0i64; order as usize + 1];
autocorrelation_into(&samples, order, &mut r);
let c2 = (C as i64) * (C as i64);
for (k, &r_k) in r.iter().enumerate() {
let expected = (N - k) as i64 * c2;
assert_eq!(r_k, expected, "R[{k}] wrong for DC input");
}
}
#[test]
fn lpc_analyze_levels_matches_lpc_analyze() {
let samples = sine_samples(960, 6, 100_000);
let mut levels = LpcLevels::new();
assert!(lpc_analyze_levels_into(
&samples,
MAX_LPC_ORDER,
&mut levels
));
for order in 1..=MAX_LPC_ORDER {
let (single_coeffs, single_shift) = lpc_analyze(&samples, order).unwrap();
let view = levels.get(order);
assert_eq!(
view.coefficients,
&single_coeffs[..],
"coefficient mismatch at order {order}"
);
assert_eq!(view.shift, single_shift, "shift mismatch at order {order}");
}
}
#[test]
fn all_zero_frame_returns_none() {
let samples = vec![0i32; 960];
assert!(lpc_analyze(&samples, 4).is_none());
let mut levels = LpcLevels::new();
assert!(!lpc_analyze_levels_into(&samples, 16, &mut levels));
}
#[test]
fn order_zero_returns_empty_coeffs() {
let samples = sine_samples(960, 4, 1000);
let (coeffs, shift) = lpc_analyze(&samples, 0).unwrap();
assert!(coeffs.is_empty());
assert_eq!(shift, 0);
}
#[test]
fn high_order_24_stays_non_trivial() {
let n = 1024usize;
let step_a: u32 = 75_143_389; let step_b: u32 = 184_443_047; let step_c: u32 = 293_742_706; let mut samples: Vec<i32> = Vec::with_capacity(n);
let mut phase_a: u32 = 0;
let mut phase_b: u32 = 0;
let mut phase_c: u32 = 0;
for _ in 0..n {
let a = (sin_q15(phase_a) as i64 * 40_000 + (1 << 14)) >> 15;
let b = (sin_q15(phase_b) as i64 * 20_000 + (1 << 14)) >> 15;
let c = (sin_q15(phase_c) as i64 * 10_000 + (1 << 14)) >> 15;
samples.push((a + b + c) as i32);
phase_a = phase_a.wrapping_add(step_a);
phase_b = phase_b.wrapping_add(step_b);
phase_c = phase_c.wrapping_add(step_c);
}
let (c24, s24) = lpc_analyze(&samples, 24).unwrap();
assert!(
c24.iter().any(|&c| c != 0),
"order-24 coefficients collapsed to zero"
);
let (c8, s8) = lpc_analyze(&samples, 8).unwrap();
let e8: u64 = compute_residuals(&samples, &c8, s8)
.iter()
.map(|&r| (r as i64 * r as i64) as u64)
.sum();
let e24: u64 = compute_residuals(&samples, &c24, s24)
.iter()
.map(|&r| (r as i64 * r as i64) as u64)
.sum();
assert!(
e24 < e8,
"high-order residual energy should be smaller: e8={e8}, e24={e24}"
);
}
#[test]
fn dc_signal_residuals_are_small() {
let samples = vec![10_000i32; 960];
let (coeffs, shift) = lpc_analyze(&samples, 4).unwrap();
let residuals = compute_residuals(&samples, &coeffs, shift);
let max_residual = residuals[4..].iter().map(|r| r.abs()).max().unwrap_or(0);
assert!(max_residual <= 20, "DC residuals too large: {max_residual}");
}
#[test]
fn order_32_roundtrip() {
let step = angular_step_q32(27, 8192);
let samples = int_sine_samples(4096, step, 1_000_000);
let (coeffs, shift) = lpc_analyze(&samples, 32).unwrap();
assert_eq!(coeffs.len(), 32);
let residuals = compute_residuals(&samples, &coeffs, shift);
let reconstructed = lpc_synthesize(&residuals, &coeffs, shift);
assert_eq!(reconstructed, samples);
}
#[test]
fn bass_sine_selects_nonzero_shift() {
let step = angular_step_q32(50, 48_000);
let samples = int_sine_samples(4096, step, 1_000_000);
let (_coeffs, shift) = lpc_analyze(&samples, 4).unwrap();
assert!(
shift >= 1,
"low-frequency sine should force shift ≥ 1, got {shift}"
);
}
#[test]
fn min_shift_for_examples() {
assert_eq!(min_shift_for(&[(1i64 << 31) - 1]), 0);
assert_eq!(min_shift_for(&[1i64 << 31]), 1);
assert_eq!(min_shift_for(&[-(1i64 << 31)]), 1);
assert_eq!(min_shift_for(&[(1i64 << 32) - 1]), 1);
assert_eq!(min_shift_for(&[1i64 << 32]), 2);
assert_eq!(min_shift_for(&[0, 0, 0]), 0);
assert_eq!(min_shift_for(&[0, 1 << 31, -(1 << 31) + 1]), 1);
}
}