#![allow(clippy::similar_names)]
use crate::codecs::amr::wb::gain_tables::{QUA_GAIN_6B, QUA_GAIN_7B};
use crate::codecs::amr::wb::math::{dot_product12, log2, pow2};
use crate::fixed_point::arith::{add, extract_h, extract_l, mult_r, negate, round, sub};
use crate::fixed_point::arith32::{l_deposit_h, l_deposit_l, l_mac, l_mult, l_negate, l_sub};
use crate::fixed_point::oper32::{l_extract, mpy_32_16};
use crate::fixed_point::shift::{l_shl, l_shr, shr};
use crate::fixed_point::types::{DspContext, Word16, Word32};
pub const L_SUBFR: usize = 64;
const PRED_ORDER: usize = 4;
const PRED: [Word16; PRED_ORDER] = [Word16(4096), Word16(3277), Word16(2458), Word16(1638)];
const PAST_ENERGY_RESET: Word16 = Word16(-14336);
const MEAN_ENER: Word16 = Word16(30);
const RANGE: usize = 64;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GainBits {
Six,
Seven,
}
impl GainBits {
#[must_use]
pub const fn from_frame_bits(frame_bits: usize) -> Self {
if frame_bits <= 177 {
Self::Six
} else {
Self::Seven
}
}
#[must_use]
pub const fn bits(self) -> usize {
match self {
Self::Six => 6,
Self::Seven => 7,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PitchCorrelations {
pub energy: Word16,
pub energy_exp: i16,
pub correlation: Word16,
pub correlation_exp: i16,
}
pub struct GainInputs<'a> {
pub target: &'a [Word16; L_SUBFR],
pub filtered_adaptive: &'a [Word16; L_SUBFR],
pub target_q: i16,
pub filtered_code: &'a [Word16; L_SUBFR],
pub code: &'a [Word16; L_SUBFR],
pub correlations: PitchCorrelations,
pub bits: GainBits,
pub pitch_gain: Word16,
pub clip_pitch_gain: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct QuantisedGains {
pub index: u16,
pub pitch_gain: Word16,
pub code_gain: Word32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GainPredictor {
past_energy: [Word16; PRED_ORDER],
}
impl Default for GainPredictor {
fn default() -> Self {
Self::new()
}
}
impl GainPredictor {
#[must_use]
pub const fn new() -> Self {
Self {
past_energy: [PAST_ENERGY_RESET; PRED_ORDER],
}
}
pub const fn reset(&mut self) {
self.past_energy = [PAST_ENERGY_RESET; PRED_ORDER];
}
fn remember(&mut self, ctx: &mut DspContext, table_gain: Word16) {
let (exponent, fraction) = log2(ctx, l_deposit_l(table_gain));
let exponent = sub(ctx, Word16(exponent), Word16(11));
let scaled = mpy_32_16(exponent, fraction, Word16(24660));
let energy = extract_l(l_shr(ctx, scaled, 3));
self.past_energy[3] = self.past_energy[2];
self.past_energy[2] = self.past_energy[1];
self.past_energy[1] = self.past_energy[0];
self.past_energy[0] = energy;
}
fn predict(self, ctx: &mut DspContext, code: &[Word16; L_SUBFR]) -> (Word16, Word16) {
let (energy, exponent) = dot_product12(ctx, code, code);
let exponent = exponent - (18 + 6 + 31);
let (log_int, log_frac) = log2(ctx, energy);
let log_int = log_int + exponent;
let mut acc = mpy_32_16(Word16(log_int), log_frac, Word16(-24660));
acc = l_mac(ctx, acc, MEAN_ENER, Word16(8192));
acc = l_shl(ctx, acc, 10);
for (&coefficient, &past) in PRED.iter().zip(self.past_energy.iter()) {
acc = l_mac(ctx, acc, coefficient, past);
}
let decibels = extract_h(acc);
let acc = l_mult(ctx, decibels, Word16(5443));
let acc = l_shr(ctx, acc, 8);
let (exponent, fraction) = l_extract(acc);
let mantissa = extract_l(pow2(ctx, 14, fraction));
(mantissa, sub(ctx, exponent, Word16(14)))
}
pub fn quantise(&mut self, ctx: &mut DspContext, inputs: &GainInputs<'_>) -> QuantisedGains {
let (table, first, size) = Self::window(ctx, inputs);
let (gcode0, exp_gcode0) = self.predict(ctx, inputs.code);
let (coeff, coeff_lo) = align_coefficients(ctx, inputs, exp_gcode0);
let mut smallest = Word32(i32::MAX);
let mut best = 0usize;
for i in 0..size {
let pair = 2 * (first + i);
let g_pitch = Word16(table[pair]);
let g_code = mult_r(ctx, Word16(table[pair + 1]), gcode0);
let g2_pitch = mult_r(ctx, g_pitch, g_pitch);
let g_pit_cod = mult_r(ctx, g_code, g_pitch);
let (g2_code, g2_code_lo) = l_extract(l_mult(ctx, g_code, g_code));
let mut acc = l_mult(ctx, coeff[2], g2_code_lo);
acc = l_shr(ctx, acc, 3);
acc = l_mac(ctx, acc, coeff_lo[0], g2_pitch);
acc = l_mac(ctx, acc, coeff_lo[1], g_pitch);
acc = l_mac(ctx, acc, coeff_lo[2], g2_code);
acc = l_mac(ctx, acc, coeff_lo[3], g_code);
acc = l_mac(ctx, acc, coeff_lo[4], g_pit_cod);
acc = l_shr(ctx, acc, 12);
acc = l_mac(ctx, acc, coeff[0], g2_pitch);
acc = l_mac(ctx, acc, coeff[1], g_pitch);
acc = l_mac(ctx, acc, coeff[2], g2_code);
acc = l_mac(ctx, acc, coeff[3], g_code);
acc = l_mac(ctx, acc, coeff[4], g_pit_cod);
if beats(ctx, acc, smallest) {
smallest = acc;
best = i;
}
}
let index = first + best;
let pitch_gain = Word16(table[2 * index]);
let table_gain = Word16(table[2 * index + 1]);
let raw = l_mult(ctx, table_gain, gcode0);
let up = add(ctx, exp_gcode0, Word16(4)).0;
let code_gain = l_shl(ctx, raw, up);
self.remember(ctx, table_gain);
QuantisedGains {
index: u16::try_from(index).expect("the window stays inside the table"),
pitch_gain,
code_gain,
}
}
fn window(ctx: &mut DspContext, inputs: &GainInputs<'_>) -> (&'static [i16], usize, usize) {
match inputs.bits {
GainBits::Six => {
let size = if inputs.clip_pitch_gain {
RANGE - 16
} else {
RANGE
};
(&QUA_GAIN_6B, 0, size)
}
GainBits::Seven => {
let scanned = if inputs.clip_pitch_gain {
RANGE - 27
} else {
RANGE
};
let mut first = 0usize;
for i in 0..scanned {
let candidate = Word16(QUA_GAIN_7B[RANGE + 2 * i]);
if sub(ctx, inputs.pitch_gain, candidate).0 > 0 {
first += 1;
}
}
(&QUA_GAIN_7B, first, RANGE)
}
}
}
}
fn beats(ctx: &mut DspContext, candidate: Word32, incumbent: Word32) -> bool {
l_sub(ctx, candidate, incumbent).0 < 0
}
fn align_coefficients(
ctx: &mut DspContext,
inputs: &GainInputs<'_>,
exp_gcode0: Word16,
) -> ([Word16; 5], [Word16; 5]) {
let g = inputs.correlations;
let q = inputs.target_q;
let (yy2, exp_yy2) = dot_product12(ctx, inputs.filtered_code, inputs.filtered_code);
let (xy2, exp_xy2) = dot_product12(ctx, inputs.target, inputs.filtered_code);
let (y1y2, exp_y1y2) = dot_product12(ctx, inputs.filtered_adaptive, inputs.filtered_code);
let mut coeff = [
g.energy,
negate(ctx, g.correlation),
extract_h(yy2),
extract_h(l_negate(ctx, xy2)),
extract_h(y1y2),
];
let exp_coeff = [
g.energy_exp,
g.correlation_exp + 1,
(exp_yy2 - 18) + 2 * q,
(exp_xy2 - 8) + q,
(exp_y1y2 - 8) + q,
];
let exp_code = exp_gcode0.0 + 4;
let exp_max = [
exp_coeff[0] - 13,
exp_coeff[1] - 14,
exp_coeff[2] + 15 + 2 * exp_code,
exp_coeff[3] + exp_code,
exp_coeff[4] + 1 + exp_code,
];
let largest = exp_max.iter().copied().fold(exp_max[0], i16::max);
let mut coeff_lo = [Word16(0); 5];
for i in 0..5 {
let down = (largest - exp_max[i]) + 2;
let aligned = l_shr(ctx, l_deposit_h(coeff[i]), down);
let (hi, lo) = l_extract(aligned);
coeff[i] = hi;
coeff_lo[i] = shr(ctx, lo, 3);
}
(coeff, coeff_lo)
}
#[must_use]
pub fn scale_code_gain(ctx: &mut DspContext, code_gain: Word32, frame_q: i16) -> Word16 {
let scaled = l_shl(ctx, code_gain, frame_q);
round(ctx, scaled)
}
#[cfg(test)]
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
mod tests {
use super::*;
use crate::codecs::amr::mode::{AmrMode, AmrVariant};
use crate::codecs::amr::storage;
use crate::codecs::amr::wb::params::FrameParams;
const TRACE: &str = include_str!("../../testdata/wb_enc_trace.txt");
const TRACED_FRAMES: usize = 3;
const BITSTREAM: &[u8] = include_bytes!("../../testdata/amrwb_enc_mode2.amr");
fn row(frame: usize, subframe: i32, name: &str) -> Vec<i32> {
let prefix = format!("T {frame} {subframe} {name} ");
let line = TRACE
.lines()
.find(|l| l.starts_with(&prefix))
.unwrap_or_else(|| panic!("trace row {frame} {subframe} {name} is missing"));
line.split_whitespace()
.skip(4)
.map(|t| t.parse().expect("trace values are integers"))
.collect()
}
fn vector(frame: usize, subframe: usize, name: &str) -> [Word16; L_SUBFR] {
let values = row(frame, subframe as i32, name);
assert_eq!(values.len(), L_SUBFR, "{name} is not a subframe vector");
let mut out = [Word16(0); L_SUBFR];
for (o, v) in out.iter_mut().zip(values) {
*o = Word16(v as i16);
}
out
}
fn scalar(frame: usize, subframe: i32, name: &str) -> i32 {
let values = row(frame, subframe, name);
assert_eq!(values.len(), 1, "{name} is not a scalar");
values[0]
}
fn pitch_correlations(
ctx: &mut DspContext,
xn: &[Word16; L_SUBFR],
y1: &[Word16; L_SUBFR],
) -> PitchCorrelations {
let (yy, exp_yy) = dot_product12(ctx, y1, y1);
let (xy, exp_xy) = dot_product12(ctx, xn, y1);
PitchCorrelations {
energy: extract_h(yy),
energy_exp: exp_yy,
correlation: extract_h(xy),
correlation_exp: exp_xy,
}
}
fn clip_memory_after(gain_pit: &[i32]) -> Vec<i16> {
let mut ctx = DspContext::new();
let mut mem = 9830i16;
let mut history = Vec::with_capacity(gain_pit.len());
for &g in gain_pit {
let acc = l_mult(&mut ctx, Word16(29491), Word16(mem));
let acc = l_mac(&mut ctx, acc, Word16(3277), Word16(g as i16));
mem = extract_h(acc).0.max(9830);
history.push(mem);
}
history
}
fn walk<F>(mut check: F) -> usize
where
F: FnMut(&mut DspContext, usize, usize, &QuantisedGains, u16),
{
let mut ctx = DspContext::new();
let mut predictor = GainPredictor::new();
let (_, frames) = storage::read(BITSTREAM).expect("the fixture parses");
let mode = AmrMode::new(AmrVariant::WideBand, 2).expect("12.65 kbit/s");
let gains: Vec<i32> = (0..TRACED_FRAMES)
.flat_map(|f| (0..4).map(move |s| scalar(f, s, "gain_pit")))
.collect();
let clip = clip_memory_after(&gains);
assert!(
clip.iter().all(|&m| m <= 14746),
"clipping would be active somewhere in the trace: {clip:?}"
);
let mut checked = 0usize;
for (frame, coded) in frames.iter().enumerate().take(TRACED_FRAMES) {
let params = FrameParams::parse(mode, &coded.data).expect("the frame parses");
let frame_q = scalar(frame, -1, "Q_new") as i16;
for subframe in 0..4 {
let expected_index = params.subframes[subframe].gain_index;
if scalar(frame, subframe as i32, "select") == 1 {
let xn = vector(frame, subframe, "xn");
let y1 = vector(frame, subframe, "y1");
let y2 = vector(frame, subframe, "y2");
let code = vector(frame, subframe, "code");
let correlations = pitch_correlations(&mut ctx, &xn, &y1);
let inputs = GainInputs {
target: &xn,
filtered_adaptive: &y1,
target_q: frame_q + scalar(frame, subframe as i32, "shift") as i16,
filtered_code: &y2,
code: &code,
correlations,
bits: GainBits::Seven,
pitch_gain: Word16(scalar(frame, subframe as i32, "gain1") as i16),
clip_pitch_gain: false,
};
let gains = predictor.quantise(&mut ctx, &inputs);
check(&mut ctx, frame, subframe, &gains, expected_index);
checked += 1;
} else {
let table_gain = Word16(QUA_GAIN_7B[2 * usize::from(expected_index) + 1]);
predictor.remember(&mut ctx, table_gain);
}
}
}
checked
}
#[test]
fn quantised_gains_match_the_reference_trace() {
let mut compared = 0usize;
let checked = walk(|ctx, frame, subframe, gains, _| {
let expected_pitch = scalar(frame, subframe as i32, "gain_pit") as i16;
let expected_code = scalar(frame, subframe as i32, "L_gain_code");
let frame_q = scalar(frame, -1, "Q_new") as i16;
let expected_scaled = scalar(frame, subframe as i32, "gain_code") as i16;
assert_eq!(
gains.pitch_gain.0, expected_pitch,
"gain_pit differs at frame {frame} subframe {subframe}"
);
assert_eq!(
gains.code_gain.0, expected_code,
"L_gain_code differs at frame {frame} subframe {subframe}"
);
assert_eq!(
scale_code_gain(ctx, gains.code_gain, frame_q).0,
expected_scaled,
"gain_code differs at frame {frame} subframe {subframe}"
);
compared += 1;
});
assert_eq!(checked, 9, "quantised {checked} subframes, expected 9");
assert_eq!(compared, 9, "compared {compared} subframes, expected 9");
}
#[test]
fn gain_index_matches_the_reference_bitstream() {
let mut compared = 0usize;
let checked = walk(|_, frame, subframe, gains, expected| {
assert_eq!(
gains.index, expected,
"gain index differs at frame {frame} subframe {subframe}"
);
compared += 1;
});
assert_eq!(checked, 9, "quantised {checked} subframes, expected 9");
assert_eq!(compared, 9, "compared {compared} subframes, expected 9");
}
#[test]
fn the_predictor_state_changes_the_chosen_index() {
let mut ctx = DspContext::new();
let xn = vector(2, 2, "xn");
let y1 = vector(2, 2, "y1");
let y2 = vector(2, 2, "y2");
let code = vector(2, 2, "code");
let correlations = pitch_correlations(&mut ctx, &xn, &y1);
let inputs = GainInputs {
target: &xn,
filtered_adaptive: &y1,
target_q: scalar(2, -1, "Q_new") as i16 + scalar(2, 2, "shift") as i16,
filtered_code: &y2,
code: &code,
correlations,
bits: GainBits::Seven,
pitch_gain: Word16(scalar(2, 2, "gain1") as i16),
clip_pitch_gain: false,
};
let mut fresh = GainPredictor::new();
let from_reset = fresh.quantise(&mut ctx, &inputs);
let expected = {
let (_, frames) = storage::read(BITSTREAM).expect("the fixture parses");
let mode = AmrMode::new(AmrVariant::WideBand, 2).expect("12.65 kbit/s");
FrameParams::parse(mode, &frames[2].data)
.expect("parses")
.subframes[2]
.gain_index
};
assert_ne!(
from_reset.index, expected,
"a reset predictor should not reproduce the tenth subframe's index"
);
}
#[test]
fn the_predictor_resets_to_minus_fourteen_decibels_and_shifts() {
let mut ctx = DspContext::new();
let mut predictor = GainPredictor::new();
assert_eq!(predictor.past_energy, [Word16(-14336); 4]);
predictor.remember(&mut ctx, Word16(2048));
assert_eq!(predictor.past_energy[0].0, 0);
assert_eq!(predictor.past_energy[1].0, -14336);
predictor.remember(&mut ctx, Word16(4096));
assert_eq!(predictor.past_energy[0].0, 6165);
assert_eq!(predictor.past_energy[1].0, 0);
assert_eq!(predictor.past_energy[2].0, -14336);
assert_eq!(predictor.past_energy[3].0, -14336);
predictor.reset();
assert_eq!(predictor.past_energy, [Word16(-14336); 4]);
}
#[test]
fn the_predictor_records_the_table_gain_not_the_applied_gain() {
let mut ctx = DspContext::new();
let mut a = GainPredictor::new();
let mut b = GainPredictor::new();
b.past_energy = [Word16(6000); 4];
let table_gain = Word16(QUA_GAIN_7B[2 * 40 + 1]);
a.remember(&mut ctx, table_gain);
b.remember(&mut ctx, table_gain);
assert_eq!(
a.past_energy[0], b.past_energy[0],
"the recorded energy must not depend on the predictor's own history"
);
}
#[test]
fn the_seven_bit_window_follows_the_unquantised_pitch_gain() {
let mut ctx = DspContext::new();
let zeros = [Word16(0); L_SUBFR];
let correlations = PitchCorrelations {
energy: Word16(0),
energy_exp: 0,
correlation: Word16(0),
correlation_exp: 0,
};
let mut inputs = GainInputs {
target: &zeros,
filtered_adaptive: &zeros,
target_q: 0,
filtered_code: &zeros,
code: &zeros,
correlations,
bits: GainBits::Seven,
pitch_gain: Word16(0),
clip_pitch_gain: false,
};
let (_, first, size) = GainPredictor::window(&mut ctx, &inputs);
assert_eq!((first, size), (0, 64));
inputs.pitch_gain = Word16(32767);
let (_, first, size) = GainPredictor::window(&mut ctx, &inputs);
assert_eq!((first, size), (64, 64));
assert_eq!(first + size, 128, "the window must stay inside the table");
inputs.clip_pitch_gain = true;
let (_, first, size) = GainPredictor::window(&mut ctx, &inputs);
assert_eq!(
(first, size),
(37, 64),
"clipping shortens the seven-bit pre-search, not the search"
);
}
#[test]
fn six_bit_clipping_shortens_the_search() {
let mut ctx = DspContext::new();
let zeros = [Word16(0); L_SUBFR];
let correlations = PitchCorrelations {
energy: Word16(0),
energy_exp: 0,
correlation: Word16(0),
correlation_exp: 0,
};
let mut inputs = GainInputs {
target: &zeros,
filtered_adaptive: &zeros,
target_q: 0,
filtered_code: &zeros,
code: &zeros,
correlations,
bits: GainBits::Six,
pitch_gain: Word16(32767),
clip_pitch_gain: false,
};
assert_eq!(GainPredictor::window(&mut ctx, &inputs).1, 0);
assert_eq!(GainPredictor::window(&mut ctx, &inputs).2, 64);
inputs.clip_pitch_gain = true;
assert_eq!(GainPredictor::window(&mut ctx, &inputs).1, 0);
assert_eq!(GainPredictor::window(&mut ctx, &inputs).2, 48);
}
#[test]
fn the_window_boundary_is_strict() {
let mut ctx = DspContext::new();
let zeros = [Word16(0); L_SUBFR];
let correlations = PitchCorrelations {
energy: Word16(0),
energy_exp: 0,
correlation: Word16(0),
correlation_exp: 0,
};
let boundary = QUA_GAIN_7B[RANGE];
let mut inputs = GainInputs {
target: &zeros,
filtered_adaptive: &zeros,
target_q: 0,
filtered_code: &zeros,
code: &zeros,
correlations,
bits: GainBits::Seven,
pitch_gain: Word16(boundary),
clip_pitch_gain: false,
};
let equal = GainPredictor::window(&mut ctx, &inputs).1;
inputs.pitch_gain = Word16(boundary + 1);
let above = GainPredictor::window(&mut ctx, &inputs).1;
assert_eq!(
above,
equal + 1,
"an equal pitch gain must not count, a greater one must"
);
}
#[test]
fn equal_distortions_keep_the_lowest_index() {
let mut ctx = DspContext::new();
for probe in [0i32, 1, -1, 12345, i32::MAX, i32::MIN] {
assert!(
!beats(&mut ctx, Word32(probe), Word32(probe)),
"an equal distortion must not displace the incumbent at {probe}"
);
}
assert!(beats(&mut ctx, Word32(-1), Word32(0)));
assert!(!beats(&mut ctx, Word32(0), Word32(-1)));
assert!(beats(&mut ctx, Word32(i32::MIN), Word32(i32::MAX)));
assert!(!beats(&mut ctx, Word32(i32::MAX), Word32(i32::MIN)));
}
#[test]
fn the_search_reports_the_smallest_distortion_in_its_window() {
let mut ctx = DspContext::new();
let mut predictor = GainPredictor::new();
let xn = vector(0, 0, "xn");
let y1 = vector(0, 0, "y1");
let y2 = vector(0, 0, "y2");
let code = vector(0, 0, "code");
let correlations = pitch_correlations(&mut ctx, &xn, &y1);
let inputs = GainInputs {
target: &xn,
filtered_adaptive: &y1,
target_q: scalar(0, -1, "Q_new") as i16 + scalar(0, 0, "shift") as i16,
filtered_code: &y2,
code: &code,
correlations,
bits: GainBits::Seven,
pitch_gain: Word16(scalar(0, 0, "gain1") as i16),
clip_pitch_gain: false,
};
let (_, first, size) = GainPredictor::window(&mut ctx, &inputs);
let chosen = predictor.quantise(&mut ctx, &inputs);
assert!(
(first..first + size).contains(&usize::from(chosen.index)),
"the chosen index must lie inside the window it searched"
);
assert_eq!(size, 64, "the seven-bit search always covers 64 candidates");
}
#[test]
fn gain_width_follows_the_frame_size() {
assert_eq!(GainBits::from_frame_bits(132), GainBits::Six);
assert_eq!(GainBits::from_frame_bits(177), GainBits::Six);
assert_eq!(GainBits::from_frame_bits(253), GainBits::Seven);
assert_eq!(GainBits::from_frame_bits(477), GainBits::Seven);
assert_eq!(GainBits::Six.bits(), 6);
assert_eq!(GainBits::Seven.bits(), 7);
}
}