#![allow(clippy::cast_possible_wrap)]
use crate::fixed_point::arith::round;
use crate::fixed_point::arith32::{l_add, l_mac};
use crate::fixed_point::oper32::{l_extract, mpy_32_16};
use crate::fixed_point::shift::l_shl;
use crate::fixed_point::types::{DspContext, Word16, Word32};
use super::super::decoder_tables::{PRE_PROC_A, PRE_PROC_B};
const INPUT_MASK: i16 = -8;
pub fn restrict_to_13_bits(frame: &mut [Word16]) {
for sample in frame {
sample.0 &= INPUT_MASK;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Preprocessor {
y1: (Word16, Word16),
y2: (Word16, Word16),
x0: Word16,
x1: Word16,
}
impl Preprocessor {
#[must_use]
pub const fn new() -> Self {
Self {
y1: (Word16(0), Word16(0)),
y2: (Word16(0), Word16(0)),
x0: Word16(0),
x1: Word16(0),
}
}
pub fn filter(&mut self, ctx: &mut DspContext, signal: &mut [Word16]) {
let b = PRE_PROC_B.map(Word16);
let a = PRE_PROC_A.map(Word16);
for sample in signal {
let x2 = self.x1;
self.x1 = self.x0;
self.x0 = *sample;
let mut acc = mpy_32_16(self.y1.0, self.y1.1, a[1]);
acc = l_add(ctx, acc, mpy_32_16(self.y2.0, self.y2.1, a[2]));
acc = l_mac(ctx, acc, self.x0, b[0]);
acc = l_mac(ctx, acc, self.x1, b[1]);
acc = l_mac(ctx, acc, x2, b[2]);
acc = l_shl(ctx, acc, 3);
*sample = round(ctx, acc);
self.y2 = self.y1;
self.y1 = l_extract(acc);
}
}
pub fn condition(&mut self, ctx: &mut DspContext, frame: &mut [Word16]) {
restrict_to_13_bits(frame);
self.filter(ctx, frame);
}
#[must_use]
pub const fn state(&self) -> ((Word16, Word16), (Word16, Word16), Word16, Word16) {
(self.y1, self.y2, self.x0, self.x1)
}
}
#[must_use]
pub fn compose(hi: Word16, lo: Word16) -> Word32 {
crate::fixed_point::oper32::l_comp(hi, lo)
}
#[cfg(test)]
pub(crate) mod trace_support {
use crate::fixed_point::types::Word16;
use super::super::super::L_FRAME;
pub const TRACE: &str = include_str!("../../testdata/nb_enc_trace.txt");
const INPUT: &[u8] = include_bytes!("../../testdata/amrnb_enc_input.pcm");
pub fn frames() -> usize {
let mut highest = None;
for line in TRACE.lines() {
let mut parts = line.split_whitespace();
if parts.next() != Some("T") {
continue;
}
let frame: usize = parts.next().expect("frame index").parse().expect("integer");
highest = Some(highest.map_or(frame, |h: usize| h.max(frame)));
}
highest.expect("trace carries at least one frame") + 1
}
pub fn row(frame: usize, subframe: i32, name: &str) -> Vec<i32> {
for line in TRACE.lines() {
let mut parts = line.split_whitespace();
if parts.next() != Some("T") {
continue;
}
let f: usize = parts.next().expect("frame index").parse().expect("integer");
let s: i32 = parts.next().expect("subframe").parse().expect("integer");
if f != frame || s != subframe || parts.next() != Some(name) {
continue;
}
return parts.map(|v| v.parse().expect("integer")).collect();
}
panic!("trace has no row {name:?} for frame {frame} subframe {subframe}");
}
pub fn words(frame: usize, subframe: i32, name: &str, expected: usize) -> Vec<Word16> {
let values = row(frame, subframe, name);
assert_eq!(
values.len(),
expected,
"frame {frame}/{subframe}: {name} length"
);
values
.into_iter()
.map(|v| Word16(i16::try_from(v).expect("row holds Word16 values")))
.collect()
}
pub fn scalar(frame: usize, subframe: i32, name: &str) -> i32 {
let values = row(frame, subframe, name);
assert_eq!(
values.len(),
1,
"frame {frame}/{subframe}: {name} is a scalar"
);
values[0]
}
pub fn input_frame(frame: usize) -> [Word16; L_FRAME] {
let mut samples = [Word16(0); L_FRAME];
let base = frame * L_FRAME * 2;
for (n, slot) in samples.iter_mut().enumerate() {
let lo = u16::from(INPUT[base + n * 2]);
let hi = u16::from(INPUT[base + n * 2 + 1]);
*slot = Word16((lo | (hi << 8)) as i16);
}
samples
}
}
#[cfg(test)]
mod tests {
use super::trace_support::{frames, input_frame, words};
use super::*;
use crate::codecs::amr::nb::L_FRAME;
fn replay(mut check: impl FnMut(usize, &[Word16; L_FRAME])) -> usize {
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let total = frames();
for frame in 0..total {
let mut samples = input_frame(frame);
pre.condition(&mut ctx, &mut samples);
check(frame, &samples);
}
total
}
#[test]
fn conditioned_input_is_bit_exact_against_ts26073() {
let mut compared = 0usize;
let count = replay(|frame, got| {
let want = words(frame, -1, "speech", L_FRAME);
for (i, &sample) in got.iter().enumerate() {
assert_eq!(
sample.0, want[i].0,
"frame {frame}: conditioned sample {i} differs from the reference"
);
compared += 1;
}
});
assert_eq!(count, 3, "the committed trace covers three frames");
assert_eq!(compared, 3 * L_FRAME, "480 samples compared");
}
#[test]
fn the_thirteen_bit_mask_is_not_a_no_op() {
let mut changed = 0usize;
let mut total = 0usize;
for frame in 0..frames() {
let raw = input_frame(frame);
let mut masked = raw;
restrict_to_13_bits(&mut masked);
for (r, m) in raw.iter().zip(masked.iter()) {
assert_eq!(m.0 & 7, 0, "masked sample still has low bits set");
assert_eq!(m.0, r.0 & !7, "mask is not a plain bitwise AND");
total += 1;
changed += usize::from(r.0 != m.0);
}
}
assert_eq!(total, 3 * L_FRAME, "480 samples inspected");
assert!(
changed > total / 2,
"the mask changed only {changed} of {total} samples; the fixture is \
not exercising it"
);
}
#[test]
fn skipping_the_mask_changes_the_conditioned_frame() {
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let mut unmasked = input_frame(0);
pre.filter(&mut ctx, &mut unmasked);
let want = words(0, -1, "speech", L_FRAME);
let differences = unmasked
.iter()
.zip(want.iter())
.filter(|(a, b)| a.0 != b.0)
.count();
assert!(
differences > 0,
"the unmasked frame matched the reference, so this fixture cannot \
tell the two paths apart"
);
}
#[test]
fn filter_state_carries_across_frames() {
let mut ctx = DspContext::default();
let mut fresh = Preprocessor::new();
let mut samples = input_frame(1);
fresh.condition(&mut ctx, &mut samples);
let want = words(1, -1, "speech", L_FRAME);
assert_ne!(
samples[0].0, want[0].0,
"a fresh preprocessor reproduced frame 1's first sample, so this \
test cannot detect a dropped state"
);
let mut carried = Preprocessor::new();
let mut frame0 = input_frame(0);
carried.condition(&mut ctx, &mut frame0);
let mut frame1 = input_frame(1);
carried.condition(&mut ctx, &mut frame1);
assert_eq!(frame1[0].0, want[0].0, "carried state reproduces frame 1");
}
#[test]
fn reset_state_is_silent_and_stays_silent() {
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let mut silence = [Word16(0); L_FRAME];
pre.condition(&mut ctx, &mut silence);
assert!(silence.iter().all(|s| s.0 == 0), "silence in, silence out");
assert_eq!(pre, Preprocessor::new(), "silence left the state untouched");
}
#[test]
fn recursion_state_is_the_unrounded_accumulator() {
let mut ctx = DspContext::default();
let mut pre = Preprocessor::new();
let mut samples = input_frame(0);
restrict_to_13_bits(&mut samples);
let mut sub_lsb_state = 0usize;
for &sample in &samples {
let mut one = [sample];
pre.filter(&mut ctx, &mut one);
let (y1, _, _, _) = pre.state();
let composed = compose(y1.0, y1.1);
sub_lsb_state += usize::from(y1.1 .0 != 0);
assert_eq!(
crate::fixed_point::arith::extract_h(composed).0,
y1.0 .0,
"the double-precision pair does not recompose"
);
}
assert!(
sub_lsb_state > L_FRAME / 2,
"only {sub_lsb_state} of {L_FRAME} state words carried sub-LSB \
precision; the recursion is running at output precision"
);
}
}