use super::decoder_tables::{
GAIN_HIGHRATES, GAIN_LOWRATES, GAIN_MR475, QUA_GAIN_CODE, QUA_GAIN_PITCH,
};
use super::math::{log2, log2_norm, pow2};
use super::L_SUBFR;
use crate::fixed_point::arith::{abs_s, add, extract_h, extract_l, mult, round, sub};
use crate::fixed_point::arith32::{l_deposit_l, l_mac, l_msu, l_mult, l_sub};
use crate::fixed_point::div::div_s;
use crate::fixed_point::oper32::{l_comp, l_extract, mpy_32_16};
use crate::fixed_point::shift::{l_shl, l_shr, norm_l, norm_s, shl, shr, shr_r};
use crate::fixed_point::types::{DspContext, Word16, Word32};
mod rate {
pub const R4_75: u8 = 0;
pub const R5_15: u8 = 1;
pub const R5_90: u8 = 2;
pub const R6_70: u8 = 3;
pub const R7_40: u8 = 4;
pub const R7_95: u8 = 5;
pub const R10_2: u8 = 6;
pub const R12_2: u8 = 7;
}
use rate::{R10_2, R12_2, R4_75, R5_15, R5_90, R6_70, R7_40, R7_95};
const NPRED: usize = 4;
const PRED_DB: [i16; NPRED] = [5571, 4751, 2785, 1556];
const PRED_LOG2: [i16; NPRED] = [44, 37, 22, 12];
const MEAN_ENER_LOG2: i32 = 783_741;
const MIN_ENERGY_DB: i16 = -14336;
const MIN_ENERGY_LOG2: i16 = -2381;
const PDOWN: [i16; 7] = [32767, 32112, 32112, 26214, 9830, 6553, 6553];
const CDOWN: [i16; 7] = [32767, 32112, 32112, 32112, 32112, 32112, 22937];
const GAIN_BUFFER: usize = 5;
const GAIN_HISTORY: usize = 7;
const LSF_ORDER: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubframeGains {
pub pitch: Word16,
pub code: Word16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GainPrediction {
pub exponent: Word16,
pub fraction: Word16,
pub innovation_energy: Option<(Word16, Word16)>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WithPrevious {
pub current: bool,
pub previous: bool,
}
impl WithPrevious {
#[must_use]
pub const fn either(self) -> bool {
self.current || self.previous
}
#[must_use]
pub const fn both(self) -> bool {
self.current && self.previous
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FrameQuality {
pub bad: WithPrevious,
pub degraded: WithPrevious,
pub background_noise: bool,
pub voiced_hangover: i16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CodeGainPredictor {
past_db: [Word16; NPRED],
past_log2: [Word16; NPRED],
}
impl Default for CodeGainPredictor {
fn default() -> Self {
Self::new()
}
}
impl CodeGainPredictor {
#[must_use]
pub const fn new() -> Self {
Self {
past_db: [Word16(MIN_ENERGY_DB); NPRED],
past_log2: [Word16(MIN_ENERGY_LOG2); NPRED],
}
}
#[must_use]
pub fn predict(
&self,
ctx: &mut DspContext,
mode_index: u8,
code: &[Word16; L_SUBFR],
) -> GainPrediction {
let mut energy = Word32(0);
for &sample in code {
energy = l_mac(ctx, energy, sample, sample);
}
if mode_index == R12_2 {
self.predict_log2_domain(ctx, energy)
} else {
self.predict_db_domain(ctx, mode_index, energy)
}
}
fn predict_log2_domain(&self, ctx: &mut DspContext, energy: Word32) -> GainPrediction {
let rounded = round(ctx, energy);
let mean_energy = l_mult(ctx, rounded, Word16(26214));
let (exponent, fraction) = log2(ctx, mean_energy);
let measured = l_comp(sub(ctx, exponent, Word16(30)), fraction);
let mut predicted = Word32(MEAN_ENER_LOG2);
for (&past, &coeff) in self.past_log2.iter().zip(PRED_LOG2.iter()) {
predicted = l_mac(ctx, predicted, past, Word16(coeff));
}
let surplus = l_sub(ctx, predicted, measured);
let excess = l_shr(ctx, surplus, 1);
let (exponent, fraction) = l_extract(excess);
GainPrediction {
exponent,
fraction,
innovation_energy: None,
}
}
fn predict_db_domain(
&self,
ctx: &mut DspContext,
mode_index: u8,
energy: Word32,
) -> GainPrediction {
let norm = norm_l(energy);
let normalised = l_shl(ctx, energy, norm);
let (exponent, fraction) = log2_norm(ctx, normalised, norm);
let mut acc = mpy_32_16(exponent, fraction, Word16(-24660));
let innovation_energy = (mode_index == R7_95)
.then(|| (sub(ctx, Word16(-11), Word16(norm)), extract_h(normalised)));
let (k, k_scale) = match mode_index {
R7_40 => (32588, 32), R6_70 => (32268, 32), R7_95 => (17062, 64), _ => (16678, 64), };
acc = l_mac(ctx, acc, Word16(k), Word16(k_scale));
acc = l_shl(ctx, acc, 10);
for (&coeff, &past) in PRED_DB.iter().zip(self.past_db.iter()) {
acc = l_mac(ctx, acc, Word16(coeff), past);
}
let gcode0 = extract_h(acc);
let scale = if mode_index == R7_40 { 5439 } else { 5443 };
let scaled = l_mult(ctx, gcode0, Word16(scale)); let acc = l_shr(ctx, scaled, 8);
let (exponent, fraction) = l_extract(acc);
GainPrediction {
exponent,
fraction,
innovation_energy,
}
}
pub fn push(&mut self, log2_energy: Word16, db_energy: Word16) {
self.past_db.copy_within(0..NPRED - 1, 1);
self.past_log2.copy_within(0..NPRED - 1, 1);
self.past_log2[0] = log2_energy;
self.past_db[0] = db_energy;
}
#[must_use]
pub fn limited_average(&self, ctx: &mut DspContext) -> (Word16, Word16) {
let mut log2_avg = Word16(0);
for &past in &self.past_log2 {
log2_avg = add(ctx, log2_avg, past);
}
log2_avg = mult(ctx, log2_avg, Word16(8192));
if sub(ctx, log2_avg, Word16(MIN_ENERGY_LOG2)).0 < 0 {
log2_avg = Word16(MIN_ENERGY_LOG2);
}
let mut db_avg = Word16(0);
for &past in &self.past_db {
db_avg = add(ctx, db_avg, past);
}
db_avg = mult(ctx, db_avg, Word16(8192));
if sub(ctx, db_avg, Word16(MIN_ENERGY_DB)).0 < 0 {
db_avg = Word16(MIN_ENERGY_DB);
}
(log2_avg, db_avg)
}
pub const fn seed_directly(&mut self, db: Word16, log2: Word16) {
self.past_db = [db; NPRED];
self.past_log2 = [log2; NPRED];
}
pub fn reseed_from_sid(&mut self, ctx: &mut DspContext, log_en: Word16) {
let half = shr(ctx, log_en, 1);
let mut seed = sub(ctx, half, Word16(9000));
if seed.0 > 0 {
seed = Word16(0);
}
if sub(ctx, seed, Word16(-14436)).0 < 0 {
seed = Word16(-14436);
}
self.past_db = [seed; NPRED];
let seed = mult(ctx, Word16(5443), seed);
self.past_log2 = [seed; NPRED];
}
}
pub fn decode_joint(
ctx: &mut DspContext,
predictor: &mut CodeGainPredictor,
mode_index: u8,
index: u16,
code: &[Word16; L_SUBFR],
even_subframe: bool,
) -> SubframeGains {
assert!(
mode_index != R7_95 && mode_index != R12_2,
"{mode_index} quantises the gains separately; use decode_pitch_gain/decode_code_gain"
);
let base = usize::from(index) * 4;
let (pitch, g_fac, log2_energy, db_energy) = match mode_index {
R10_2 | R7_40 | R6_70 => {
let entry = &GAIN_HIGHRATES[base..base + 4];
(
Word16(entry[0]),
Word16(entry[1]),
Word16(entry[2]),
Word16(entry[3]),
)
}
R4_75 => {
let half = base + if even_subframe { 0 } else { 2 };
let pitch = Word16(GAIN_MR475[half]);
let g_fac = Word16(GAIN_MR475[half + 1]);
let (exponent, fraction) = log2(ctx, l_deposit_l(g_fac));
let exponent = sub(ctx, exponent, Word16(12));
let rounded_fraction = shr_r(ctx, fraction, 5); let whole = shl(ctx, exponent, 10); let log2_energy = add(ctx, rounded_fraction, whole);
let scaled = mpy_32_16(exponent, fraction, Word16(24660));
let widened = l_shl(ctx, scaled, 13);
let db_energy = round(ctx, widened);
(pitch, g_fac, log2_energy, db_energy)
}
_ => {
let entry = &GAIN_LOWRATES[base..base + 4];
(
Word16(entry[0]),
Word16(entry[1]),
Word16(entry[2]),
Word16(entry[3]),
)
}
};
let prediction = predictor.predict(ctx, mode_index, code);
let gcode0 = extract_l(pow2(ctx, Word16(14), prediction.fraction));
let acc = l_mult(ctx, g_fac, gcode0);
let denorm = sub(ctx, Word16(10), prediction.exponent).0;
let acc = l_shr(ctx, acc, denorm);
let code_gain = extract_h(acc);
predictor.push(log2_energy, db_energy);
SubframeGains {
pitch,
code: code_gain,
}
}
#[must_use]
pub fn decode_pitch_gain(ctx: &mut DspContext, mode_index: u8, index: u16) -> Word16 {
assert!(
mode_index == R7_95 || mode_index == R12_2,
"{mode_index} quantises the gains jointly; use decode_joint"
);
let gain = Word16(QUA_GAIN_PITCH[usize::from(index)]);
if mode_index == R12_2 {
let dropped = shr(ctx, gain, 2);
shl(ctx, dropped, 2)
} else {
gain
}
}
pub fn decode_code_gain(
ctx: &mut DspContext,
predictor: &mut CodeGainPredictor,
mode_index: u8,
index: u16,
code: &[Word16; L_SUBFR],
) -> Word16 {
assert!(
mode_index == R7_95 || mode_index == R12_2,
"{mode_index} quantises the gains jointly; use decode_joint"
);
let prediction = predictor.predict(ctx, mode_index, code);
let base = usize::from(index) * 3;
let entry = &QUA_GAIN_CODE[base..base + 3];
let g_fac = Word16(entry[0]);
let gain = if mode_index == R12_2 {
let gcode0 = extract_l(pow2(ctx, prediction.exponent, prediction.fraction));
let gcode0 = shl(ctx, gcode0, 4); let scaled = mult(ctx, gcode0, g_fac); shl(ctx, scaled, 1) } else {
let gcode0 = extract_l(pow2(ctx, Word16(14), prediction.fraction));
let acc = l_mult(ctx, g_fac, gcode0);
let denorm = sub(ctx, Word16(9), prediction.exponent).0;
extract_h(l_shr(ctx, acc, denorm))
};
predictor.push(Word16(entry[1]), Word16(entry[2]));
gain
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PitchGainConcealer {
recent: [Word16; GAIN_BUFFER],
last: Word16,
last_good: Word16,
}
impl Default for PitchGainConcealer {
fn default() -> Self {
Self::new()
}
}
impl PitchGainConcealer {
#[must_use]
pub const fn new() -> Self {
Self {
recent: [Word16(1640); GAIN_BUFFER],
last: Word16(0),
last_good: Word16(16384),
}
}
#[must_use]
pub fn conceal(&self, ctx: &mut DspContext, state: u8) -> Word16 {
let mut gain = median(ctx, &self.recent);
if sub(ctx, gain, self.last).0 > 0 {
gain = self.last;
}
mult(ctx, gain, Word16(PDOWN[usize::from(state)]))
}
pub fn update(&mut self, ctx: &mut DspContext, bad: WithPrevious, gain: Word16) -> Word16 {
let mut gain = gain;
if !bad.current {
if bad.previous && sub(ctx, gain, self.last_good).0 > 0 {
gain = self.last_good;
}
self.last_good = gain;
}
self.last = gain;
if sub(ctx, self.last, Word16(16384)).0 > 0 {
self.last = Word16(16384);
}
self.recent.copy_within(1..GAIN_BUFFER, 0);
self.recent[GAIN_BUFFER - 1] = self.last;
gain
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CodeGainConcealer {
recent: [Word16; GAIN_BUFFER],
last: Word16,
last_good: Word16,
}
impl Default for CodeGainConcealer {
fn default() -> Self {
Self::new()
}
}
impl CodeGainConcealer {
#[must_use]
pub const fn new() -> Self {
Self {
recent: [Word16(1); GAIN_BUFFER],
last: Word16(0),
last_good: Word16(1),
}
}
pub fn conceal(
&self,
ctx: &mut DspContext,
predictor: &mut CodeGainPredictor,
state: u8,
) -> Word16 {
let mut gain = median(ctx, &self.recent);
if sub(ctx, gain, self.last).0 > 0 {
gain = self.last;
}
let gain = mult(ctx, gain, Word16(CDOWN[usize::from(state)]));
let (log2_avg, db_avg) = predictor.limited_average(ctx);
predictor.push(log2_avg, db_avg);
gain
}
pub fn update(&mut self, ctx: &mut DspContext, bad: WithPrevious, gain: Word16) -> Word16 {
let mut gain = gain;
if !bad.current {
if bad.previous && sub(ctx, gain, self.last_good).0 > 0 {
gain = self.last_good;
}
self.last_good = gain;
}
self.last = gain;
self.recent.copy_within(1..GAIN_BUFFER, 0);
self.recent[GAIN_BUFFER - 1] = gain;
gain
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CodeGainSmoother {
history: [Word16; GAIN_HISTORY],
hang_var: Word16,
hang_count: Word16,
}
impl CodeGainSmoother {
#[must_use]
pub const fn new() -> Self {
Self {
history: [Word16(0); GAIN_HISTORY],
hang_var: Word16(0),
hang_count: Word16(0),
}
}
pub const fn reset_for_comfort_noise(&mut self) {
*self = Self::new();
self.hang_var = Word16(20);
}
pub fn smooth(
&mut self,
ctx: &mut DspContext,
mode_index: u8,
gain_code: Word16,
lsf: &[Word16; LSF_ORDER],
lsf_avg: &[Word16; LSF_ORDER],
quality: FrameQuality,
) -> Word16 {
let mut mixed = gain_code;
self.history.copy_within(1..GAIN_HISTORY, 0);
self.history[GAIN_HISTORY - 1] = gain_code;
let diff = spectral_motion(ctx, lsf, lsf_avg);
self.hang_var = if sub(ctx, diff, Word16(5325)).0 > 0 {
add(ctx, self.hang_var, Word16(1))
} else {
Word16(0)
};
if sub(ctx, self.hang_var, Word16(10)).0 > 0 {
self.hang_count = Word16(0);
}
if mode_index <= R6_70 || mode_index == R10_2 {
mixed = self.mix(ctx, mode_index, mixed, diff, quality);
}
self.hang_count = add(ctx, self.hang_count, Word16(1));
mixed
}
fn mix(
&self,
ctx: &mut DspContext,
mode_index: u8,
gain_code: Word16,
diff: Word16,
quality: FrameQuality,
) -> Word16 {
let lowest_three = matches!(mode_index, R4_75 | R5_15 | R5_90);
let errors = quality.degraded.both() || quality.bad.either();
let stronger = errors
&& sub(ctx, Word16(quality.voiced_hangover), Word16(1)).0 > 0
&& quality.background_noise
&& lowest_three;
let threshold = if stronger { 4506 } else { 3277 };
let excess = sub(ctx, diff, Word16(threshold));
let excess = if excess.0 > 0 { excess } else { Word16(0) };
let from_motion = if sub(ctx, Word16(2048), excess).0 < 0 {
Word16(8192)
} else {
shl(ctx, excess, 2)
};
let disabled =
sub(ctx, self.hang_count, Word16(40)).0 < 0 || sub(ctx, diff, Word16(5325)).0 > 0;
let bg_mix = if disabled { Word16(8192) } else { from_motion };
let mut acc = l_mult(ctx, Word16(6554), self.history[2]);
for &gain in &self.history[3..] {
acc = l_mac(ctx, acc, Word16(6554), gain);
}
let five_tap = round(ctx, acc);
let mean = if quality.bad.either() && quality.background_noise && lowest_three {
let mut acc = l_mult(ctx, Word16(4681), self.history[0]);
for &gain in &self.history[1..] {
acc = l_mac(ctx, acc, Word16(4681), gain);
}
round(ctx, acc)
} else {
five_tap
};
let mut acc = l_mult(ctx, bg_mix, gain_code);
acc = l_mac(ctx, acc, Word16(8192), mean);
acc = l_msu(ctx, acc, bg_mix, mean);
let widened = l_shl(ctx, acc, 2); round(ctx, widened) }
}
fn spectral_motion(
ctx: &mut DspContext,
lsf: &[Word16; LSF_ORDER],
lsf_avg: &[Word16; LSF_ORDER],
) -> Word16 {
let mut diff = Word16(0);
for (¤t, &average) in lsf.iter().zip(lsf_avg.iter()) {
assert!(
average.0 > 0,
"the LSF average must be positive to divide by"
);
let gap = sub(ctx, average, current);
let numerator = abs_s(ctx, gap);
let num_shift = sub(ctx, Word16(norm_s(numerator)), Word16(1)).0;
let numerator = shl(ctx, numerator, num_shift);
let den_shift = norm_s(average);
let denominator = shl(ctx, average, den_shift);
let ratio = div_s(numerator, denominator);
let biased = add(ctx, Word16(2), Word16(num_shift));
let shift = sub(ctx, biased, Word16(den_shift)).0;
let ratio = if shift >= 0 {
shr(ctx, ratio, shift)
} else {
shl(ctx, ratio, -shift)
};
diff = add(ctx, diff, ratio);
}
diff
}
fn median(ctx: &mut DspContext, values: &[Word16]) -> Word16 {
const NMAX: usize = 9;
let n = values.len();
assert!(n % 2 == 1 && n <= NMAX, "gmed_n is defined for odd n <= 9");
let mut remaining = [Word16(0); NMAX];
remaining[..n].copy_from_slice(values);
let mut rank = [0usize; NMAX];
let mut index = 0usize;
for slot in &mut rank[..n] {
let mut max = Word16(-32767);
for (j, &value) in remaining[..n].iter().enumerate() {
if sub(ctx, value, max).0 >= 0 {
max = value;
index = j;
}
}
remaining[index] = Word16(i16::MIN);
*slot = index;
}
values[rank[n / 2]]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codecs::amr::nb::vectors::{next_noise, rows, Row};
fn ctx() -> DspContext {
DspContext::default()
}
fn draw_masked(seed: &mut i16, shift: u32, mask: u32) -> i32 {
let value = next_noise(seed);
i32::try_from((i32::from(value).cast_unsigned() >> shift) & mask).expect("masked draw fits")
}
fn draw_code(seed: &mut i16) -> [Word16; L_SUBFR] {
let mut c = ctx();
let mut code = [Word16(0); L_SUBFR];
for sample in &mut code {
*sample = shr(&mut c, Word16(next_noise(seed)), 4);
}
code
}
const fn joint_index_mask(mode_index: u8) -> u32 {
match mode_index {
R4_75 => 0xFF,
R5_15 | R5_90 => 0x3F,
_ => 0x7F,
}
}
fn parse_tag(row: &Row, n: usize) -> i32 {
row.tag(n).parse().expect("integer token")
}
#[test]
fn joint_and_split_gain_decoding_are_bit_exact_against_ts26073() {
let mut compared = 0usize;
let mut c = ctx();
let mut predictor = CodeGainPredictor::new();
let mut seed = 0i16;
let mut split = false;
let mut mode_index = 0u8;
for row in rows("gains") {
match row.label {
"seq" => {
split = match row.tag(0) {
"joint" => false,
"split" => true,
other => panic!("unknown gain path {other:?}"),
};
mode_index = u8::try_from(parse_tag(&row, 1)).expect("mode index");
seed = i16::try_from(parse_tag(&row, 2)).expect("seed");
predictor = CodeGainPredictor::new();
}
"step" => {
let want = row.ints();
if split {
let pitch_index = draw_masked(&mut seed, 3, 0x0F);
let code_index = draw_masked(&mut seed, 3, 0x1F);
assert_eq!(pitch_index, want[0], "pitch index stream diverged");
assert_eq!(code_index, want[1], "code index stream diverged");
let pitch = decode_pitch_gain(
&mut c,
mode_index,
u16::try_from(pitch_index).expect("index"),
);
let code = draw_code(&mut seed);
let gain = decode_code_gain(
&mut c,
&mut predictor,
mode_index,
u16::try_from(code_index).expect("index"),
&code,
);
assert_eq!(
i32::from(pitch.0),
want[2],
"mode {mode_index} step {compared}: gain_pit"
);
assert_eq!(
i32::from(gain.0),
want[3],
"mode {mode_index} step {compared}: gain_cod"
);
} else {
let index = draw_masked(&mut seed, 3, joint_index_mask(mode_index));
assert_eq!(index, want[0], "joint index stream diverged");
let code = draw_code(&mut seed);
let gains = decode_joint(
&mut c,
&mut predictor,
mode_index,
u16::try_from(index).expect("index"),
&code,
want[1] != 0,
);
assert_eq!(
i32::from(gains.pitch.0),
want[2],
"mode {mode_index} step {compared}: gain_pit"
);
assert_eq!(
i32::from(gains.code.0),
want[3],
"mode {mode_index} step {compared}: gain_cod"
);
}
compared += 1;
}
other => panic!("unexpected row {other:?} in the gains section"),
}
}
assert_eq!(
compared, 192,
"compared {compared} gain steps, expected 192"
);
}
#[test]
fn gain_concealment_is_bit_exact_against_ts26073() {
let mut compared = 0usize;
let mut c = ctx();
let mut pitch_state = PitchGainConcealer::new();
let mut code_state = CodeGainConcealer::new();
let mut predictor = CodeGainPredictor::new();
let mut seed = 0i16;
for row in rows("conceal") {
match row.label {
"seq" => seed = i16::try_from(parse_tag(&row, 0)).expect("seed"),
"step" => {
let want = row.ints();
let bad = WithPrevious {
current: want[0] != 0,
previous: want[1] != 0,
};
let state = u8::try_from(want[2]).expect("concealment state");
let (pitch, code) = if bad.current {
(
pitch_state.conceal(&mut c, state),
code_state.conceal(&mut c, &mut predictor, state),
)
} else {
let pitch = draw_masked(&mut seed, 2, 0x3FFF);
let code = draw_masked(&mut seed, 3, 0x0FFF);
(
Word16(i16::try_from(pitch).expect("gain_pit")),
Word16(i16::try_from(code).expect("gain_code")),
)
};
let pitch = pitch_state.update(&mut c, bad, pitch);
let code = code_state.update(&mut c, bad, code);
assert_eq!(i32::from(pitch.0), want[3], "step {compared}: gain_pit");
assert_eq!(i32::from(code.0), want[4], "step {compared}: gain_code");
compared += 1;
}
other => panic!("unexpected row {other:?} in the conceal section"),
}
}
assert_eq!(
compared, 40,
"compared {compared} conceal steps, expected 40"
);
}
#[test]
fn code_gain_smoothing_is_bit_exact_against_ts26073() {
let mut compared = 0usize;
let mut c = ctx();
let mut smoother = CodeGainSmoother::new();
let mut seed = 0i16;
let mut lsf_avg = [Word16(0); LSF_ORDER];
let mut lsf = [Word16(0); LSF_ORDER];
let mut gain_code = 0i32;
for row in rows("cbgainav") {
match row.label {
"seed" => seed = i16::try_from(parse_tag(&row, 0)).expect("seed"),
"lspavg" => {
lsf_avg.copy_from_slice(&row.words());
}
"lsp" => {
gain_code = draw_masked(&mut seed, 3, 0x0FFF);
let want = row.i16s();
for (i, slot) in lsf.iter_mut().enumerate() {
let jitter = draw_masked(&mut seed, 9, 0x1F);
let value = 1900 + i32::try_from(i).expect("index") * 2510 + jitter;
*slot = Word16(i16::try_from(value).expect("lsf fits"));
assert_eq!(i32::from(slot.0), i32::from(want[i]), "lsf stream diverged");
}
}
"step" => {
let want: Vec<i32> = row
.parts()
.filter(|token| *token != "->")
.map(|token| token.parse().expect("integer token"))
.collect();
assert_eq!(want.len(), 8, "cbgainav step row shape");
assert_eq!(gain_code, want[0], "gain stream diverged");
let quality = FrameQuality {
bad: WithPrevious {
current: want[1] != 0,
previous: want[2] != 0,
},
degraded: WithPrevious {
current: want[3] != 0,
previous: want[4] != 0,
},
background_noise: want[5] != 0,
voiced_hangover: i16::try_from(want[6]).expect("hangover"),
};
let got = smoother.smooth(
&mut c,
R6_70,
Word16(i16::try_from(gain_code).expect("gain fits")),
&lsf,
&lsf_avg,
quality,
);
assert_eq!(i32::from(got.0), want[7], "step {compared}: mixed gain");
compared += 1;
}
other => panic!("unexpected row {other:?} in the cbgainav section"),
}
}
assert_eq!(
compared, 12,
"compared {compared} smoothing steps, expected 12"
);
}
#[test]
fn the_smoothing_fixture_never_reaches_the_mixing_path() {
let identical = rows("cbgainav")
.iter()
.filter(|row| row.label == "step")
.all(|row| {
let tokens: Vec<&str> = row.parts().collect();
tokens[0] == tokens[tokens.len() - 1]
});
assert!(
identical,
"the cbgainav vectors now exercise mixing; test against them directly"
);
}
#[test]
fn a_still_spectrum_mixes_all_the_way_to_the_running_mean() {
let mut c = ctx();
let mut smoother = CodeGainSmoother::new();
let lsf = [
Word16(2000),
Word16(4500),
Word16(7000),
Word16(9500),
Word16(12000),
Word16(14500),
Word16(17000),
Word16(19500),
Word16(22000),
Word16(24500),
];
let quality = FrameQuality::default();
for step in 0..40i16 {
let gain = Word16(100 + step);
let out = smoother.smooth(&mut c, R6_70, gain, &lsf, &lsf, quality);
assert_eq!(out, gain, "step {step} should still pass through");
}
let gain = Word16(4000);
let out = smoother.smooth(&mut c, R6_70, gain, &lsf, &lsf, quality);
let history: [i32; 5] = [136, 137, 138, 139, 4000];
let mut acc = 0i32;
for value in history {
acc += 6554 * value * 2;
}
let want = i16::try_from((acc + 0x8000) >> 16).expect("mean fits");
assert_eq!(out.0, want, "a zero mix constant must give the plain mean");
assert_ne!(out, gain, "the mix did not engage");
}
#[test]
fn smoothing_state_advances_for_the_rates_that_discard_the_result() {
let mut c = ctx();
let lsf = [Word16(2000); LSF_ORDER];
let quality = FrameQuality::default();
let mut smoothing = CodeGainSmoother::new();
let mut discarding = CodeGainSmoother::new();
for step in 0..8i16 {
let gain = Word16(500 + step * 37);
smoothing.smooth(&mut c, R6_70, gain, &lsf, &lsf, quality);
let passed = discarding.smooth(&mut c, R12_2, gain, &lsf, &lsf, quality);
assert_eq!(passed, gain, "12.2 must return its input unchanged");
}
assert_eq!(
smoothing.history, discarding.history,
"the gain history must advance for every rate"
);
assert_eq!(smoothing.hang_count, discarding.hang_count);
assert_eq!(smoothing.hang_var, discarding.hang_var);
}
#[test]
fn the_predictor_averages_before_scaling_not_after() {
let mut c = ctx();
let mut predictor = CodeGainPredictor::new();
for _ in 0..NPRED {
predictor.push(Word16(30000), Word16(30000));
}
let (log2_avg, db_avg) = predictor.limited_average(&mut c);
assert_eq!(log2_avg.0, 8191, "the sum must saturate before scaling");
assert_eq!(db_avg.0, 8191);
}
#[test]
fn the_reset_average_saturates_in_one_scale_and_not_the_other() {
let mut c = ctx();
let predictor = CodeGainPredictor::new();
let (log2_avg, db_avg) = predictor.limited_average(&mut c);
assert_eq!(
log2_avg.0, MIN_ENERGY_LOG2,
"the log2 sum must not saturate"
);
assert_eq!(db_avg.0, -8192, "the dB sum must saturate before scaling");
assert!(
db_avg.0 > MIN_ENERGY_DB,
"the floor is never actually reached here"
);
}
#[test]
fn only_the_log2_floor_is_reachable_through_the_saturating_sum() {
let mut c = ctx();
let mut predictor = CodeGainPredictor::new();
for _ in 0..NPRED {
predictor.push(Word16(i16::MIN), Word16(i16::MIN));
}
let (log2_avg, db_avg) = predictor.limited_average(&mut c);
assert_eq!(log2_avg.0, MIN_ENERGY_LOG2, "the log2 floor is reachable");
assert_eq!(db_avg.0, -8192, "and is what the dB scale bottoms out at");
for extreme in [i16::MIN, -20000, -14336, 0, 20000, i16::MAX] {
let mut predictor = CodeGainPredictor::new();
for _ in 0..NPRED {
predictor.push(Word16(extreme), Word16(extreme));
}
let (_, db_avg) = predictor.limited_average(&mut c);
assert!(
db_avg.0 >= -8192,
"history {extreme} averaged to {}, below the -8192 the sum can reach \
and so below the {MIN_ENERGY_DB} floor",
db_avg.0
);
}
}
#[test]
fn the_predictor_history_is_newest_first_and_four_deep() {
let mut predictor = CodeGainPredictor::new();
for step in 1..=4i16 {
predictor.push(Word16(step), Word16(step * 10));
}
assert_eq!(predictor.past_log2.map(|w| w.0), [4, 3, 2, 1]);
assert_eq!(predictor.past_db.map(|w| w.0), [40, 30, 20, 10]);
predictor.push(Word16(5), Word16(50));
assert_eq!(predictor.past_log2.map(|w| w.0), [5, 4, 3, 2]);
}
#[test]
fn the_pitch_concealer_clamps_its_state_but_not_its_output() {
let mut c = ctx();
let mut state = PitchGainConcealer::new();
let loud = Word16(20000);
let returned = state.update(&mut c, WithPrevious::default(), loud);
assert_eq!(returned, loud, "the returned gain must not be clamped");
assert_eq!(state.last.0, 16384, "the state must be clamped to 1.0");
assert_eq!(
state.recent[GAIN_BUFFER - 1].0,
16384,
"the clamped value is what enters the history"
);
}
#[test]
fn the_code_concealer_clamps_nothing() {
let mut c = ctx();
let mut state = CodeGainConcealer::new();
let loud = Word16(20000);
let returned = state.update(&mut c, WithPrevious::default(), loud);
assert_eq!(returned, loud);
assert_eq!(state.last, loud, "the code gain state has no clamp");
assert_eq!(state.recent[GAIN_BUFFER - 1], loud);
}
#[test]
fn a_good_frame_after_a_bad_one_is_limited_but_a_run_of_good_ones_is_not() {
let mut c = ctx();
let mut state = PitchGainConcealer::new();
state.update(
&mut c,
WithPrevious {
current: false,
previous: false,
},
Word16(4000),
);
state.update(
&mut c,
WithPrevious {
current: true,
previous: false,
},
Word16(9000),
);
let limited = state.update(
&mut c,
WithPrevious {
current: false,
previous: true,
},
Word16(9000),
);
assert_eq!(limited.0, 4000, "the first good frame must be limited");
let free = state.update(
&mut c,
WithPrevious {
current: false,
previous: false,
},
Word16(9000),
);
assert_eq!(free.0, 9000, "only the frame after an erasure is limited");
}
#[test]
fn only_the_code_concealer_advances_the_predictor() {
let mut c = ctx();
let mut predictor = CodeGainPredictor::new();
for step in 1..=4i16 {
predictor.push(Word16(step), Word16(step * 10));
}
let before = predictor;
let pitch = PitchGainConcealer::new();
let _substituted = pitch.conceal(&mut c, 3);
assert_eq!(
predictor, before,
"ec_gain_pitch must not touch the predictor"
);
let code = CodeGainConcealer::new();
code.conceal(&mut c, &mut predictor, 3);
assert_ne!(predictor, before, "ec_gain_code must advance the predictor");
let (log2_avg, db_avg) = before.limited_average(&mut c);
assert_eq!(predictor.past_log2[0], log2_avg);
assert_eq!(predictor.past_db[0], db_avg);
}
#[test]
fn the_median_is_the_middle_value_however_the_history_is_ordered() {
let mut c = ctx();
let values = [Word16(7), Word16(1), Word16(9), Word16(3), Word16(5)];
assert_eq!(median(&mut c, &values).0, 5);
let mut permuted = values;
for rotate in 1..5 {
permuted.rotate_left(rotate);
assert_eq!(median(&mut c, &permuted).0, 5, "rotation {rotate}");
}
let signed = [Word16(-9), Word16(-1), Word16(-5), Word16(-3), Word16(-7)];
assert_eq!(median(&mut c, &signed).0, -5);
}
#[test]
fn the_median_reproduces_the_references_degenerate_case() {
let mut c = ctx();
let floor = [Word16(i16::MIN); GAIN_BUFFER];
assert_eq!(median(&mut c, &floor).0, i16::MIN);
let tied = [Word16(4), Word16(4), Word16(4), Word16(1), Word16(9)];
assert_eq!(median(&mut c, &tied).0, 4);
}
#[test]
fn twelve_two_clears_two_bits_of_the_pitch_quantiser_and_seven_ninety_five_does_not() {
let mut c = ctx();
for index in 0..16u16 {
let wide = decode_pitch_gain(&mut c, R7_95, index);
let narrow = decode_pitch_gain(&mut c, R12_2, index);
assert_eq!(wide.0, QUA_GAIN_PITCH[usize::from(index)]);
assert_eq!(narrow.0, wide.0 & !3, "index {index}");
}
}
#[test]
fn the_pitch_quantiser_stays_inside_its_q14_range() {
for (index, &gain) in QUA_GAIN_PITCH.iter().enumerate() {
assert!(
(0..=19661).contains(&gain),
"entry {index} is {gain}, outside the Q14 pitch-gain range"
);
}
}
#[test]
fn the_two_gain_paths_denormalise_by_different_shifts() {
let mut c = ctx();
let code = [Word16(1200); L_SUBFR];
let index = 31u16;
let g_fac = Word16(QUA_GAIN_CODE[usize::from(index) * 3]);
let mut split_state = CodeGainPredictor::new();
let prediction = split_state.predict(&mut c, R7_95, &code);
let gcode0 = extract_l(pow2(&mut c, Word16(14), prediction.fraction));
let product = l_mult(&mut c, g_fac, gcode0);
let nine = sub(&mut c, Word16(9), prediction.exponent).0;
let with_nine = extract_h(l_shr(&mut c, product, nine));
let ten = sub(&mut c, Word16(10), prediction.exponent).0;
let with_ten = extract_h(l_shr(&mut c, product, ten));
assert_ne!(
with_nine, with_ten,
"pick a louder case; the two shifts agree"
);
let split = decode_code_gain(&mut c, &mut split_state, R7_95, index, &code);
assert_eq!(split, with_nine, "the split path must shift by 9 - exp");
let joint_index = 100u16;
let joint_g_fac = Word16(GAIN_HIGHRATES[usize::from(joint_index) * 4 + 1]);
let mut joint_state = CodeGainPredictor::new();
let prediction = joint_state.predict(&mut c, R6_70, &code);
let gcode0 = extract_l(pow2(&mut c, Word16(14), prediction.fraction));
let product = l_mult(&mut c, joint_g_fac, gcode0);
let nine = sub(&mut c, Word16(9), prediction.exponent).0;
let with_nine = extract_h(l_shr(&mut c, product, nine));
let ten = sub(&mut c, Word16(10), prediction.exponent).0;
let with_ten = extract_h(l_shr(&mut c, product, ten));
assert_ne!(
with_nine, with_ten,
"pick a louder case; the two shifts agree"
);
let joint = decode_joint(&mut c, &mut joint_state, R6_70, joint_index, &code, true);
assert_eq!(
joint.code, with_ten,
"the joint path must shift by 10 - exp"
);
}
#[test]
fn seven_forty_uses_its_own_slightly_wrong_constant() {
let mut c = ctx();
let code = [Word16(2000); L_SUBFR];
let predictor = CodeGainPredictor::new();
let is641 = predictor.predict(&mut c, R7_40, &code);
let correct = predictor.predict(&mut c, R10_2, &code);
assert_ne!(
(is641.exponent, is641.fraction),
(correct.exponent, correct.fraction),
"7.40 must keep the IS-641 constant"
);
}
#[test]
fn only_seven_ninety_five_reports_the_innovation_energy() {
let mut c = ctx();
let code = [Word16(3000); L_SUBFR];
let predictor = CodeGainPredictor::new();
for mode_index in 0..=R12_2 {
let prediction = predictor.predict(&mut c, mode_index, &code);
assert_eq!(
prediction.innovation_energy.is_some(),
mode_index == R7_95,
"mode {mode_index} innovation energy"
);
}
}
#[test]
fn the_predicted_fraction_is_never_negative() {
let mut c = ctx();
let predictor = CodeGainPredictor::new();
let mut seed = 4242i16;
for mode_index in 0..=R12_2 {
for _ in 0..8 {
let mut code = [Word16(0); L_SUBFR];
for sample in &mut code {
*sample = shr(&mut c, Word16(next_noise(&mut seed)), 2);
}
let prediction = predictor.predict(&mut c, mode_index, &code);
assert!(
prediction.fraction.0 >= 0,
"mode {mode_index}: fraction {} is negative",
prediction.fraction.0
);
}
}
}
#[test]
fn a_silent_innovation_predicts_without_dividing_by_zero() {
let mut c = ctx();
let mut predictor = CodeGainPredictor::new();
let silence = [Word16(0); L_SUBFR];
for mode_index in 0..=R12_2 {
let prediction = predictor.predict(&mut c, mode_index, &silence);
assert!(prediction.fraction.0 >= 0);
}
decode_joint(&mut c, &mut predictor, R4_75, 0, &silence, true);
decode_code_gain(&mut c, &mut predictor, R12_2, 0, &silence);
}
#[test]
fn four_seventy_five_reads_a_different_half_of_its_entry_per_subframe() {
let mut c = ctx();
let code = [Word16(1500); L_SUBFR];
let mut even_state = CodeGainPredictor::new();
let even = decode_joint(&mut c, &mut even_state, R4_75, 37, &code, true);
let mut odd_state = CodeGainPredictor::new();
let odd = decode_joint(&mut c, &mut odd_state, R4_75, 37, &code, false);
assert_eq!(even.pitch.0, GAIN_MR475[37 * 4]);
assert_eq!(odd.pitch.0, GAIN_MR475[37 * 4 + 2]);
assert_ne!(
even.pitch, odd.pitch,
"the two halves of entry 37 are identical; pick another index"
);
}
#[test]
fn comfort_noise_reseeding_leaves_the_two_scales_related_not_equal() {
let mut c = ctx();
let mut predictor = CodeGainPredictor::new();
predictor.reseed_from_sid(&mut c, Word16(12000));
let db = predictor.past_db[0];
let log2_scaled = predictor.past_log2[0];
assert!(db.0 <= 0, "the seed is clamped at zero from above");
assert!(db.0 >= -14436, "and floored below");
assert_eq!(log2_scaled, mult(&mut c, Word16(5443), db));
assert!(predictor.past_db.iter().all(|&v| v == db));
assert!(predictor.past_log2.iter().all(|&v| v == log2_scaled));
predictor.reseed_from_sid(&mut c, Word16(32000));
assert_eq!(predictor.past_db[0].0, 0);
}
#[test]
fn comfort_noise_wipes_the_smoother_and_re_arms_its_hangover() {
let mut c = ctx();
let mut smoother = CodeGainSmoother::new();
let lsf = [Word16(2000); LSF_ORDER];
for step in 0..6i16 {
smoother.smooth(
&mut c,
R6_70,
Word16(700 + step),
&lsf,
&lsf,
FrameQuality::default(),
);
}
assert_ne!(smoother.history, [Word16(0); GAIN_HISTORY]);
smoother.reset_for_comfort_noise();
assert_eq!(smoother.history, [Word16(0); GAIN_HISTORY]);
assert_eq!(smoother.hang_count.0, 0);
assert_eq!(
smoother.hang_var.0, 20,
"the hangover is re-armed, not cleared"
);
}
}