use super::decoder_tables::MEAN_LSF_5;
use crate::fixed_point::arith::{add, extract_h, round, sub};
use crate::fixed_point::arith32::{l_deposit_h, l_mac, l_msu};
use crate::fixed_point::shift::{l_shl, shl, shr};
use crate::fixed_point::types::{DspContext, Word16, Word32};
const LP_ORDER: usize = 10;
const ENERGY_HISTORY: usize = 60;
const RECENT_START: usize = 2 * ENERGY_HISTORY / 3;
const MAX_SCAN_SKIP: usize = 4;
const FRAME_ENERGY_LIMIT: Word16 = Word16(17578);
const LOWER_NOISE_LIMIT: Word16 = Word16(20);
const UPPER_NOISE_LIMIT: Word16 = Word16(1953);
const NOISE_HANGOVER_MAX: Word16 = Word16(30);
const VOICED_HANGOVER_MAX: Word16 = Word16(10);
const LTP_LIMIT_BASE: Word16 = Word16(13926);
const LTP_LIMIT_TIGHT: Word16 = Word16(15565);
const LTP_LIMIT_TIGHTEST: Word16 = Word16(16383);
const MEDIAN_MAX: usize = 9;
const LSF_SMOOTH: Word16 = Word16(5243);
#[must_use]
pub fn median(ctx: &mut DspContext, window: &[Word16]) -> Word16 {
let n = window.len();
assert!(
n <= MEDIAN_MAX && n % 2 == 1,
"gmed_n is defined for an odd window of at most {MEDIAN_MAX}, got {n}"
);
let mut pool = [Word16(0); MEDIAN_MAX];
pool[..n].copy_from_slice(window);
let mut winner = 0usize;
let mut ranked = [0usize; MEDIAN_MAX];
for slot in &mut ranked[..n] {
let mut best = Word16(-32767);
for (j, &candidate) in pool[..n].iter().enumerate() {
if sub(ctx, candidate, best).0 >= 0 {
best = candidate;
winner = j;
}
}
pool[winner] = Word16(i16::MIN);
*slot = winner;
}
window[ranked[n / 2]]
}
#[must_use]
pub fn interpolate_lsf(
ctx: &mut DspContext,
lsf_old: &[Word16; LP_ORDER],
lsf_new: &[Word16; LP_ORDER],
subframe_start: usize,
) -> [Word16; LP_ORDER] {
let mut out = [Word16(0); LP_ORDER];
match subframe_start {
0 => {
for (i, slot) in out.iter_mut().enumerate() {
let quarter_old = shr(ctx, lsf_old[i], 2);
let three_quarter_old = sub(ctx, lsf_old[i], quarter_old);
let quarter_new = shr(ctx, lsf_new[i], 2);
*slot = add(ctx, three_quarter_old, quarter_new);
}
}
40 => {
for (i, slot) in out.iter_mut().enumerate() {
let half_old = shr(ctx, lsf_old[i], 1);
let half_new = shr(ctx, lsf_new[i], 1);
*slot = add(ctx, half_old, half_new);
}
}
80 => {
for (i, slot) in out.iter_mut().enumerate() {
let quarter_old = shr(ctx, lsf_old[i], 2);
let quarter_new = shr(ctx, lsf_new[i], 2);
let three_quarter_new = sub(ctx, lsf_new[i], quarter_new);
*slot = add(ctx, quarter_old, three_quarter_new);
}
}
120 => out = *lsf_new,
other => panic!("Int_lsf is defined at subframe starts 0, 40, 80 and 120, got {other}"),
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LsfAverage {
mean: [Word16; LP_ORDER],
}
impl Default for LsfAverage {
fn default() -> Self {
Self::new()
}
}
impl LsfAverage {
#[must_use]
pub fn new() -> Self {
Self {
mean: MEAN_LSF_5.map(Word16),
}
}
#[must_use]
pub const fn mean(&self) -> &[Word16; LP_ORDER] {
&self.mean
}
pub fn update(&mut self, ctx: &mut DspContext, lsf: &[Word16; LP_ORDER]) {
for (slot, &fresh) in self.mean.iter_mut().zip(lsf.iter()) {
let mut acc = l_deposit_h(*slot);
acc = l_msu(ctx, acc, LSF_SMOOTH, *slot);
acc = l_mac(ctx, acc, LSF_SMOOTH, fresh);
*slot = round(ctx, acc);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceDetector {
energy_history: [Word16; ENERGY_HISTORY],
noise_hangover: Word16,
voiced_hangover: Word16,
background_noise: bool,
}
impl Default for SourceDetector {
fn default() -> Self {
Self::new()
}
}
impl SourceDetector {
#[must_use]
pub const fn new() -> Self {
Self {
energy_history: [Word16(0); ENERGY_HISTORY],
noise_hangover: Word16(0),
voiced_hangover: Word16(0),
background_noise: false,
}
}
#[must_use]
pub const fn background_noise(&self) -> bool {
self.background_noise
}
#[must_use]
pub const fn voiced_hangover(&self) -> Word16 {
self.voiced_hangover
}
pub fn update(
&mut self,
ctx: &mut DspContext,
ltp_gains: &[Word16; MEDIAN_MAX],
synthesis: &[Word16; super::L_FRAME],
) -> bool {
let current = Self::frame_energy(ctx, synthesis);
let mut quietest = Word16(i16::MAX);
for &past in &self.energy_history {
if sub(ctx, past, quietest).0 < 0 {
quietest = past;
}
}
let noise_floor = shl(ctx, quietest, 4);
let mut loudest = self.energy_history[0];
for &past in &self.energy_history[1..ENERGY_HISTORY - MAX_SCAN_SKIP] {
if sub(ctx, loudest, past).0 < 0 {
loudest = past;
}
}
let mut loudest_recent = self.energy_history[RECENT_START];
for &past in &self.energy_history[RECENT_START + 1..] {
if sub(ctx, loudest_recent, past).0 < 0 {
loudest_recent = past;
}
}
let noise_like = sub(ctx, loudest, LOWER_NOISE_LIMIT).0 > 0
&& sub(ctx, current, FRAME_ENERGY_LIMIT).0 < 0
&& sub(ctx, current, LOWER_NOISE_LIMIT).0 > 0
&& (sub(ctx, current, noise_floor).0 < 0
|| sub(ctx, loudest_recent, UPPER_NOISE_LIMIT).0 < 0);
if noise_like {
let bumped = add(ctx, self.noise_hangover, Word16(1));
self.noise_hangover = if sub(ctx, bumped, NOISE_HANGOVER_MAX).0 > 0 {
NOISE_HANGOVER_MAX
} else {
bumped
};
} else {
self.noise_hangover = Word16(0);
}
let inbg = sub(ctx, self.noise_hangover, Word16(1)).0 > 0;
self.energy_history.copy_within(1.., 0);
self.energy_history[ENERGY_HISTORY - 1] = current;
let past_first_threshold = sub(ctx, self.noise_hangover, Word16(8)).0 > 0;
let past_second_threshold = sub(ctx, self.noise_hangover, Word16(15)).0 > 0;
let ltp_limit = if past_second_threshold {
LTP_LIMIT_TIGHTEST
} else if past_first_threshold {
LTP_LIMIT_TIGHT
} else {
LTP_LIMIT_BASE
};
let recent_voiced = {
let recent_median = median(ctx, <p_gains[4..]);
sub(ctx, recent_median, ltp_limit).0 > 0
};
let voiced = if sub(ctx, self.noise_hangover, Word16(20)).0 > 0 {
let full_median = median(ctx, ltp_gains);
sub(ctx, full_median, ltp_limit).0 > 0
} else {
recent_voiced
};
if voiced {
self.voiced_hangover = Word16(0);
} else {
let bumped = add(ctx, self.voiced_hangover, Word16(1));
self.voiced_hangover = if sub(ctx, bumped, VOICED_HANGOVER_MAX).0 > 0 {
VOICED_HANGOVER_MAX
} else {
bumped
};
}
self.background_noise = inbg;
inbg
}
fn frame_energy(ctx: &mut DspContext, synthesis: &[Word16; super::L_FRAME]) -> Word16 {
let mut acc = Word32(0);
for &sample in synthesis {
acc = l_mac(ctx, acc, sample, sample);
}
extract_h(l_shl(ctx, acc, 2))
}
}
#[cfg(test)]
mod tests {
use super::super::vectors::{rows, Row};
use super::super::L_FRAME;
use super::*;
fn words(row: &Row, label: &str, len: usize) -> Vec<Word16> {
assert_eq!(
row.label, label,
"expected a {label:?} row, got {:?}",
row.label
);
let v = row.words();
assert_eq!(v.len(), len, "{label} row should carry {len} values");
v
}
fn array<const N: usize>(v: &[Word16]) -> [Word16; N] {
let mut out = [Word16(0); N];
out.copy_from_slice(v);
out
}
#[test]
fn background_noise_detection_is_bit_exact_against_ts26073() {
let rows = rows("bgnscd");
assert_eq!(
rows[0].label, "seed",
"the bgnscd section should open with its seed"
);
let mut detector = SourceDetector::new();
let mut ctx = DspContext::default();
let mut compared = 0;
for (n, frame) in rows[1..].chunks(3).enumerate() {
let gains = words(&frame[0], "ltp", MEDIAN_MAX);
let synth = words(&frame[1], "syn", L_FRAME);
assert_eq!(
frame[2].label, "step",
"each bgnscd case ends with a step row"
);
let want = frame[2].i16s();
assert_eq!(want.len(), 2, "a step row is `bgn hangover`");
let bgn = detector.update(&mut ctx, &array(&gains), &array(&synth));
assert_eq!(
i16::from(bgn),
want[0],
"bgnscd case {n}: decision {} but the reference gives {}",
i16::from(bgn),
want[0]
);
assert_eq!(
detector.voiced_hangover().0,
want[1],
"bgnscd case {n}: voiced hangover {} but the reference gives {}",
detector.voiced_hangover().0,
want[1]
);
assert_eq!(
i16::from(detector.background_noise()),
want[0],
"bgnscd case {n}: the latched decision disagrees with the returned one"
);
compared += 1;
}
assert_eq!(
compared, 10,
"compared {compared} bgnscd cases, expected 10"
);
}
#[test]
fn the_lsf_average_is_bit_exact_against_ts26073() {
let rows = rows("lspavg");
assert_eq!(
rows[0].label, "seed",
"the lspavg section should open with its seed"
);
let mut average = LsfAverage::new();
let mut ctx = DspContext::default();
let mut compared = 0;
for (n, case) in rows[1..].chunks(2).enumerate() {
let lsf = words(&case[0], "lsf", LP_ORDER);
let want = words(&case[1], "mean", LP_ORDER);
average.update(&mut ctx, &array(&lsf));
for (i, (&got, &expected)) in average.mean().iter().zip(want.iter()).enumerate() {
assert_eq!(
got.0, expected.0,
"lspavg case {n}: mean[{i}] = {} but the reference gives {}",
got.0, expected.0
);
}
compared += 1;
}
assert_eq!(
compared, 10,
"compared {compared} lspavg cases, expected 10"
);
}
#[test]
fn lsf_interpolation_is_bit_exact_against_ts26073() {
let rows = rows("intlsf");
assert_eq!(
rows[0].label, "seed",
"the intlsf section should open with its seed"
);
let lsf_old: [Word16; LP_ORDER] = array(&words(&rows[1], "old", LP_ORDER));
let lsf_new: [Word16; LP_ORDER] = array(&words(&rows[2], "new", LP_ORDER));
let mut ctx = DspContext::default();
let mut compared = 0;
for case in rows[3..].chunks(2) {
assert_eq!(
case[0].label, "case",
"each intlsf case opens with its subframe start"
);
let start =
usize::try_from(case[0].ints()[0]).expect("a subframe start is non-negative");
let want = words(&case[1], "out", LP_ORDER);
let got = interpolate_lsf(&mut ctx, &lsf_old, &lsf_new, start);
for (i, (&g, &w)) in got.iter().zip(want.iter()).enumerate() {
assert_eq!(
g.0, w.0,
"intlsf at i_subfr {start}: out[{i}] = {} but the reference gives {}",
g.0, w.0
);
}
compared += 1;
}
assert_eq!(compared, 4, "compared {compared} intlsf cases, expected 4");
}
fn splitmix(state: &mut u64) -> u32 {
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
(z >> 32) as u32
}
#[test]
fn the_median_is_a_real_order_statistic() {
let mut ctx = DspContext::default();
let mut state = 0x5EED_1234_u64;
let mut compared = 0;
for n in [1usize, 3, 5, 7, 9] {
for _ in 0..400 {
let window: Vec<Word16> = (0..n)
.map(|_| {
let v = i32::try_from(splitmix(&mut state) % 65535).expect("small") - 32767;
Word16(i16::try_from(v).expect("in range by construction"))
})
.collect();
let mut sorted: Vec<i16> = window.iter().map(|w| w.0).collect();
sorted.sort_unstable();
assert_eq!(
median(&mut ctx, &window).0,
sorted[n / 2],
"median of {window:?} disagrees with the sorted middle"
);
compared += 1;
}
}
assert_eq!(compared, 2000, "compared {compared} windows, expected 2000");
}
#[test]
fn the_median_reproduces_the_reference_i16_min_pathology() {
let mut ctx = DspContext::default();
let window = [
Word16(i16::MIN),
Word16(i16::MIN),
Word16(5),
Word16(i16::MIN),
Word16(i16::MIN),
];
assert_eq!(median(&mut ctx, &window).0, 5);
let window = [
Word16(i16::MIN),
Word16(-32767),
Word16(5),
Word16(i16::MIN),
Word16(i16::MIN),
];
assert_eq!(median(&mut ctx, &window).0, -32767);
let window = [
Word16(i16::MIN),
Word16(i16::MIN),
Word16(i16::MIN),
Word16(i16::MIN),
Word16(7),
Word16(9),
Word16(i16::MIN),
Word16(i16::MIN),
Word16(i16::MIN),
];
assert_eq!(median(&mut ctx, &window).0, 7);
let floor = [Word16(i16::MIN); MEDIAN_MAX];
assert_eq!(median(&mut ctx, &floor).0, i16::MIN);
let window = [
Word16(i16::MIN),
Word16(5),
Word16(-9),
Word16(400),
Word16(7),
];
let mut sorted: Vec<i16> = window.iter().map(|w| w.0).collect();
sorted.sort_unstable();
assert_eq!(median(&mut ctx, &window).0, sorted[2]);
}
#[test]
fn a_tie_hands_the_rank_to_the_last_index() {
let mut ctx = DspContext::default();
let window = [Word16(7), Word16(9), Word16(1), Word16(9), Word16(3)];
assert_eq!(median(&mut ctx, &window).0, 7);
let window = [Word16(100); 5];
assert_eq!(median(&mut ctx, &window).0, 100);
}
#[test]
fn lsf_interpolation_carries_the_weights_it_claims() {
let mut ctx = DspContext::default();
let mut state = 0xC0FF_EE00_u64;
let mut compared = 0;
for _ in 0..500 {
let mut lsf_old = [Word16(0); LP_ORDER];
let mut lsf_new = [Word16(0); LP_ORDER];
for i in 0..LP_ORDER {
lsf_old[i] = Word16(i16::try_from(splitmix(&mut state) % 16385).expect("in range"));
lsf_new[i] = Word16(i16::try_from(splitmix(&mut state) % 16385).expect("in range"));
}
for (k, start) in [0usize, 40, 80, 120].into_iter().enumerate() {
let got = interpolate_lsf(&mut ctx, &lsf_old, &lsf_new, start);
let weight = i32::try_from(k).expect("small") + 1;
for i in 0..LP_ORDER {
let old = i32::from(lsf_old[i].0);
let new = i32::from(lsf_new[i].0);
let exact_quarters = (4 - weight) * old + weight * new;
let deviation = i32::from(got[i].0) * 4 - exact_quarters;
assert!(
(-4..=3).contains(&deviation),
"i_subfr {start}: coefficient {i} is {deviation} quarters off \
{exact_quarters}/4 — the subframe weight is wrong, not just rounded"
);
let (lo, hi) = (old.min(new), old.max(new));
assert!(
(lo - 1..=hi).contains(&i32::from(got[i].0)),
"i_subfr {start}: coefficient {i} left [{lo}, {hi}]"
);
}
compared += 1;
}
assert_eq!(
interpolate_lsf(&mut ctx, &lsf_old, &lsf_new, 120),
lsf_new,
"the fourth subframe is a plain copy of the new vector"
);
}
assert_eq!(
compared, 2000,
"compared {compared} interpolations, expected 2000"
);
}
#[test]
fn the_lsf_average_contracts_toward_its_input_without_overshooting() {
let mut ctx = DspContext::default();
let mut state = 0xBEEF_0007_u64;
for _ in 0..40 {
let mut average = LsfAverage::new();
let mut target = [Word16(0); LP_ORDER];
for slot in &mut target {
*slot = Word16(i16::try_from(splitmix(&mut state) % 16385).expect("in range"));
}
let mut previous = *average.mean();
for step in 0..200 {
average.update(&mut ctx, &target);
for i in 0..LP_ORDER {
let was = i32::from(previous[i].0) - i32::from(target[i].0);
let now = i32::from(average.mean()[i].0) - i32::from(target[i].0);
assert!(
now.abs() <= was.abs(),
"step {step}: coefficient {i} moved away from its target"
);
assert!(
was.signum() * now.signum() >= 0,
"step {step}: coefficient {i} overshot its target"
);
}
previous = *average.mean();
}
for (i, (&settled, &want)) in average.mean().iter().zip(target.iter()).enumerate() {
let gap = i32::from(settled.0) - i32::from(want.0);
assert!(
gap.abs() <= 3,
"coefficient {i} settled {gap} away from its target"
);
}
}
}
fn square(amplitude: i16) -> [Word16; L_FRAME] {
let mut synth = [Word16(0); L_FRAME];
for (i, slot) in synth.iter_mut().enumerate() {
*slot = Word16(if i % 2 == 0 { amplitude } else { -amplitude });
}
synth
}
fn quiet_frame() -> [Word16; L_FRAME] {
square(300)
}
fn first_noise_frame(frames: &[[Word16; L_FRAME]], gain: i16) -> Option<usize> {
let mut ctx = DspContext::default();
let mut detector = SourceDetector::new();
let gains = [Word16(gain); MEDIAN_MAX];
frames
.iter()
.position(|f| detector.update(&mut ctx, &gains, f))
.map(|i| i + 1)
}
#[test]
fn frame_energy_saturates_where_the_reference_says_it_does() {
let mut ctx = DspContext::default();
for (amplitude, expected) in [(300i16, 1757i16), (392, 3001), (700, 9570), (32767, 32767)] {
let got = SourceDetector::frame_energy(&mut ctx, &square(amplitude));
assert_eq!(
got.0, expected,
"amplitude {amplitude}: energy {} but the reference gives {expected}",
got.0
);
}
}
#[test]
fn a_loud_frame_can_never_be_background_noise() {
let mut ctx = DspContext::default();
let mut detector = SourceDetector::new();
let mut state = 0x1234_ABCD_u64;
for frame in 0..80 {
let mut synth = [Word16(0); L_FRAME];
for slot in &mut synth {
let v = i16::try_from(splitmix(&mut state) % 4096).expect("in range") - 2048;
*slot = Word16(v);
}
let gains = [Word16(0); MEDIAN_MAX];
assert!(
!detector.update(&mut ctx, &gains, &synth),
"frame {frame}: a full-band frame was called background noise"
);
assert!(detector.voiced_hangover().0 <= VOICED_HANGOVER_MAX.0);
}
assert_eq!(detector.voiced_hangover().0, VOICED_HANGOVER_MAX.0);
}
#[test]
fn the_detector_needs_two_frames_and_the_history_fills_from_the_top() {
let mut ctx = DspContext::default();
let mut detector = SourceDetector::new();
let gains = [Word16(0); MEDIAN_MAX];
let synth = quiet_frame();
let mut first_positive = None;
for frame in 0..12 {
if detector.update(&mut ctx, &gains, &synth) && first_positive.is_none() {
first_positive = Some(frame);
}
}
assert_eq!(
first_positive,
Some(6),
"the first background-noise verdict should be the seventh frame"
);
for _ in 0..40 {
detector.update(&mut ctx, &gains, &synth);
}
assert_eq!(detector.noise_hangover, NOISE_HANGOVER_MAX);
}
#[test]
fn the_voicing_threshold_tightens_once_the_decoder_settles_into_noise() {
let limits = [LTP_LIMIT_BASE.0, LTP_LIMIT_TIGHT.0, LTP_LIMIT_TIGHTEST.0];
assert!(
limits.windows(2).all(|w| w[0] < w[1]),
"the voicing limits must tighten monotonically, got {limits:?}"
);
let mut ctx = DspContext::default();
let mut detector = SourceDetector::new();
let gains = [Word16(14500); MEDIAN_MAX];
let synth = quiet_frame();
for frame in 1..=13 {
detector.update(&mut ctx, &gains, &synth);
assert_eq!(
detector.voiced_hangover().0,
0,
"frame {frame}: 0.885 clears the 0.85 limit and should read as voiced"
);
}
assert_eq!(
detector.noise_hangover.0, 8,
"hangover should sit at the first threshold"
);
detector.update(&mut ctx, &gains, &synth);
assert_eq!(
detector.noise_hangover.0, 9,
"hangover should have crossed 8"
);
assert_eq!(
detector.voiced_hangover().0,
1,
"past hangover 8 the limit is 0.95, which 0.885 does not clear"
);
}
#[test]
fn the_second_voicing_threshold_is_reachable_too() {
let mut ctx = DspContext::default();
let mut detector = SourceDetector::new();
let gains = [Word16(16000); MEDIAN_MAX];
let synth = quiet_frame();
for frame in 1..=20 {
detector.update(&mut ctx, &gains, &synth);
assert_eq!(
detector.voiced_hangover().0,
0,
"frame {frame}: 0.977 still clears the 0.95 limit"
);
}
assert_eq!(
detector.noise_hangover.0, 15,
"hangover should sit on the second threshold"
);
detector.update(&mut ctx, &gains, &synth);
assert_eq!(detector.noise_hangover.0, 16);
assert_eq!(
detector.voiced_hangover().0,
1,
"past hangover 15 the limit is 1.00, which 0.977 does not clear"
);
}
#[test]
fn the_noise_floor_saturates_once_the_history_fills() {
let frames = vec![square(392); 66];
assert_eq!(
first_noise_frame(&frames, 100),
Some(62),
"the saturating noise floor should open the detector at frame 62"
);
}
#[test]
fn a_quiet_recent_window_latches_before_the_loud_frames_leave_history() {
let mut frames = vec![square(700); 25];
frames.extend(std::iter::repeat_n(square(300), 25));
assert_eq!(
first_noise_frame(&frames, 100),
Some(47),
"the recent-energy window should open the detector at frame 47"
);
}
#[test]
fn deep_in_noise_the_nine_tap_median_can_clear_a_voiced_verdict() {
let mut ctx = DspContext::default();
let mut detector = SourceDetector::new();
let synth = quiet_frame();
let mut gains = [Word16(0); MEDIAN_MAX];
gains[4] = Word16(17000);
gains[5] = Word16(17000);
gains[6] = Word16(17000);
for frame in 1..=25 {
detector.update(&mut ctx, &gains, &synth);
assert_eq!(
detector.voiced_hangover().0,
0,
"frame {frame}: the five-tap median alone should still read voiced"
);
}
assert_eq!(
detector.noise_hangover.0, 20,
"hangover should sit exactly on the threshold"
);
detector.update(&mut ctx, &gains, &synth);
assert_eq!(
detector.noise_hangover.0, 21,
"hangover should have crossed 20"
);
assert_eq!(
detector.voiced_hangover().0,
1,
"the nine-tap median should have overturned the five-tap verdict"
);
}
}