use super::codebook::L_SUBFR;
use crate::fixed_point::arith::round;
use crate::fixed_point::arith32::{l_deposit_h, l_mac, l_msu, l_mult};
use crate::fixed_point::shift::l_shl;
use crate::fixed_point::types::{DspContext, Word16, Word32};
const UP_SAMP: usize = 4;
const L_INTERPOL2: usize = 16;
pub const PIT_MAX: usize = 231;
pub const L_INTERPOL: usize = 17;
pub const HISTORY: usize = PIT_MAX + L_INTERPOL;
pub const PIT_SHARP: Word16 = Word16(27853);
const INTER4_2: [i16; UP_SAMP * 2 * L_INTERPOL2] = [
0, 1, 2, 1, -2, -7, -10, -7, 4, 19, 28, 22, -2, -33, -55, -49, -10, 47, 91, 92, 38, -52, -133,
-153, -88, 43, 175, 231, 165, -9, -209, -325, -275, -60, 226, 431, 424, 175, -213, -544, -619,
-355, 153, 656, 871, 626, -16, -762, -1207, -1044, -249, 853, 1699, 1749, 780, -923, -2598,
-3267, -2147, 968, 5531, 10359, 14031, 15401, 14031, 10359, 5531, 968, -2147, -3267, -2598,
-923, 780, 1749, 1699, 853, -249, -1044, -1207, -762, -16, 626, 871, 656, 153, -355, -619,
-544, -213, 175, 424, 431, 226, -60, -275, -325, -209, -9, 165, 231, 175, 43, -88, -153, -133,
-52, 38, 92, 91, 47, -10, -49, -55, -33, -2, 22, 28, 19, 4, -7, -10, -7, -2, 1, 2, 1, 0, 0,
];
const LTP_LP: (Word16, Word16) = (Word16(5898), Word16(20972));
pub fn predict(buffer: &mut [Word16], offset: usize, lag: usize, frac: u8, len: usize) {
let mut ctx = DspContext::default();
let mut start = offset - lag;
let phase = if frac == 0 {
0
} else {
start -= 1;
UP_SAMP - usize::from(frac)
};
start -= L_INTERPOL2 - 1;
for j in 0..len {
let mut sum = Word32(0);
let mut k = UP_SAMP - 1 - phase;
for i in 0..2 * L_INTERPOL2 {
sum = l_mac(&mut ctx, sum, buffer[start + j + i], Word16(INTER4_2[k]));
k += UP_SAMP;
}
let scaled = l_shl(&mut ctx, sum, 1);
buffer[offset + j] = round(&mut ctx, scaled);
}
}
#[must_use]
pub fn low_pass(input: &[Word16]) -> [Word16; L_SUBFR] {
assert!(
input.len() >= L_SUBFR + 2,
"the low-pass filter needs one sample either side of the subframe"
);
let mut ctx = DspContext::default();
let mut out = [Word16(0); L_SUBFR];
for (i, slot) in out.iter_mut().enumerate() {
let mut acc = l_mult(&mut ctx, LTP_LP.0, input[i]);
acc = l_mac(&mut ctx, acc, LTP_LP.1, input[i + 1]);
acc = l_mac(&mut ctx, acc, LTP_LP.0, input[i + 2]);
*slot = round(&mut ctx, acc);
}
out
}
pub fn preemphasis(x: &mut [Word16], mu: Word16, memory: &mut Word16) {
let mut ctx = DspContext::default();
let last = x[x.len() - 1];
for i in (1..x.len()).rev() {
let acc = l_msu(&mut ctx, l_deposit_h(x[i]), x[i - 1], mu);
x[i] = round(&mut ctx, acc);
}
let acc = l_msu(&mut ctx, l_deposit_h(x[0]), *memory, mu);
x[0] = round(&mut ctx, acc);
*memory = last;
}
pub fn sharpen(x: &mut [Word16], lag: usize, sharp: Word16) {
let mut ctx = DspContext::default();
for i in lag..x.len() {
let acc = l_mac(&mut ctx, l_deposit_h(x[i]), x[i - lag], sharp);
x[i] = round(&mut ctx, acc);
}
}
#[must_use]
pub const fn sharpening_lag(lag: usize, frac: u8) -> usize {
if frac > 2 {
lag + 1
} else {
lag
}
}
#[cfg(test)]
mod tests {
use super::super::lp::isp_to_lp::tests_support::{block_row, has_block};
use super::*;
fn history() -> Vec<Word16> {
(0..HISTORY)
.map(|n| {
let phase = n % 57;
#[allow(clippy::cast_precision_loss)]
let phase = phase as f64;
let base = if phase < 3.0 {
2000.0f64.mul_add(-phase, 8000.0)
} else {
12.0f64.mul_add(phase, -300.0)
};
#[allow(clippy::cast_precision_loss)]
let drift =
0.4f64.mul_add((2.0 * std::f64::consts::PI * n as f64 / 313.0).sin(), 0.6);
#[allow(clippy::cast_possible_truncation)]
Word16((base * drift) as i16)
})
.collect()
}
fn innovation(lag_index: usize, frac_index: usize) -> [Word16; L_SUBFR] {
let mut code = [Word16(0); L_SUBFR];
for n in 0..8 {
let i = (n * 7 + lag_index * 3 + frac_index) % L_SUBFR;
code[i] = Word16(code[i].0 + if n % 2 == 1 { -512 } else { 512 });
}
code
}
const LAGS: [usize; 6] = [34, 60, 64, 100, 128, 231];
#[test]
fn the_adaptive_codebook_is_bit_exact_against_ts26173() {
assert!(has_block("ltp"), "fixture block ltp missing");
let history = history();
let mut checked = 0;
for (li, &lag) in LAGS.iter().enumerate() {
for frac in 0..4u8 {
let c = li * 4 + usize::from(frac);
let meta = block_row("ltp", &format!("meta{c}"));
assert_eq!(
(i64::from(meta[0]), i64::from(meta[1])),
(i64::try_from(lag).expect("lag"), i64::from(frac)),
"case {c}: fixture is for a different lag"
);
let mut buffer = history.clone();
buffer.resize(HISTORY + L_SUBFR + 1, Word16(0));
predict(&mut buffer, HISTORY, lag, frac, L_SUBFR + 1);
let out = &buffer[HISTORY..];
let want = block_row("ltp", &format!("pred{c}"));
assert_eq!(want.len(), L_SUBFR + 1, "case {c}: length");
for (i, (&g, &w)) in out.iter().zip(want.iter()).enumerate() {
assert_eq!(
g.0, w,
"case {c} (lag {lag}, frac {frac}): sample {i} = {} but the reference gives {w}",
g.0
);
}
checked += 1;
}
}
assert_eq!(checked, 24, "expected every lag and fraction combination");
}
#[test]
fn the_low_pass_filter_is_bit_exact_against_ts26173() {
let history = history();
for (li, &lag) in LAGS.iter().enumerate() {
for frac in 0..4u8 {
let c = li * 4 + usize::from(frac);
let mut buffer = history.clone();
buffer.resize(HISTORY + L_SUBFR + 1, Word16(0));
predict(&mut buffer, HISTORY, lag, frac, L_SUBFR + 1);
let input = &buffer[HISTORY - 1..];
let got = low_pass(input);
let want = block_row("ltp", &format!("ltpf{c}"));
assert_eq!(want.len(), L_SUBFR, "case {c}: length");
for (i, (&g, &w)) in got.iter().zip(want.iter()).enumerate() {
assert_eq!(g.0, w, "case {c}: filtered sample {i}");
}
}
}
}
#[test]
fn preemphasis_and_sharpening_are_bit_exact_against_ts26173() {
for (li, &lag) in LAGS.iter().enumerate() {
for frac in 0..4u8 {
let c = li * 4 + usize::from(frac);
let mut code = innovation(li, usize::from(frac));
let mut memory = Word16(0);
preemphasis(&mut code, Word16(6554), &mut memory);
sharpen(&mut code, sharpening_lag(lag, frac), PIT_SHARP);
let want = block_row("ltp", &format!("shrp{c}"));
assert_eq!(want.len(), L_SUBFR, "case {c}: length");
for (i, (&g, &w)) in code.iter().zip(want.iter()).enumerate() {
assert_eq!(g.0, w, "case {c}: sample {i}");
}
}
}
}
#[test]
fn an_integer_lag_copies_the_history_almost_exactly() {
let history = history();
let lag = 100usize;
let mut buffer = history.clone();
buffer.resize(HISTORY + L_SUBFR, Word16(0));
predict(&mut buffer, HISTORY, lag, 0, L_SUBFR);
let out = buffer[HISTORY..].to_vec();
let reference = &history[HISTORY - lag..][..L_SUBFR];
let (mut dot, mut norm_a, mut norm_b) = (0i64, 0i64, 0i64);
for (a, b) in out.iter().zip(reference.iter()) {
dot += i64::from(a.0) * i64::from(b.0);
norm_a += i64::from(a.0) * i64::from(a.0);
norm_b += i64::from(b.0) * i64::from(b.0);
}
#[allow(clippy::cast_precision_loss, clippy::suboptimal_flops)]
let correlation = dot as f64 / ((norm_a as f64).sqrt() * (norm_b as f64).sqrt());
assert!(
correlation > 0.95,
"integer-lag prediction correlates only {correlation:.3} with the history"
);
}
#[test]
fn each_fraction_gives_a_distinct_vector() {
let history = history();
let mut seen: Vec<[i16; 8]> = Vec::new();
for frac in 0..4u8 {
let mut buffer = history.clone();
buffer.resize(HISTORY + L_SUBFR, Word16(0));
predict(&mut buffer, HISTORY, 64, frac, L_SUBFR);
let head: [i16; 8] = std::array::from_fn(|i| buffer[HISTORY + i].0);
assert!(
!seen.contains(&head),
"fraction {frac} produced a vector already seen"
);
seen.push(head);
}
}
#[test]
fn sharpening_leaves_the_first_period_untouched() {
for lag in [34usize, 64, 100] {
let mut code = innovation(0, 0);
let before = code;
sharpen(&mut code, lag, PIT_SHARP);
for i in 0..lag.min(L_SUBFR) {
assert_eq!(code[i], before[i], "lag {lag}: sample {i} was modified");
}
}
}
#[test]
fn the_sharpening_lag_rounds_rather_than_truncates() {
assert_eq!(sharpening_lag(64, 0), 64);
assert_eq!(sharpening_lag(64, 2), 64);
assert_eq!(sharpening_lag(64, 3), 65);
}
}