#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::many_single_char_names,
clippy::similar_names,
clippy::unreadable_literal
)]
use super::super::decoder_tables::{
GAMMA1, GAMMA1_12K2, GAMMA2, LAG_WINDOW_H, LAG_WINDOW_L, LP_WINDOW_160_80, LP_WINDOW_200_40,
LP_WINDOW_232_8, LSP_GRID,
};
use super::super::lsp::{AZ_SIZE, M, MP1};
use super::super::synthesis::{
expand_bandwidth, lp_residual, synthesis_filter, synthesis_filter_in_place,
};
use super::super::{L_FRAME, L_SUBFR};
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, mult_r};
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, MAX_32};
pub const MR475: u8 = 0;
pub const MR515: u8 = 1;
pub const MR795: u8 = 5;
pub const MR102: u8 = 6;
pub const MR122: u8 = 7;
pub const MRDTX: u8 = 8;
pub const L_WINDOW: usize = 240;
pub const L_TOTAL: usize = 320;
pub const L_NEXT: usize = 40;
const CODED_BASE: usize = L_TOTAL - L_FRAME - L_NEXT;
const NC: usize = M / 2;
const GRID_POINTS: usize = 60;
const K_UNSTABLE: Word16 = Word16(32750);
const FLAT_FILTER: Word16 = Word16(4096);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpeechBuffer {
samples: [Word16; L_TOTAL],
}
impl Default for SpeechBuffer {
fn default() -> Self {
Self::new()
}
}
impl SpeechBuffer {
#[must_use]
pub const fn new() -> Self {
Self {
samples: [Word16(0); L_TOTAL],
}
}
pub fn push(&mut self, frame: &[Word16; L_FRAME]) {
self.samples[L_TOTAL - L_FRAME..].copy_from_slice(frame);
}
pub fn shift(&mut self) {
self.samples.copy_within(L_FRAME.., 0);
}
#[must_use]
pub fn analysis_window(&self) -> &[Word16; L_WINDOW] {
self.samples[L_TOTAL - L_WINDOW..]
.try_into()
.expect("the window is the tail of the buffer")
}
#[must_use]
pub fn analysis_window_12k2(&self) -> &[Word16; L_WINDOW] {
self.samples[L_TOTAL - L_WINDOW - L_NEXT..L_TOTAL - L_NEXT]
.try_into()
.expect("the 12.2 window is one lookahead earlier")
}
#[must_use]
pub fn newest(&self) -> &[Word16; L_FRAME] {
self.samples[L_TOTAL - L_FRAME..]
.try_into()
.expect("the newest frame is the tail of the buffer")
}
#[must_use]
pub fn vad_window(&self) -> &[Word16; L_NEXT + L_FRAME] {
self.samples[L_TOTAL - L_FRAME - L_NEXT..]
.try_into()
.expect("the detector window is the tail of the buffer")
}
#[must_use]
pub fn coded(&self) -> &[Word16; L_FRAME] {
self.samples[CODED_BASE..CODED_BASE + L_FRAME]
.try_into()
.expect("the coded frame sits one lookahead into the buffer")
}
#[must_use]
pub fn with_history(&self, offset: usize, len: usize) -> &[Word16] {
assert!(offset + len <= L_FRAME, "window runs past the coded frame");
let base = CODED_BASE + offset - M;
&self.samples[base..base + M + len]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Autocorrelation {
pub r_h: [Word16; MP1],
pub r_l: [Word16; MP1],
pub norm: i16,
}
#[must_use]
pub fn autocorrelate(
ctx: &mut DspContext,
x: &[Word16; L_WINDOW],
window: &[i16; L_WINDOW],
) -> Autocorrelation {
let mut y = [Word16(0); L_WINDOW];
for (slot, (&sample, &w)) in y.iter_mut().zip(x.iter().zip(window.iter())) {
*slot = mult_r(ctx, sample, Word16(w));
}
let mut overfl_shft = Word16(0);
let mut sum;
loop {
sum = Word32(0);
for &v in &y {
sum = l_mac(ctx, sum, v, v);
}
if l_sub(ctx, sum, Word32(MAX_32)).0 != 0 {
break;
}
overfl_shft = add(ctx, overfl_shft, Word16(4));
for v in &mut y {
*v = shr(ctx, *v, 2);
}
}
sum = l_add(ctx, sum, Word32(1));
let norm = norm_l(sum);
sum = l_shl(ctx, sum, norm);
let mut r_h = [Word16(0); MP1];
let mut r_l = [Word16(0); MP1];
(r_h[0], r_l[0]) = l_extract(sum);
for lag in 1..=M {
let mut sum = Word32(0);
for j in 0..L_WINDOW - lag {
sum = l_mac(ctx, sum, y[j], y[j + lag]);
}
sum = l_shl(ctx, sum, norm);
(r_h[lag], r_l[lag]) = l_extract(sum);
}
Autocorrelation {
r_h,
r_l,
norm: sub(ctx, Word16(norm), overfl_shft).0,
}
}
pub fn lag_window(r: &mut Autocorrelation) {
for i in 1..=M {
let x = mpy_32(
r.r_h[i],
r.r_l[i],
Word16(LAG_WINDOW_H[i - 1]),
Word16(LAG_WINDOW_L[i - 1]),
);
(r.r_h[i], r.r_l[i]) = l_extract(x);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Levinson {
old_a: [Word16; MP1],
}
impl Default for Levinson {
fn default() -> Self {
Self::new()
}
}
impl Levinson {
#[must_use]
pub const fn new() -> Self {
let mut old_a = [Word16(0); MP1];
old_a[0] = FLAT_FILTER;
Self { old_a }
}
pub fn solve(
&mut self,
ctx: &mut DspContext,
r: &Autocorrelation,
) -> ([Word16; MP1], [Word16; 4]) {
let (rh, rl) = (&r.r_h, &r.r_l);
let mut rc = [Word16(0); 4];
let mut ah = [Word16(0); MP1];
let mut al = [Word16(0); MP1];
let mut anh = [Word16(0); MP1];
let mut anl = [Word16(0); MP1];
let t1 = l_comp(rh[1], rl[1]);
let t2 = l_abs(ctx, t1);
let mut t0 = div_32(t2, rh[0], rl[0]);
if t1.0 > 0 {
t0 = l_negate(ctx, t0);
}
let (mut kh, mut kl) = l_extract(t0);
rc[0] = round(ctx, t0);
t0 = l_shr(ctx, t0, 4);
(ah[1], al[1]) = l_extract(t0);
let mut t0 = mpy_32(kh, kl, kh, kl);
t0 = l_abs(ctx, t0);
t0 = l_sub(ctx, Word32(MAX_32), t0);
let (hi, lo) = l_extract(t0);
t0 = mpy_32(rh[0], rl[0], hi, lo);
let mut alp_exp = norm_l(t0);
t0 = l_shl(ctx, t0, alp_exp);
let (mut alp_h, mut alp_l) = l_extract(t0);
for i in 2..=M {
let mut t0 = Word32(0);
for j in 1..i {
t0 = l_add(ctx, t0, mpy_32(rh[j], rl[j], ah[i - j], al[i - j]));
}
t0 = l_shl(ctx, t0, 4);
t0 = l_add(ctx, t0, l_comp(rh[i], rl[i]));
let t1 = l_abs(ctx, t0);
let mut t2 = div_32(t1, alp_h, alp_l);
if t0.0 > 0 {
t2 = l_negate(ctx, t2);
}
t2 = l_shl(ctx, t2, alp_exp);
(kh, kl) = l_extract(t2);
if i < 5 {
rc[i - 1] = round(ctx, t2);
}
let magnitude = abs_s(ctx, kh);
if sub(ctx, magnitude, K_UNSTABLE).0 > 0 {
return (self.old_a, [Word16(0); 4]);
}
for j in 1..i {
let mut t = mpy_32(kh, kl, ah[i - j], al[i - j]);
t = l_add(ctx, t, l_comp(ah[j], al[j]));
(anh[j], anl[j]) = l_extract(t);
}
t2 = l_shr(ctx, t2, 4);
(anh[i], anl[i]) = l_extract(t2);
let mut t = mpy_32(kh, kl, kh, kl);
t = l_abs(ctx, t);
t = l_sub(ctx, Word32(MAX_32), t);
let (hi, lo) = l_extract(t);
t = mpy_32(alp_h, alp_l, hi, lo);
let shift = norm_l(t);
t = l_shl(ctx, t, shift);
(alp_h, alp_l) = l_extract(t);
alp_exp = add(ctx, Word16(alp_exp), Word16(shift)).0;
ah[1..=i].copy_from_slice(&anh[1..=i]);
al[1..=i].copy_from_slice(&anl[1..=i]);
}
let mut a = [Word16(0); MP1];
a[0] = FLAT_FILTER;
for i in 1..=M {
let t0 = l_shl(ctx, l_comp(ah[i], al[i]), 1);
a[i] = round(ctx, t0);
self.old_a[i] = a[i];
}
(a, rc)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LpAnalysis {
levinson: Levinson,
}
impl LpAnalysis {
#[must_use]
pub const fn new() -> Self {
Self {
levinson: Levinson::new(),
}
}
pub fn analyse(
&mut self,
ctx: &mut DspContext,
mode: u8,
speech: &SpeechBuffer,
az: &mut [Word16; AZ_SIZE],
) {
if mode == MR122 {
let window = speech.analysis_window_12k2();
let mid = self.one_analysis(ctx, window, &LP_WINDOW_160_80);
az[MP1..2 * MP1].copy_from_slice(&mid);
let end = self.one_analysis(ctx, window, &LP_WINDOW_232_8);
az[3 * MP1..].copy_from_slice(&end);
} else {
let window = speech.analysis_window();
let end = self.one_analysis(ctx, window, &LP_WINDOW_200_40);
az[3 * MP1..].copy_from_slice(&end);
}
}
fn one_analysis(
&mut self,
ctx: &mut DspContext,
x: &[Word16; L_WINDOW],
window: &[i16; L_WINDOW],
) -> [Word16; MP1] {
let mut r = autocorrelate(ctx, x, window);
lag_window(&mut r);
let (a, _rc) = self.levinson.solve(ctx, &r);
a
}
}
fn chebps(ctx: &mut DspContext, x: Word16, f: &[Word16; NC + 1]) -> Word16 {
let mut b2_h = Word16(256);
let mut b2_l = Word16(0);
let mut t0 = l_mult(ctx, x, Word16(512));
t0 = l_mac(ctx, t0, f[1], Word16(8192));
let (mut b1_h, mut b1_l) = l_extract(t0);
for &coefficient in &f[2..NC] {
let mut t = mpy_32_16(b1_h, b1_l, x);
t = l_shl(ctx, t, 1);
t = l_mac(ctx, t, b2_h, Word16(-32768));
t = l_msu(ctx, t, b2_l, Word16(1));
t = l_mac(ctx, t, coefficient, Word16(8192));
let (b0_h, b0_l) = l_extract(t);
b2_l = b1_l;
b2_h = b1_h;
b1_l = b0_l;
b1_h = b0_h;
}
let mut t = mpy_32_16(b1_h, b1_l, x);
t = l_mac(ctx, t, b2_h, Word16(-32768));
t = l_msu(ctx, t, b2_l, Word16(1));
t = l_mac(ctx, t, f[NC], Word16(4096));
t = l_shl(ctx, t, 6);
extract_h(t)
}
fn brackets_a_root(ctx: &mut DspContext, a: Word16, b: Word16) -> bool {
l_mult(ctx, a, b).0 <= 0
}
#[must_use]
pub fn az_lsp(ctx: &mut DspContext, a: &[Word16; MP1], old_lsp: &[Word16; M]) -> [Word16; M] {
let mut f1 = [Word16(0); NC + 1];
let mut f2 = [Word16(0); NC + 1];
f1[0] = Word16(1024);
f2[0] = Word16(1024);
for i in 0..NC {
let mut t0 = l_mult(ctx, a[i + 1], Word16(8192));
t0 = l_mac(ctx, t0, a[M - i], Word16(8192));
f1[i + 1] = sub(ctx, extract_h(t0), f1[i]);
let mut t0 = l_mult(ctx, a[i + 1], Word16(8192));
t0 = l_msu(ctx, t0, a[M - i], Word16(8192));
f2[i + 1] = add(ctx, extract_h(t0), f2[i]);
}
let mut lsp = [Word16(0); M];
let mut found = 0usize;
let mut on_f2 = false;
let mut xlow = Word16(LSP_GRID[0]);
let mut ylow = chebps(ctx, xlow, &f1);
let mut j = 0usize;
while found < M && j < GRID_POINTS {
j += 1;
let mut xhigh = xlow;
let mut yhigh = ylow;
xlow = Word16(LSP_GRID[j]);
let coef = if on_f2 { &f2 } else { &f1 };
ylow = chebps(ctx, xlow, coef);
if !brackets_a_root(ctx, ylow, yhigh) {
continue;
}
for _ in 0..4 {
let half_low = shr(ctx, xlow, 1);
let half_high = shr(ctx, xhigh, 1);
let xmid = add(ctx, half_low, half_high);
let ymid = chebps(ctx, xmid, coef);
if brackets_a_root(ctx, ylow, ymid) {
yhigh = ymid;
xhigh = xmid;
} else {
ylow = ymid;
xlow = xmid;
}
}
lsp[found] = interpolate_root(ctx, xlow, xhigh, ylow, yhigh);
xlow = lsp[found];
found += 1;
on_f2 = !on_f2;
let coef = if on_f2 { &f2 } else { &f1 };
ylow = chebps(ctx, xlow, coef);
}
if found < M {
lsp.copy_from_slice(old_lsp);
}
lsp
}
fn interpolate_root(
ctx: &mut DspContext,
xlow: Word16,
xhigh: Word16,
ylow: Word16,
yhigh: Word16,
) -> Word16 {
let x = sub(ctx, xhigh, xlow);
let y = sub(ctx, yhigh, ylow);
if y.0 == 0 {
return xlow;
}
let sign = y;
let magnitude = abs_s(ctx, y);
let exp = norm_s(magnitude);
let normalised = shl(ctx, magnitude, exp);
let reciprocal = div_s(Word16(16383), normalised);
let mut t0 = l_mult(ctx, x, reciprocal);
let shift = sub(ctx, Word16(20), Word16(exp)).0;
t0 = l_shr(ctx, t0, shift);
let mut slope = extract_l(t0);
if sign.0 < 0 {
slope = negate(ctx, slope);
}
let scaled = l_mult(ctx, ylow, slope);
let t0 = l_shr(ctx, scaled, 11);
let correction = extract_l(t0);
sub(ctx, xlow, correction)
}
const fn pre_big_gamma1(mode: u8) -> &'static [i16; M] {
if mode <= MR795 {
&GAMMA1
} else {
&GAMMA1_12K2
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct WeightedSpeech {
mem_w: [Word16; M],
}
impl WeightedSpeech {
#[must_use]
pub const fn new() -> Self {
Self {
mem_w: [Word16(0); M],
}
}
pub fn half_frame(
&mut self,
ctx: &mut DspContext,
mode: u8,
az: &[Word16; AZ_SIZE],
offset: usize,
speech: &SpeechBuffer,
wsp: &mut [Word16; L_FRAME],
) {
assert!(
offset.is_multiple_of(L_SUBFR) && offset + 2 * L_SUBFR <= L_FRAME,
"pre_big works on two whole subframes inside the frame"
);
let g1 = pre_big_gamma1(mode);
let mut slot = usize::from(offset > 0) * 2;
let mut at = offset;
for _ in 0..2 {
let a = &az[slot * MP1..(slot + 1) * MP1];
let ap1 = expand_bandwidth(ctx, a, g1);
let ap2 = expand_bandwidth(ctx, a, &GAMMA2);
lp_residual(
ctx,
&ap1,
speech.with_history(at, L_SUBFR),
&mut wsp[at..at + L_SUBFR],
);
self.mem_w =
synthesis_filter_in_place(ctx, &ap2, &mut wsp[at..at + L_SUBFR], &self.mem_w);
slot += 1;
at += L_SUBFR;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubframeTargets {
pub xn: [Word16; L_SUBFR],
pub h1: [Word16; L_SUBFR],
pub res: [Word16; L_SUBFR],
}
const fn subframe_gamma1(mode: u8) -> &'static [i16; M] {
if mode == MR122 || mode == MR102 {
&GAMMA1_12K2
} else {
&GAMMA1
}
}
#[must_use]
pub fn subframe_targets(
ctx: &mut DspContext,
mode: u8,
a: &[Word16],
aq: &[Word16],
speech: &[Word16],
mem_err: &[Word16; M],
mem_w0: &[Word16; M],
) -> SubframeTargets {
assert_eq!(
speech.len(),
M + L_SUBFR,
"subframePreProc needs M samples of speech history"
);
let ap1 = expand_bandwidth(ctx, a, subframe_gamma1(mode));
let ap2 = expand_bandwidth(ctx, a, &GAMMA2);
let mut ai_zero = [Word16(0); L_SUBFR];
ai_zero[..MP1].copy_from_slice(&ap1);
let silence = [Word16(0); M];
let mut h1 = [Word16(0); L_SUBFR];
let _ = synthesis_filter(ctx, aq, &ai_zero, &mut h1, &silence);
let _ = synthesis_filter_in_place(ctx, &ap2, &mut h1, &silence);
let mut res = [Word16(0); L_SUBFR];
lp_residual(ctx, aq, speech, &mut res);
let mut error = [Word16(0); M + L_SUBFR];
error[..M].copy_from_slice(mem_err);
let mut filtered = [Word16(0); L_SUBFR];
let _ = synthesis_filter(ctx, aq, &res, &mut filtered, mem_err);
error[M..].copy_from_slice(&filtered);
let mut xn = [Word16(0); L_SUBFR];
lp_residual(ctx, &ap1, &error, &mut xn);
let _ = synthesis_filter_in_place(ctx, &ap2, &mut xn, mem_w0);
SubframeTargets { xn, h1, res }
}
#[cfg(test)]
mod tests {
use super::super::preproc::trace_support::{frames, input_frame, scalar, words};
use super::super::preproc::Preprocessor;
use super::*;
use crate::codecs::amr::nb::lsp::{interpolate_lsp, lsp_to_lp, LsfDecoder};
use crate::codecs::amr::nb::{bitstream, L_FRAME as FRAME};
const TRACE_MODE: u8 = 4;
const NB_SUBFR: usize = 4;
struct QuantisedSpectrum {
lsf: LsfDecoder,
lsp_old: [Word16; M],
payloads: Vec<Vec<u8>>,
}
impl QuantisedSpectrum {
fn new() -> Self {
const AMR: &[u8] = include_bytes!("../../testdata/amrnb_enc_mode4.amr");
let magic = b"#!AMR\n";
assert_eq!(&AMR[..magic.len()], magic, "unexpected .amr magic");
let mut payloads = Vec::new();
let mut at = magic.len();
while at < AMR.len() {
let toc = AMR[at];
let frame_type = (toc >> 3) & 0x0f;
assert_eq!(
frame_type, TRACE_MODE,
"the committed .amr is not at the trace's rate"
);
let len = 19usize;
payloads.push(AMR[at + 1..at + 1 + len].to_vec());
at += 1 + len;
}
Self {
lsf: LsfDecoder::at_reset(),
lsp_old: crate::codecs::amr::nb::lsp::initial_lsp(),
payloads,
}
}
fn next(&mut self, frame: usize) -> [Word16; AZ_SIZE] {
let params = bitstream::parse(TRACE_MODE, &self.payloads[frame]).expect("frame parses");
let lsp_new = self.lsf.decode(TRACE_MODE, ¶ms[..3], false);
let mut ctx = DspContext::default();
let az = interpolate_lsp(&mut ctx, &self.lsp_old, &lsp_new);
self.lsp_old = lsp_new;
az
}
}
#[allow(clippy::struct_field_names)]
struct PostProc {
mem_syn: [Word16; M],
mem_err: [Word16; M],
mem_w0: [Word16; M],
}
impl PostProc {
const fn new() -> Self {
Self {
mem_syn: [Word16(0); M],
mem_err: [Word16(0); M],
mem_w0: [Word16(0); M],
}
}
fn advance(
&mut self,
ctx: &mut DspContext,
aq: &[Word16],
speech: &[Word16],
frame: usize,
subfr: i32,
) {
let adapt = words(frame, subfr, "adapt", L_SUBFR);
let code = words(frame, subfr, "code", L_SUBFR);
let y1 = words(frame, subfr, "y1", L_SUBFR);
let y2 = words(frame, subfr, "y2", L_SUBFR);
let xn = words(frame, subfr, "xn", L_SUBFR);
let gain_pit = Word16(scalar(frame, subfr, "gain_pit") as i16);
let gain_code = Word16(scalar(frame, subfr, "gain_code") as i16);
let (temp_shift, k_shift, pitch_fac) = (1, 2, gain_pit);
let mut exc = [Word16(0); L_SUBFR];
for i in 0..L_SUBFR {
let mut acc = l_mult(ctx, adapt[i], pitch_fac);
acc = l_mac(ctx, acc, code[i], gain_code);
acc = l_shl(ctx, acc, temp_shift);
exc[i] = round(ctx, acc);
}
let mut synth = [Word16(0); L_SUBFR];
self.mem_syn = synthesis_filter(ctx, aq, &exc, &mut synth, &self.mem_syn);
for (j, i) in (L_SUBFR - M..L_SUBFR).enumerate() {
self.mem_err[j] = sub(ctx, speech[i], synth[i]);
let scaled = l_mult(ctx, y1[i], gain_pit);
let temp = extract_h(l_shl(ctx, scaled, 1));
let scaled = l_mult(ctx, y2[i], gain_code);
let k = extract_h(l_shl(ctx, scaled, k_shift));
let together = add(ctx, temp, k);
self.mem_w0[j] = sub(ctx, xn[i], together);
}
}
}
struct FrameOutput {
az: [Word16; AZ_SIZE],
lsp_new: [Word16; M],
targets: Vec<SubframeTargets>,
}
fn replay(mut check: impl FnMut(usize, &FrameOutput)) -> usize {
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let mut buffer = SpeechBuffer::new();
let mut analysis = LpAnalysis::new();
let mut lsp_old = crate::codecs::amr::nb::lsp::initial_lsp();
let mut quantised = QuantisedSpectrum::new();
let mut post = PostProc::new();
let total = frames();
for frame in 0..total {
let mut samples = input_frame(frame);
pre.condition(&mut ctx, &mut samples);
buffer.push(&samples);
let mut az = [Word16(0); AZ_SIZE];
analysis.analyse(&mut ctx, TRACE_MODE, &buffer, &mut az);
let a_end: [Word16; MP1] = az[3 * MP1..].try_into().expect("slot 3");
let lsp_new = az_lsp(&mut ctx, &a_end, &lsp_old);
let interpolated = interpolate_lsp(&mut ctx, &lsp_old, &lsp_new);
az[..3 * MP1].copy_from_slice(&interpolated[..3 * MP1]);
lsp_old = lsp_new;
let aq = quantised.next(frame);
let mut targets = Vec::new();
for subfr in 0..NB_SUBFR {
let at = subfr * L_SUBFR;
let t = subframe_targets(
&mut ctx,
TRACE_MODE,
&az[subfr * MP1..(subfr + 1) * MP1],
&aq[subfr * MP1..(subfr + 1) * MP1],
buffer.with_history(at, L_SUBFR),
&post.mem_err,
&post.mem_w0,
);
targets.push(t);
post.advance(
&mut ctx,
&aq[subfr * MP1..(subfr + 1) * MP1],
&buffer.coded()[at..at + L_SUBFR],
frame,
subfr as i32,
);
}
check(
frame,
&FrameOutput {
az,
lsp_new,
targets,
},
);
buffer.shift();
}
total
}
#[test]
fn interpolated_lp_coefficients_are_bit_exact_against_ts26073() {
let mut compared = 0usize;
let count = replay(|frame, out| {
let want = words(frame, -1, "A_t", AZ_SIZE);
for (i, &got) in out.az.iter().enumerate() {
assert_eq!(
got.0,
want[i].0,
"frame {frame}: A_t[{i}] (subframe {}, tap {}) differs",
i / MP1,
i % MP1
);
compared += 1;
}
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(compared, 3 * AZ_SIZE, "132 coefficients compared");
}
#[test]
fn levinson_output_reaches_the_fourth_subframe_slot() {
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let mut buffer = SpeechBuffer::new();
let mut analysis = LpAnalysis::new();
let mut samples = input_frame(0);
pre.condition(&mut ctx, &mut samples);
buffer.push(&samples);
let mut az = [Word16(0); AZ_SIZE];
analysis.analyse(&mut ctx, TRACE_MODE, &buffer, &mut az);
let want = words(0, -1, "A_t", AZ_SIZE);
for i in 0..MP1 {
assert_eq!(az[3 * MP1 + i].0, want[3 * MP1 + i].0, "slot 3 tap {i}");
}
assert!(
az[..3 * MP1].iter().all(|w| w.0 == 0),
"a single-analysis rate must leave slots 0..2 for the interpolation"
);
}
#[test]
fn lsps_are_bit_exact_against_ts26073() {
let mut compared = 0usize;
let count = replay(|frame, out| {
for subfr in 0..NB_SUBFR {
let want = words(frame, subfr as i32, "lsp_new", M);
for (i, &got) in out.lsp_new.iter().enumerate() {
assert_eq!(got.0, want[i].0, "frame {frame}: lsp_new[{i}] differs");
compared += 1;
}
}
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(
compared,
3 * NB_SUBFR * M,
"120 line spectral pairs compared"
);
}
#[test]
fn subframe_targets_are_bit_exact_against_ts26073() {
let mut compared = 0usize;
let count = replay(|frame, out| {
for (subfr, t) in out.targets.iter().enumerate() {
let s = subfr as i32;
for (name, got) in [("xn", &t.xn), ("h1", &t.h1), ("res", &t.res)] {
let want = words(frame, s, name, L_SUBFR);
for (i, &value) in got.iter().enumerate() {
assert_eq!(
value.0, want[i].0,
"frame {frame} subframe {subfr}: {name}[{i}] differs"
);
compared += 1;
}
}
}
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(
compared,
3 * NB_SUBFR * 3 * L_SUBFR,
"1440 target samples compared"
);
}
#[test]
fn the_lookahead_is_never_primed() {
let mut buffer = SpeechBuffer::new();
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let mut samples = input_frame(0);
pre.condition(&mut ctx, &mut samples);
buffer.push(&samples);
assert!(
buffer.coded()[..L_NEXT].iter().all(|w| w.0 == 0),
"the first 40 coded samples are the unprimed lookahead"
);
assert_eq!(
buffer.coded()[L_NEXT].0,
samples[0].0,
"the coded frame lags the pushed frame by exactly one lookahead"
);
}
#[test]
fn windows_address_the_spans_the_reference_uses() {
let mut buffer = SpeechBuffer::new();
let mut marked = [Word16(0); FRAME];
for (i, slot) in marked.iter_mut().enumerate() {
*slot = Word16(i as i16 + 1);
}
buffer.push(&marked);
let w = buffer.analysis_window();
assert!(w[..80].iter().all(|v| v.0 == 0));
assert_eq!(w[80].0, 1, "the frame starts 80 samples into p_window");
assert_eq!(w[L_WINDOW - 1].0, FRAME as i16);
let w = buffer.analysis_window_12k2();
assert!(w[..120].iter().all(|v| v.0 == 0));
assert_eq!(w[120].0, 1, "the 12.2 window starts one lookahead earlier");
assert_eq!(w[L_WINDOW - 1].0, (FRAME - L_NEXT) as i16);
let s = buffer.with_history(0, L_SUBFR);
assert_eq!(s.len(), M + L_SUBFR);
assert!(
s.iter().all(|v| v.0 == 0),
"subframe 0 of a fresh buffer is entirely history"
);
let s = buffer.with_history(L_NEXT, L_SUBFR);
assert!(
s[..M].iter().all(|v| v.0 == 0),
"the history is still silent"
);
assert_eq!(
s[M].0, 1,
"the pushed frame starts exactly one lookahead in"
);
}
#[test]
fn shifting_moves_the_frame_into_the_history() {
let mut buffer = SpeechBuffer::new();
let mut marked = [Word16(0); FRAME];
for (i, slot) in marked.iter_mut().enumerate() {
*slot = Word16(i as i16 + 1);
}
buffer.push(&marked);
buffer.shift();
assert_eq!(buffer.analysis_window()[0].0, 81);
assert_eq!(buffer.analysis_window()[79].0, FRAME as i16);
}
#[test]
fn az_lsp_round_trips_through_the_decoders_inverse() {
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let mut buffer = SpeechBuffer::new();
let mut analysis = LpAnalysis::new();
let mut lsp_old = crate::codecs::amr::nb::lsp::initial_lsp();
let mut checked = 0usize;
for frame in 0..frames() {
let mut samples = input_frame(frame);
pre.condition(&mut ctx, &mut samples);
buffer.push(&samples);
let mut az = [Word16(0); AZ_SIZE];
analysis.analyse(&mut ctx, TRACE_MODE, &buffer, &mut az);
let a: [Word16; MP1] = az[3 * MP1..].try_into().expect("slot 3");
let lsp = az_lsp(&mut ctx, &a, &lsp_old);
let back = lsp_to_lp(&mut ctx, &lsp);
for i in 0..MP1 {
let delta = i32::from(a[i].0) - i32::from(back[i].0);
assert!(
delta.abs() <= 4,
"frame {frame}: round trip moved a[{i}] by {delta}"
);
checked += 1;
}
lsp_old = lsp;
buffer.shift();
}
assert_eq!(checked, 3 * MP1, "33 coefficients round-tripped");
}
#[test]
fn the_root_bracket_test_counts_an_exact_zero_as_a_crossing() {
let mut ctx = DspContext::default();
assert!(
brackets_a_root(&mut ctx, Word16(0), Word16(12345)),
"a zero low endpoint must count as a crossing"
);
assert!(
brackets_a_root(&mut ctx, Word16(12345), Word16(0)),
"a zero high endpoint must count as a crossing"
);
assert!(brackets_a_root(&mut ctx, Word16(-1), Word16(1)));
assert!(brackets_a_root(&mut ctx, Word16(1), Word16(-1)));
assert!(!brackets_a_root(&mut ctx, Word16(1), Word16(1)));
assert!(!brackets_a_root(&mut ctx, Word16(-1), Word16(-1)));
assert!(!brackets_a_root(&mut ctx, Word16(-32768), Word16(-32768)));
}
#[test]
fn az_lsp_returns_ten_descending_roots() {
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let mut buffer = SpeechBuffer::new();
let mut analysis = LpAnalysis::new();
let mut lsp_old = crate::codecs::amr::nb::lsp::initial_lsp();
let mut checked = 0usize;
for frame in 0..frames() {
let mut samples = input_frame(frame);
pre.condition(&mut ctx, &mut samples);
buffer.push(&samples);
let mut az = [Word16(0); AZ_SIZE];
analysis.analyse(&mut ctx, TRACE_MODE, &buffer, &mut az);
let a: [Word16; MP1] = az[3 * MP1..].try_into().expect("slot 3");
let lsp = az_lsp(&mut ctx, &a, &lsp_old);
for pair in lsp.windows(2) {
assert!(pair[0].0 > pair[1].0, "frame {frame}: roots not descending");
checked += 1;
}
lsp_old = lsp;
buffer.shift();
}
assert_eq!(checked, 3 * (M - 1), "27 adjacent pairs checked");
}
#[test]
fn az_lsp_falls_back_to_the_previous_set_when_roots_are_missing() {
let mut ctx = DspContext::default();
let mut a = [Word16(0); MP1];
a[0] = Word16(4096);
a[1] = Word16(8192);
let old = [
Word16(31000),
Word16(30000),
Word16(29000),
Word16(28000),
Word16(27000),
Word16(26000),
Word16(25000),
Word16(24000),
Word16(23000),
Word16(22000),
];
let lsp = az_lsp(&mut ctx, &a, &old);
assert_eq!(lsp, old, "the fallback replaces the whole vector");
let mut pre = Preprocessor::new();
let mut buffer = SpeechBuffer::new();
let mut analysis = LpAnalysis::new();
let mut samples = input_frame(0);
pre.condition(&mut ctx, &mut samples);
buffer.push(&samples);
let mut az = [Word16(0); AZ_SIZE];
analysis.analyse(&mut ctx, TRACE_MODE, &buffer, &mut az);
let good: [Word16; MP1] = az[3 * MP1..].try_into().expect("slot 3");
assert_ne!(
az_lsp(&mut ctx, &good, &old),
old,
"a well-behaved filter must not take the fallback"
);
}
#[test]
fn levinson_reemits_the_previous_filter_when_unstable() {
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let mut buffer = SpeechBuffer::new();
let mut samples = input_frame(0);
pre.condition(&mut ctx, &mut samples);
buffer.push(&samples);
let mut levinson = Levinson::new();
let mut r = autocorrelate(&mut ctx, buffer.analysis_window(), &LP_WINDOW_200_40);
lag_window(&mut r);
let (stable, rc) = levinson.solve(&mut ctx, &r);
assert!(
stable[1..].iter().any(|w| w.0 != 0) && rc.iter().any(|w| w.0 != 0),
"the stable solve produced nothing to re-emit"
);
let mut r_h = [Word16(0); MP1];
let mut r_l = [Word16(0); MP1];
r_h[0] = Word16(0x4000);
r_h[1] = Word16(0x3fff);
r_l[1] = Word16(0x7fff);
let unstable = Autocorrelation { r_h, r_l, norm: 0 };
let (a, rc) = levinson.solve(&mut ctx, &unstable);
assert_eq!(a, stable, "the unstable path re-emits the previous filter");
assert!(
rc.iter().all(|w| w.0 == 0),
"unstable zeroes all four reflection coefficients"
);
let (again, _) = levinson.solve(&mut ctx, &unstable);
assert_eq!(
again, stable,
"an unstable frame must not update the stored filter"
);
}
#[test]
fn the_stability_threshold_is_strictly_greater_than_32750() {
assert_eq!(K_UNSTABLE.0, 32750, "the threshold moved");
let mut ctx = DspContext::default();
assert!(
{
let m = abs_s(&mut ctx, K_UNSTABLE);
sub(&mut ctx, m, K_UNSTABLE).0 <= 0
},
"equality must not trip the instability test"
);
}
#[test]
fn the_two_gamma1_tests_disagree_on_the_dtx_pseudo_mode() {
assert_ne!(GAMMA1, GAMMA1_12K2, "the two numerators must be different");
for mode in 0..=MR122 {
assert_eq!(
pre_big_gamma1(mode),
subframe_gamma1(mode),
"mode {mode}: the two tests must agree on every speech rate"
);
}
assert_eq!(pre_big_gamma1(MRDTX), &GAMMA1_12K2);
assert_eq!(subframe_gamma1(MRDTX), &GAMMA1);
assert_eq!(pre_big_gamma1(MR795), &GAMMA1);
assert_eq!(pre_big_gamma1(MR102), &GAMMA1_12K2);
assert_eq!(pre_big_gamma1(MR475), &GAMMA1);
assert_eq!(pre_big_gamma1(MR515), &GAMMA1);
}
#[test]
fn the_weighted_speech_half_frames_use_slots_zero_and_two() {
let mut ctx = DspContext::default();
let mut az = [Word16(0); AZ_SIZE];
for slot in 0..4 {
az[slot * MP1] = Word16(4096);
az[slot * MP1 + 1] = Word16(-1000 * (slot as i16 + 1));
}
let mut buffer = SpeechBuffer::new();
let mut marked = [Word16(0); FRAME];
for (i, s) in marked.iter_mut().enumerate() {
*s = Word16(((i % 32) as i16) - 16);
}
buffer.push(&marked);
let mut wsp = [Word16(0); FRAME];
let mut weighted = WeightedSpeech::new();
weighted.half_frame(&mut ctx, TRACE_MODE, &az, 0, &buffer, &mut wsp);
let first = weighted;
weighted.half_frame(&mut ctx, TRACE_MODE, &az, 80, &buffer, &mut wsp);
let mut swapped = az;
swapped.swap(2 * MP1 + 1, 3 * MP1 + 1);
let mut other = [Word16(0); FRAME];
let mut w2 = WeightedSpeech::new();
w2.half_frame(&mut ctx, TRACE_MODE, &swapped, 0, &buffer, &mut other);
assert_eq!(w2, first, "the first half-frame does not read slots 2 or 3");
assert_eq!(other[..80], wsp[..80], "the first half-frame is unchanged");
w2.half_frame(&mut ctx, TRACE_MODE, &swapped, 80, &buffer, &mut other);
assert_ne!(
other[80..],
wsp[80..],
"the second half-frame reads slots 2 and 3"
);
}
#[test]
fn the_weighting_memory_advances_once_per_subframe() {
let mut ctx = DspContext::default();
let mut az = [Word16(0); AZ_SIZE];
for slot in 0..4 {
az[slot * MP1] = Word16(4096);
az[slot * MP1 + 1] = Word16(-2000);
}
let mut buffer = SpeechBuffer::new();
let mut marked = [Word16(0); FRAME];
for (i, s) in marked.iter_mut().enumerate() {
*s = Word16(((i % 32) as i16) - 16);
}
buffer.push(&marked);
let mut wsp = [Word16(0); FRAME];
let mut weighted = WeightedSpeech::new();
let states: Vec<_> = (0..2)
.map(|half| {
weighted.half_frame(&mut ctx, TRACE_MODE, &az, half * 80, &buffer, &mut wsp);
weighted
})
.collect();
assert_ne!(states[0], WeightedSpeech::new(), "the memory advanced");
assert_ne!(states[0], states[1], "and advanced again");
assert!(
wsp.iter().any(|w| w.0 != 0),
"the weighted speech is not identically zero"
);
}
#[test]
fn the_impulse_response_is_the_weighted_synthesis_filters() {
let count = replay(|frame, out| {
for (subfr, t) in out.targets.iter().enumerate() {
assert_eq!(
t.h1[0].0, 4096,
"frame {frame} subframe {subfr}: h1[0] is not the monic lead"
);
}
});
assert_eq!(count, 3);
}
#[test]
fn autocorrelation_rescales_on_saturation_rather_than_widening() {
let mut ctx = DspContext::default();
let loud = [Word16(32760); L_WINDOW];
let flat = [32767i16; L_WINDOW];
let r = autocorrelate(&mut ctx, &loud, &flat);
assert!(
r.norm < 0,
"a saturating window did not trigger the rescale (norm = {})",
r.norm
);
assert_eq!(r.norm % 4, 0, "the rescale exponent moves in steps of four");
let silent = [Word16(0); L_WINDOW];
let r = autocorrelate(&mut ctx, &silent, &flat);
assert_eq!(r.norm, 30, "silence normalises the +1 bias to the top");
}
#[test]
fn the_lag_window_leaves_r_zero_alone() {
let mut r = Autocorrelation {
r_h: [Word16(0x4000); MP1],
r_l: [Word16(0); MP1],
norm: 0,
};
let before = r.r_h[0];
lag_window(&mut r);
assert_eq!(r.r_h[0].0, before.0, "r[0] must not be windowed");
assert!(
r.r_h[1].0 > r.r_h[M].0,
"the lag window is applied from r[1]"
);
}
}