#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::many_single_char_names,
clippy::similar_names,
clippy::unreadable_literal
)]
use super::super::lp::autocorr::{
autocorrelation, lag_window, Autocorrelation, LP_ORDER, WINDOW_LEN,
};
use super::super::lp::isf::{interpolate_isp, isp_to_isf, NB_SUBFR};
use super::super::math::scale_sig;
use super::preproc::{Preprocessor, Scaling, L_FRAME, L_FRAME16K, L_TOTAL, NEW_SPEECH};
use crate::fixed_point::arith::{abs_s, add, extract_h, extract_l, negate, round, sub};
use crate::fixed_point::arith32::{l_abs, l_add, l_mac, l_msu, l_mult, l_negate, l_sub};
use crate::fixed_point::div::div_s;
use crate::fixed_point::oper32::{div_32, l_comp, l_extract, mpy_32, mpy_32_16};
use crate::fixed_point::shift::{l_shl, l_shr, norm_l, norm_s, shl, shr};
use crate::fixed_point::types::{DspContext, Word16, Word32};
pub const L_SUBFR: usize = 64;
pub const L_NEXT: usize = 64;
pub const SPEECH: usize = L_TOTAL - L_FRAME - L_NEXT;
const _: () = assert!(L_TOTAL == WINDOW_LEN);
pub const OPL_DECIM: usize = 2;
pub const PIT_MAX: usize = 231;
pub const WSP_HISTORY: usize = PIT_MAX / OPL_DECIM;
pub const GAMMA1: Word16 = Word16(30147);
pub const TILT_FAC: Word16 = Word16(22282);
const NC: usize = LP_ORDER / 2;
const GRID_POINTS: usize = 100;
pub const ISP_INIT: [Word16; LP_ORDER] = [
Word16(32138),
Word16(30274),
Word16(27246),
Word16(23170),
Word16(18205),
Word16(12540),
Word16(6393),
Word16(0),
Word16(-6393),
Word16(-12540),
Word16(-18205),
Word16(-23170),
Word16(-27246),
Word16(-30274),
Word16(-32138),
Word16(1475),
];
const GRID: [Word16; GRID_POINTS + 1] = {
const fn w(v: i16) -> Word16 {
Word16(v)
}
[
w(32767),
w(32751),
w(32703),
w(32622),
w(32509),
w(32364),
w(32187),
w(31978),
w(31738),
w(31466),
w(31164),
w(30830),
w(30466),
w(30072),
w(29649),
w(29196),
w(28714),
w(28204),
w(27666),
w(27101),
w(26509),
w(25891),
w(25248),
w(24579),
w(23886),
w(23170),
w(22431),
w(21669),
w(20887),
w(20083),
w(19260),
w(18418),
w(17557),
w(16680),
w(15786),
w(14876),
w(13951),
w(13013),
w(12062),
w(11099),
w(10125),
w(9141),
w(8149),
w(7148),
w(6140),
w(5126),
w(4106),
w(3083),
w(2057),
w(1029),
w(0),
w(-1029),
w(-2057),
w(-3083),
w(-4106),
w(-5126),
w(-6140),
w(-7148),
w(-8149),
w(-9141),
w(-10125),
w(-11099),
w(-12062),
w(-13013),
w(-13951),
w(-14876),
w(-15786),
w(-16680),
w(-17557),
w(-18418),
w(-19260),
w(-20083),
w(-20887),
w(-21669),
w(-22431),
w(-23170),
w(-23886),
w(-24579),
w(-25248),
w(-25891),
w(-26509),
w(-27101),
w(-27666),
w(-28204),
w(-28714),
w(-29196),
w(-29649),
w(-30072),
w(-30466),
w(-30830),
w(-31164),
w(-31466),
w(-31738),
w(-31978),
w(-32187),
w(-32364),
w(-32509),
w(-32622),
w(-32703),
w(-32751),
w(-32760),
]
};
const H_FIR: [Word16; 5] = [
Word16(4260),
Word16(7536),
Word16(9175),
Word16(7536),
Word16(4260),
];
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LevinsonMemory {
previous_a: [Word16; LP_ORDER],
previous_rc: [Word16; 2],
}
impl LevinsonMemory {
#[must_use]
pub const fn new() -> Self {
Self {
previous_a: [Word16(0); LP_ORDER],
previous_rc: [Word16(0); 2],
}
}
}
pub fn levinson(
r: &Autocorrelation,
mem: &mut LevinsonMemory,
) -> ([Word16; LP_ORDER + 1], [Word16; LP_ORDER]) {
let mut ctx = DspContext::default();
let mut a = [Word16(0); LP_ORDER + 1];
let mut rc = [Word16(0); LP_ORDER];
let mut a_hi = [Word16(0); LP_ORDER + 1];
let mut a_lo = [Word16(0); LP_ORDER + 1];
let r1 = l_comp(r.high[1], r.low[1]);
let magnitude = l_abs(&mut ctx, r1);
let mut k = div_32(magnitude, r.high[0], r.low[0]);
if r1.0 > 0 {
k = l_negate(&mut ctx, k);
}
let (mut k_hi, mut k_lo) = l_extract(k);
rc[0] = k_hi;
let k_q27 = l_shr(&mut ctx, k, 4);
let (hi, lo) = l_extract(k_q27);
a_hi[1] = hi;
a_lo[1] = lo;
let (mut alpha_hi, mut alpha_lo, mut alpha_exp) =
prediction_gain(&mut ctx, r.high[0], r.low[0], k_hi, k_lo);
for i in 2..=LP_ORDER {
let mut acc = Word32(0);
for j in 1..i {
let term = mpy_32(r.high[j], r.low[j], a_hi[i - j], a_lo[i - j]);
acc = l_add(&mut ctx, acc, term);
}
acc = l_shl(&mut ctx, acc, 4);
let ri = l_comp(r.high[i], r.low[i]);
let error = l_add(&mut ctx, acc, ri);
let magnitude = l_abs(&mut ctx, error);
let mut k = div_32(magnitude, alpha_hi, alpha_lo);
if error.0 > 0 {
k = l_negate(&mut ctx, k);
}
let mut k = l_shl(&mut ctx, k, alpha_exp);
let (hi, lo) = l_extract(k);
k_hi = hi;
k_lo = lo;
rc[i - 1] = k_hi;
if abs_s(&mut ctx, k_hi).0 > 32750 {
a[0] = Word16(4096);
a[1..=LP_ORDER].copy_from_slice(&mem.previous_a);
rc[0] = mem.previous_rc[0];
rc[1] = mem.previous_rc[1];
return (a, rc);
}
let mut an_hi = [Word16(0); LP_ORDER + 1];
let mut an_lo = [Word16(0); LP_ORDER + 1];
for j in 1..i {
let mut term = mpy_32(k_hi, k_lo, a_hi[i - j], a_lo[i - j]);
term = l_add(&mut ctx, term, l_comp(a_hi[j], a_lo[j]));
let (hi, lo) = l_extract(term);
an_hi[j] = hi;
an_lo[j] = lo;
}
k = l_shr(&mut ctx, k, 4);
let (hi, lo) = l_extract(k);
an_hi[i] = hi;
an_lo[i] = lo;
let (hi, lo, exp) = prediction_gain(&mut ctx, alpha_hi, alpha_lo, k_hi, k_lo);
alpha_hi = hi;
alpha_lo = lo;
alpha_exp = add(&mut ctx, Word16(alpha_exp), Word16(exp)).0;
a_hi[1..=i].copy_from_slice(&an_hi[1..=i]);
a_lo[1..=i].copy_from_slice(&an_lo[1..=i]);
}
a[0] = Word16(4096);
for i in 1..=LP_ORDER {
let value = l_comp(a_hi[i], a_lo[i]);
let lifted = l_shl(&mut ctx, value, 1);
a[i] = round(&mut ctx, lifted);
mem.previous_a[i - 1] = a[i];
}
mem.previous_rc[0] = rc[0];
mem.previous_rc[1] = rc[1];
(a, rc)
}
fn prediction_gain(
ctx: &mut DspContext,
gain_hi: Word16,
gain_lo: Word16,
k_hi: Word16,
k_lo: Word16,
) -> (Word16, Word16, i16) {
let squared = mpy_32(k_hi, k_lo, k_hi, k_lo);
let squared = l_abs(ctx, squared);
let complement = l_sub(ctx, Word32(0x7fff_ffff), squared);
let (hi, lo) = l_extract(complement);
let gain = mpy_32(gain_hi, gain_lo, hi, lo);
let exp = norm_l(gain);
let gain = l_shl(ctx, gain, exp);
let (hi, lo) = l_extract(gain);
(hi, lo, exp)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IspSearch {
pub isp: [Word16; LP_ORDER],
pub accepted_at: [usize; LP_ORDER - 1],
pub roots_found: usize,
pub fell_back: bool,
}
#[must_use]
pub fn az_isp(a: &[Word16; LP_ORDER + 1], old_isp: &[Word16; LP_ORDER]) -> [Word16; LP_ORDER] {
az_isp_detail(a, old_isp).isp
}
#[must_use]
pub fn az_isp_detail(a: &[Word16; LP_ORDER + 1], old_isp: &[Word16; LP_ORDER]) -> IspSearch {
let mut ctx = DspContext::default();
let (f1, f2) = sum_difference_polynomials(&mut ctx, a);
let mut isp = [Word16(0); LP_ORDER];
let mut accepted_at = [0usize; LP_ORDER - 1];
let mut found = 0usize;
let mut on_f1 = true;
let mut xlow = GRID[0];
let mut ylow = evaluate(xlow, &f1, &f2, on_f1);
let mut j = 0usize;
while found < LP_ORDER - 1 && j < GRID_POINTS {
j += 1;
let mut xhigh = xlow;
let mut yhigh = ylow;
xlow = GRID[j];
ylow = evaluate(xlow, &f1, &f2, on_f1);
if l_mult(&mut ctx, ylow, yhigh).0 > 0 {
continue;
}
for _ in 0..2 {
let half_low = shr(&mut ctx, xlow, 1);
let half_high = shr(&mut ctx, xhigh, 1);
let xmid = add(&mut ctx, half_low, half_high);
let ymid = evaluate(xmid, &f1, &f2, on_f1);
if l_mult(&mut ctx, ylow, ymid).0 <= 0 {
yhigh = ymid;
xhigh = xmid;
} else {
ylow = ymid;
xlow = xmid;
}
}
let root = interpolate_root(&mut ctx, xlow, xhigh, ylow, yhigh);
isp[found] = root;
accepted_at[found] = j;
found += 1;
xlow = root;
on_f1 = !on_f1;
ylow = evaluate(xlow, &f1, &f2, on_f1);
}
if found < LP_ORDER - 1 {
return IspSearch {
isp: *old_isp,
accepted_at,
roots_found: found,
fell_back: true,
};
}
isp[LP_ORDER - 1] = shl(&mut ctx, a[LP_ORDER], 3);
IspSearch {
isp,
accepted_at,
roots_found: found,
fell_back: false,
}
}
fn sum_difference_polynomials(
ctx: &mut DspContext,
a: &[Word16; LP_ORDER + 1],
) -> ([Word16; NC + 1], [Word16; NC]) {
let mut f1 = [Word16(0); NC + 1];
let mut f2 = [Word16(0); NC];
for i in 0..NC {
let base = l_mult(ctx, a[i], Word16(16384));
let sum = l_mac(ctx, base, a[LP_ORDER - i], Word16(16384));
f1[i] = round(ctx, sum);
let difference = l_msu(ctx, base, a[LP_ORDER - i], Word16(16384));
f2[i] = round(ctx, difference);
}
f1[NC] = a[NC];
for i in 2..NC {
f2[i] = add(ctx, f2[i], f2[i - 2]);
}
(f1, f2)
}
fn evaluate(x: Word16, f1: &[Word16; NC + 1], f2: &[Word16; NC], on_f1: bool) -> Word16 {
if on_f1 {
chebps2(x, f1, NC)
} else {
chebps2(x, f2, NC - 1)
}
}
fn interpolate_root(
ctx: &mut DspContext,
xlow: Word16,
xhigh: Word16,
ylow: Word16,
yhigh: Word16,
) -> Word16 {
let dx = sub(ctx, xhigh, xlow);
let dy = sub(ctx, yhigh, ylow);
if dy.0 == 0 {
return xlow;
}
let sign = dy;
let magnitude = abs_s(ctx, dy);
let exp = norm_s(magnitude);
let normalised = shl(ctx, magnitude, exp);
let reciprocal = div_s(Word16(16383), normalised);
let scaled = l_mult(ctx, dx, reciprocal);
let shift = sub(ctx, Word16(20), Word16(exp));
let slope = l_shr(ctx, scaled, shift.0);
let mut slope = extract_l(slope);
if sign.0 < 0 {
slope = negate(ctx, slope);
}
let step = l_mult(ctx, ylow, slope);
let step = l_shr(ctx, step, 11);
sub(ctx, xlow, extract_l(step))
}
fn chebps2(x: Word16, f: &[Word16], n: usize) -> Word16 {
let mut ctx = DspContext::default();
let seed = l_mult(&mut ctx, f[0], Word16(4096));
let (mut b2_hi, mut b2_lo) = l_extract(seed);
let mut acc = mpy_32_16(b2_hi, b2_lo, x);
acc = l_shl(&mut ctx, acc, 1);
acc = l_mac(&mut ctx, acc, f[1], Word16(4096));
let (mut b1_hi, mut b1_lo) = l_extract(acc);
for &coefficient in f.iter().take(n).skip(2) {
let mut acc = mpy_32_16(b1_hi, b1_lo, x);
acc = l_mac(&mut ctx, acc, b2_hi, Word16(-16384));
acc = l_mac(&mut ctx, acc, coefficient, Word16(2048));
acc = l_shl(&mut ctx, acc, 1);
acc = l_msu(&mut ctx, acc, b2_lo, Word16(1));
let (b0_hi, b0_lo) = l_extract(acc);
b2_hi = b1_hi;
b2_lo = b1_lo;
b1_hi = b0_hi;
b1_lo = b0_lo;
}
let mut acc = mpy_32_16(b1_hi, b1_lo, x);
acc = l_mac(&mut ctx, acc, b2_hi, Word16(-32768));
acc = l_msu(&mut ctx, acc, b2_lo, Word16(1));
acc = l_mac(&mut ctx, acc, f[n], Word16(2048));
let acc = l_shl(&mut ctx, acc, 6);
let value = extract_h(acc);
if value.0 == -32768 {
Word16(-32767)
} else {
value
}
}
#[must_use]
pub fn weight_a(a: &[Word16; LP_ORDER + 1], gamma: Word16) -> [Word16; LP_ORDER + 1] {
let mut ctx = DspContext::default();
let mut ap = [Word16(0); LP_ORDER + 1];
ap[0] = a[0];
let mut fac = gamma;
for i in 1..LP_ORDER {
let scaled = l_mult(&mut ctx, a[i], fac);
ap[i] = round(&mut ctx, scaled);
let next = l_mult(&mut ctx, fac, gamma);
fac = round(&mut ctx, next);
}
let scaled = l_mult(&mut ctx, a[LP_ORDER], fac);
ap[LP_ORDER] = round(&mut ctx, scaled);
ap
}
pub fn residu(a: &[Word16], signal: &[Word16], out: &mut [Word16]) {
let m = a.len() - 1;
assert_eq!(
signal.len(),
m + out.len(),
"residu needs `m` samples of history in front of the block"
);
let mut ctx = DspContext::default();
for (i, slot) in out.iter_mut().enumerate() {
let window = &signal[i..=i + m];
let mut acc = l_mult(&mut ctx, window[m], a[0]);
for j in 1..=m {
acc = l_mac(&mut ctx, acc, a[j], window[m - j]);
}
let acc = l_shl(&mut ctx, acc, 4);
*slot = round(&mut ctx, acc);
}
}
pub fn deemph2(x: &mut [Word16], mu: Word16, memory: &mut Word16) {
let mut ctx = DspContext::default();
let mut previous = *memory;
for slot in x.iter_mut() {
let mut acc = l_mult(&mut ctx, *slot, Word16(16384));
acc = l_mac(&mut ctx, acc, previous, mu);
*slot = round(&mut ctx, acc);
previous = *slot;
}
*memory = previous;
}
pub fn lp_decim2(x: &mut [Word16], memory: &mut [Word16; 3]) {
let n = x.len();
assert!(
n <= L_FRAME && n.is_multiple_of(2),
"decimation needs an even block"
);
let mut ctx = DspContext::default();
let mut buffer = [Word16(0); 3 + L_FRAME];
buffer[..3].copy_from_slice(memory);
buffer[3..3 + n].copy_from_slice(x);
memory.copy_from_slice(&x[n - 3..n]);
for j in 0..n / 2 {
let mut acc = Word32(0);
for (k, &tap) in H_FIR.iter().enumerate() {
acc = l_mac(&mut ctx, acc, buffer[2 * j + k], tap);
}
x[j] = round(&mut ctx, acc);
}
}
#[derive(Clone, Debug)]
pub struct FrontEndFrame {
pub window: [Word16; L_TOTAL],
pub scaling: Scaling,
pub wsp_shift: i16,
pub wsp_exp: i16,
pub autocorr_raw: Autocorrelation,
pub autocorr: Autocorrelation,
pub a: [Word16; LP_ORDER + 1],
pub rc: [Word16; LP_ORDER],
pub isp: [Word16; LP_ORDER],
pub isf: [Word16; LP_ORDER],
pub a_interp: [[Word16; LP_ORDER + 1]; NB_SUBFR],
pub wsp: [Word16; L_FRAME / OPL_DECIM],
pub wsp_history: [Word16; WSP_HISTORY],
}
#[derive(Clone, Debug)]
pub struct FrontEnd {
preproc: Preprocessor,
old_speech: [Word16; L_TOTAL - L_FRAME],
old_wsp: [Word16; WSP_HISTORY],
levinson: LevinsonMemory,
isp_old: [Word16; LP_ORDER],
wsp_memory: Word16,
decim2_memory: [Word16; 3],
old_wsp_max: Word16,
old_wsp_shift: i16,
}
impl Default for FrontEnd {
fn default() -> Self {
Self::new()
}
}
impl FrontEnd {
#[must_use]
pub const fn new() -> Self {
Self {
preproc: Preprocessor::new(),
old_speech: [Word16(0); L_TOTAL - L_FRAME],
old_wsp: [Word16(0); WSP_HISTORY],
levinson: LevinsonMemory::new(),
isp_old: ISP_INIT,
wsp_memory: Word16(0),
decim2_memory: [Word16(0); 3],
old_wsp_max: Word16(0),
old_wsp_shift: 0,
}
}
pub fn process_frame(&mut self, speech16k: &[Word16; L_FRAME16K]) -> FrontEndFrame {
let mut ctx = DspContext::default();
let mut window = [Word16(0); L_TOTAL];
window[..L_TOTAL - L_FRAME].copy_from_slice(&self.old_speech);
self.preproc
.band_limit(speech16k, &mut window[NEW_SPEECH..]);
let scaling = self.preproc.preemphasise(&mut window[NEW_SPEECH..]);
scale_sig(&mut ctx, &mut window[..NEW_SPEECH], scaling.exp);
scale_sig(&mut ctx, &mut self.decim2_memory, scaling.exp);
scale_sig(
&mut ctx,
core::slice::from_mut(&mut self.wsp_memory),
scaling.exp,
);
let autocorr_raw = autocorrelation(&window);
let mut autocorr = autocorr_raw;
lag_window(&mut autocorr);
let (a, rc) = levinson(&autocorr, &mut self.levinson);
let isp = az_isp(&a, &self.isp_old);
let a_interp = interpolate_isp(&self.isp_old, &isp);
self.isp_old = isp;
let isf = isp_to_isf(&isp);
let (wsp, wsp_history, wsp_shift, wsp_exp) =
self.weighted_speech(&window, &a_interp, scaling);
self.old_speech.copy_from_slice(&window[L_FRAME..]);
FrontEndFrame {
window,
scaling,
wsp_shift,
wsp_exp,
autocorr_raw,
autocorr,
a,
rc,
isp,
isf,
a_interp,
wsp,
wsp_history,
}
}
fn weighted_speech(
&mut self,
window: &[Word16; L_TOTAL],
a_interp: &[[Word16; LP_ORDER + 1]; NB_SUBFR],
scaling: Scaling,
) -> (
[Word16; L_FRAME / OPL_DECIM],
[Word16; WSP_HISTORY],
i16,
i16,
) {
let mut ctx = DspContext::default();
let mut buffer = [Word16(0); L_FRAME + WSP_HISTORY];
buffer[..WSP_HISTORY].copy_from_slice(&self.old_wsp);
for (k, a) in a_interp.iter().enumerate() {
let ap = weight_a(a, GAMMA1);
let start = k * L_SUBFR;
let source = &window[SPEECH + start - LP_ORDER..SPEECH + start + L_SUBFR];
residu(
&ap,
source,
&mut buffer[WSP_HISTORY + start..WSP_HISTORY + start + L_SUBFR],
);
}
deemph2(
&mut buffer[WSP_HISTORY..WSP_HISTORY + L_FRAME],
TILT_FAC,
&mut self.wsp_memory,
);
let mut peak = Word16(0);
for i in 0..L_FRAME {
let magnitude = abs_s(&mut ctx, buffer[WSP_HISTORY + i]);
if magnitude.0 > peak.0 {
peak = magnitude;
}
}
let reference = peak.max(self.old_wsp_max);
self.old_wsp_max = peak;
let wsp_shift = sub(&mut ctx, Word16(norm_s(reference)), Word16(3)).0.min(0);
lp_decim2(
&mut buffer[WSP_HISTORY..WSP_HISTORY + L_FRAME],
&mut self.decim2_memory,
);
let decimated = L_FRAME / OPL_DECIM;
scale_sig(
&mut ctx,
&mut buffer[WSP_HISTORY..WSP_HISTORY + decimated],
wsp_shift,
);
let change = sub(&mut ctx, Word16(wsp_shift), Word16(self.old_wsp_shift));
let wsp_exp = add(&mut ctx, Word16(scaling.exp), change).0;
self.old_wsp_shift = wsp_shift;
scale_sig(&mut ctx, &mut buffer[..WSP_HISTORY], wsp_exp);
let mut wsp = [Word16(0); L_FRAME / OPL_DECIM];
wsp.copy_from_slice(&buffer[WSP_HISTORY..WSP_HISTORY + decimated]);
let mut history = [Word16(0); WSP_HISTORY];
history.copy_from_slice(&buffer[..WSP_HISTORY]);
self.old_wsp
.copy_from_slice(&buffer[decimated..decimated + WSP_HISTORY]);
(wsp, history, wsp_shift, wsp_exp)
}
}
#[cfg(test)]
mod tests {
use super::super::preproc::restrict_to_14_bit;
use super::super::preproc::trace_support::{frames, input_frame, scalar, words};
use super::*;
fn replay(mut check: impl FnMut(usize, &FrontEndFrame)) -> usize {
let mut front = FrontEnd::new();
let total = frames();
for frame in 0..total {
let mut input = input_frame(frame);
restrict_to_14_bit(&mut input);
let result = front.process_frame(&input);
check(frame, &result);
}
total
}
fn compare(frame: usize, label: &str, got: &[Word16], want: &[Word16]) -> usize {
assert_eq!(got.len(), want.len(), "frame {frame}: {label} length");
for (i, (&g, &w)) in got.iter().zip(want.iter()).enumerate() {
assert_eq!(
g.0, w.0,
"frame {frame}: {label}[{i}] = {} but the reference gives {}",
g.0, w.0
);
}
got.len()
}
#[test]
fn analysis_window_is_bit_exact_against_ts26173() {
let mut compared = 0usize;
let count = replay(|frame, got| {
compared += compare(
frame,
"window",
&got.window,
&words(frame, "window", L_TOTAL),
);
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(compared, 3 * L_TOTAL, "1152 window samples compared");
}
#[test]
fn autocorrelation_is_bit_exact_against_ts26173() {
let mut compared = 0usize;
let count = replay(|frame, got| {
compared += compare(
frame,
"r_h_pre",
&got.autocorr_raw.high,
&words(frame, "r_h_pre", LP_ORDER + 1),
);
compared += compare(
frame,
"r_l_pre",
&got.autocorr_raw.low,
&words(frame, "r_l_pre", LP_ORDER + 1),
);
compared += compare(
frame,
"r_h",
&got.autocorr.high,
&words(frame, "r_h", LP_ORDER + 1),
);
compared += compare(
frame,
"r_l",
&got.autocorr.low,
&words(frame, "r_l", LP_ORDER + 1),
);
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(compared, 3 * 4 * (LP_ORDER + 1), "204 lag values compared");
}
#[test]
fn levinson_is_bit_exact_against_ts26173() {
let mut compared = 0usize;
let count = replay(|frame, got| {
compared += compare(frame, "A", &got.a, &words(frame, "A", LP_ORDER + 1));
compared += compare(frame, "rc", &got.rc, &words(frame, "rc", LP_ORDER));
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(
compared,
3 * (LP_ORDER + 1 + LP_ORDER),
"99 values compared"
);
}
#[test]
fn az_isp_is_bit_exact_against_ts26173() {
let mut compared = 0usize;
let count = replay(|frame, got| {
compared += compare(frame, "ispnew", &got.isp, &words(frame, "ispnew", LP_ORDER));
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(compared, 3 * LP_ORDER, "48 ISPs compared");
}
#[test]
fn unquantised_isf_is_bit_exact_against_ts26173() {
let mut compared = 0usize;
let count = replay(|frame, got| {
compared += compare(
frame,
"isf_unq46",
&got.isf,
&words(frame, "isf_unq46", LP_ORDER),
);
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(compared, 3 * LP_ORDER, "48 ISFs compared");
}
#[test]
fn interpolated_predictors_are_bit_exact_against_ts26173() {
let mut compared = 0usize;
let count = replay(|frame, got| {
let want = words(frame, "A_interp", NB_SUBFR * (LP_ORDER + 1));
let flat: Vec<Word16> = got.a_interp.iter().flatten().copied().collect();
compared += compare(frame, "A_interp", &flat, &want);
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(
compared,
3 * NB_SUBFR * (LP_ORDER + 1),
"204 values compared"
);
}
#[test]
fn weighted_speech_is_bit_exact_against_ts26173() {
let mut compared = 0usize;
let mut shifts = Vec::new();
let count = replay(|frame, got| {
compared += compare(
frame,
"wsp",
&got.wsp,
&words(frame, "wsp", L_FRAME / OPL_DECIM),
);
assert_eq!(
i32::from(got.wsp_shift),
scalar(frame, "wsp_shift"),
"frame {frame}: wsp_shift"
);
shifts.push(got.wsp_shift);
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(compared, 3 * L_FRAME / OPL_DECIM, "384 samples compared");
assert_eq!(
shifts,
vec![-1, -1, 0],
"wsp_shift must vary across the trace"
);
}
#[test]
fn the_root_search_finds_fifteen_roots_inside_their_own_grid_intervals() {
let count = replay(|frame, got| {
let search = az_isp_detail(&got.a, &ISP_INIT);
assert_eq!(
search.roots_found,
LP_ORDER - 1,
"frame {frame}: root count"
);
assert!(!search.fell_back, "frame {frame}: fell back to old ISPs");
let mut previous_index = 0usize;
for (n, &j) in search.accepted_at.iter().enumerate() {
assert!(
j > previous_index,
"frame {frame}: root {n} accepted at grid {j}, not after {previous_index}"
);
let upper = if n == 0 {
GRID[j - 1]
} else {
search.isp[n - 1].max(GRID[j - 1])
};
assert!(
search.isp[n].0 >= GRID[j].0 && search.isp[n].0 <= upper.0,
"frame {frame}: root {n} = {} outside [{}, {}]",
search.isp[n].0,
GRID[j].0,
upper.0
);
previous_index = j;
}
for n in 1..LP_ORDER - 1 {
assert!(
search.isp[n].0 < search.isp[n - 1].0,
"frame {frame}: roots {} and {n} are not descending",
n - 1
);
}
let mut ctx = DspContext::default();
assert_eq!(search.isp[LP_ORDER - 1], shl(&mut ctx, got.a[LP_ORDER], 3));
});
assert_eq!(count, 3, "the committed trace covers three frames");
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Variant {
Reference,
MidpointRoundsTogether,
StrictSignChange,
NoRootInheritance,
NoReevaluation,
FourBisections,
}
fn search(a: &[Word16; LP_ORDER + 1], variant: Variant) -> [Word16; LP_ORDER] {
let mut ctx = DspContext::default();
let (f1, f2) = sum_difference_polynomials(&mut ctx, a);
let mut isp = [Word16(0); LP_ORDER];
let mut found = 0usize;
let mut on_f1 = true;
let mut xlow = GRID[0];
let mut ylow = evaluate(xlow, &f1, &f2, on_f1);
let mut j = 0usize;
while found < LP_ORDER - 1 && j < GRID_POINTS {
j += 1;
let mut xhigh = xlow;
let mut yhigh = ylow;
xlow = GRID[j];
ylow = evaluate(xlow, &f1, &f2, on_f1);
let product = l_mult(&mut ctx, ylow, yhigh).0;
let bracketed = if variant == Variant::StrictSignChange {
product < 0
} else {
product <= 0
};
if !bracketed {
continue;
}
let halvings = if variant == Variant::FourBisections {
4
} else {
2
};
for _ in 0..halvings {
let xmid = if variant == Variant::MidpointRoundsTogether {
Word16(((i32::from(xlow.0) + i32::from(xhigh.0)) >> 1) as i16)
} else {
let half_low = shr(&mut ctx, xlow, 1);
let half_high = shr(&mut ctx, xhigh, 1);
add(&mut ctx, half_low, half_high)
};
let ymid = evaluate(xmid, &f1, &f2, on_f1);
if l_mult(&mut ctx, ylow, ymid).0 <= 0 {
yhigh = ymid;
xhigh = xmid;
} else {
ylow = ymid;
xlow = xmid;
}
}
let root = interpolate_root(&mut ctx, xlow, xhigh, ylow, yhigh);
isp[found] = root;
found += 1;
if variant == Variant::NoRootInheritance {
xlow = GRID[j];
} else {
xlow = root;
}
on_f1 = !on_f1;
if variant != Variant::NoReevaluation {
ylow = evaluate(xlow, &f1, &f2, on_f1);
}
}
if found < LP_ORDER - 1 {
return ISP_INIT;
}
isp[LP_ORDER - 1] = shl(&mut ctx, a[LP_ORDER], 3);
isp
}
#[test]
fn the_search_decisions_are_the_ones_the_trace_pins_down() {
let alternatives = [
Variant::MidpointRoundsTogether,
Variant::StrictSignChange,
Variant::NoRootInheritance,
Variant::NoReevaluation,
Variant::FourBisections,
];
let mut distinguished = vec![false; alternatives.len()];
let count = replay(|frame, got| {
assert_eq!(
search(&got.a, Variant::Reference),
got.isp,
"frame {frame}: the parameterised copy must agree with az_isp"
);
for (n, &variant) in alternatives.iter().enumerate() {
if search(&got.a, variant) != got.isp {
distinguished[n] = true;
}
}
});
assert_eq!(count, 3, "the committed trace covers three frames");
let undistinguished: Vec<Variant> = alternatives
.iter()
.zip(distinguished.iter())
.filter_map(|(&v, &d)| (!d).then_some(v))
.collect();
assert_eq!(
undistinguished,
vec![Variant::StrictSignChange],
"the set of search decisions this trace cannot distinguish changed"
);
}
#[test]
fn an_exact_zero_at_a_grid_point_counts_as_a_sign_change() {
let mut ctx = DspContext::default();
for (ylow, yhigh) in [(0i16, 5i16), (5, 0), (0, 0), (0, -5), (-5, 0)] {
assert!(
l_mult(&mut ctx, Word16(ylow), Word16(yhigh)).0 <= 0,
"({ylow}, {yhigh}) must bracket a root"
);
}
for (ylow, yhigh) in [(5i16, 7i16), (-5, -7)] {
assert!(
l_mult(&mut ctx, Word16(ylow), Word16(yhigh)).0 > 0,
"({ylow}, {yhigh}) must not bracket a root"
);
}
}
#[test]
fn a_failed_search_returns_every_old_isp_including_the_last() {
let mut a = [Word16(0); LP_ORDER + 1];
a[0] = Word16(4096);
a[NC] = Word16(4096);
a[LP_ORDER] = Word16(-4096);
let old: [Word16; LP_ORDER] = ISP_INIT;
let search = az_isp_detail(&a, &old);
assert!(search.fell_back, "this predictor must fail the search");
assert_eq!(search.roots_found, 0, "F1 has no sign change to find");
assert_eq!(search.isp, old, "all sixteen ISPs come from old_isp");
let mut ctx = DspContext::default();
assert_ne!(search.isp[LP_ORDER - 1], shl(&mut ctx, a[LP_ORDER], 3));
}
#[test]
fn the_grid_matches_its_generating_formula() {
assert_eq!(GRID[0].0, 32767);
assert_eq!(GRID[GRID_POINTS].0, -32760);
#[allow(clippy::cast_precision_loss)]
for (i, point) in GRID.iter().enumerate().take(GRID_POINTS).skip(1) {
let want =
((std::f64::consts::PI * i as f64 / GRID_POINTS as f64).cos() * 32768.0).round();
assert!(
(f64::from(point.0) - want).abs() <= 1.0,
"grid[{i}] = {} but cos gives {want}",
point.0
);
}
for (i, pair) in GRID.windows(2).enumerate() {
assert!(pair[1].0 < pair[0].0, "grid must descend at {}", i + 1);
}
}
#[test]
fn an_unstable_reflection_coefficient_keeps_the_previous_filter() {
let mut mem = LevinsonMemory::new();
for i in 0..LP_ORDER {
mem.previous_a[i] = Word16(100 + i as i16);
}
mem.previous_rc = [Word16(-1000), Word16(2000)];
let before = mem.clone();
let mut r = Autocorrelation::default();
let (hi, lo) = l_extract(Word32(0x7fff_ffff));
r.high[0] = hi;
r.low[0] = lo;
let (hi, lo) = l_extract(Word32(-0x7fff_ffff));
r.high[2] = hi;
r.low[2] = lo;
let (a, rc) = levinson(&r, &mut mem);
assert_eq!(a[0].0, 4096, "a[0] is written even on the unstable path");
assert_eq!(
&a[1..],
&before.previous_a[..],
"the old filter is re-emitted"
);
assert_eq!(rc[0], before.previous_rc[0]);
assert_eq!(rc[1], before.previous_rc[1]);
assert_eq!(mem, before, "the unstable path must not update the memory");
}
#[test]
fn levinson_updates_its_memory_on_the_stable_path() {
let count = replay(|frame, got| {
let mut mem = LevinsonMemory::new();
let (a, rc) = levinson(&got.autocorr, &mut mem);
assert_eq!(&mem.previous_a[..], &a[1..], "frame {frame}: stored A");
assert_eq!(mem.previous_rc, [rc[0], rc[1]], "frame {frame}: stored rc");
});
assert_eq!(count, 3, "the committed trace covers three frames");
}
#[test]
fn weighting_builds_its_gamma_powers_by_iterated_rounding() {
let count = replay(|frame, got| {
let ap = weight_a(&got.a_interp[0], GAMMA1);
assert_eq!(ap[0], got.a_interp[0][0], "frame {frame}: ap[0] is a copy");
let mut ctx = DspContext::default();
let mut fac = GAMMA1;
for (i, (&weighted, &plain)) in
ap.iter().zip(got.a_interp[0].iter()).enumerate().skip(1)
{
let scaled = l_mult(&mut ctx, plain, fac);
let want = round(&mut ctx, scaled);
assert_eq!(weighted, want, "frame {frame}: ap[{i}]");
if i < LP_ORDER {
let next = l_mult(&mut ctx, fac, GAMMA1);
fac = round(&mut ctx, next);
}
}
});
assert_eq!(count, 3, "the committed trace covers three frames");
let mut ctx = DspContext::default();
let mut fac = GAMMA1;
let mut exact = f64::from(GAMMA1.0) / 32768.0;
let mut diverged = false;
for _ in 1..LP_ORDER {
let next = l_mult(&mut ctx, fac, GAMMA1);
fac = round(&mut ctx, next);
exact *= f64::from(GAMMA1.0) / 32768.0;
#[allow(clippy::cast_possible_truncation)]
let single_rounding = (exact * 32768.0).round() as i16;
if single_rounding != fac.0 {
diverged = true;
}
}
assert!(diverged, "the iterated ladder must drift from gamma^i");
}
}