use super::codebook::L_SUBFR;
use super::gain::FrameQuality;
use super::highband::NoiseGenerator;
use crate::fixed_point::arith::{add, mult, sub};
use crate::fixed_point::shift::shr;
use crate::fixed_point::types::{DspContext, Word16};
const HISTORY: usize = 5;
const ONE_PER_3: Word16 = Word16(10923);
const ONE_PER_HISTORY: Word16 = Word16(6554);
const CONFIDENT_GAIN: Word16 = Word16(8192);
const WEAK_GAIN: Word16 = Word16(6554);
const STEADY_SPREAD: Word16 = Word16(10);
const LOOSE_SPREAD: Word16 = Word16(70);
const MAX_JITTER: Word16 = Word16(40);
const RESET_LAG: Word16 = Word16(64);
const MAX_SEVERITY: Word16 = Word16(6);
#[derive(Debug, Clone)]
pub struct Erasure {
severity: Word16,
previous_bad: bool,
current_bad: bool,
lags: LagHistory,
innovation: NoiseGenerator,
}
impl Default for Erasure {
fn default() -> Self {
Self::new()
}
}
impl Erasure {
#[must_use]
pub const fn new() -> Self {
Self {
severity: Word16(0),
previous_bad: false,
current_bad: false,
lags: LagHistory::new(),
innovation: NoiseGenerator::new(),
}
}
pub fn begin_frame(&mut self, quality: FrameQuality) {
let mut ctx = DspContext::default();
self.previous_bad = self.current_bad;
self.current_bad = quality != FrameQuality::Good;
if self.current_bad {
self.severity = add(&mut ctx, self.severity, Word16(1));
if self.severity.0 > MAX_SEVERITY.0 {
self.severity = MAX_SEVERITY;
}
} else {
self.severity = shr(&mut ctx, self.severity, 1);
}
}
#[must_use]
pub fn severity(&self) -> usize {
usize::from(self.severity.0.unsigned_abs())
}
#[must_use]
pub const fn previous_frame_was_bad(&self) -> bool {
self.previous_bad
}
pub fn pitch_lag(
&mut self,
ctx: &mut DspContext,
lag: u16,
frac: u8,
quality: FrameQuality,
) -> (u16, u8) {
if quality == FrameQuality::Good {
return (lag, frac);
}
let received = Word16(i16::try_from(lag).unwrap_or(RESET_LAG.0));
let chosen = self
.lags
.substitute(ctx, received, quality == FrameQuality::Unusable);
(chosen.0.unsigned_abs(), 0)
}
pub fn note_good_subframe(&mut self, lag: u16, pitch_gain: Word16) {
self.lags.push(
Word16(i16::try_from(lag).unwrap_or(RESET_LAG.0)),
pitch_gain,
);
}
#[must_use]
pub fn lost_innovation(&mut self, ctx: &mut DspContext) -> [Word16; L_SUBFR] {
let mut code = [Word16(0); L_SUBFR];
for slot in &mut code {
let sample = self.innovation.next(ctx);
*slot = shr(ctx, sample, 3);
}
code
}
}
#[derive(Debug, Clone)]
struct LagHistory {
lags: [Word16; HISTORY],
gains: [Word16; HISTORY],
last_good: Word16,
jitter: NoiseGenerator,
}
impl LagHistory {
const fn new() -> Self {
Self {
lags: [RESET_LAG; HISTORY],
gains: [Word16(0); HISTORY],
last_good: RESET_LAG,
jitter: NoiseGenerator::new(),
}
}
fn push(&mut self, lag: Word16, pitch_gain: Word16) {
self.lags.rotate_right(1);
self.lags[0] = lag;
self.gains.rotate_left(1);
self.gains[HISTORY - 1] = pitch_gain;
self.last_good = lag;
}
fn substitute(&mut self, ctx: &mut DspContext, received: Word16, lost: bool) -> Word16 {
let newest_lag = self.lags[0];
let newest_gain = self.gains[HISTORY - 1];
let previous_gain = self.gains[HISTORY - 2];
let min_lag = self.lags.iter().map(|w| w.0).min().unwrap_or(RESET_LAG.0);
let max_lag = self.lags.iter().map(|w| w.0).max().unwrap_or(RESET_LAG.0);
let weakest_gain = self.gains.iter().map(|w| w.0).min().unwrap_or(0);
let spread = sub(ctx, Word16(max_lag), Word16(min_lag));
let steady_and_voiced = weakest_gain > CONFIDENT_GAIN.0 && spread.0 < STEADY_SPREAD.0;
let recently_voiced =
newest_gain.0 > CONFIDENT_GAIN.0 && previous_gain.0 > CONFIDENT_GAIN.0;
if lost {
let chosen = if steady_and_voiced {
self.last_good
} else if recently_voiced {
newest_lag
} else {
self.weighted_guess(ctx)
};
return clamp(chosen, min_lag, max_lag);
}
let above_floor = received.0 > sub(ctx, Word16(min_lag), Word16(5)).0;
let near_max = sub(ctx, received, Word16(max_lag)).0 < 5;
let inside = received.0 > min_lag && received.0 < max_lag;
let near_newest = {
let gap = sub(ctx, received, newest_lag).0;
gap > -10 && gap < 10
};
let mean_lag = {
let mut sum = Word16(0);
for lag in &self.lags {
sum = add(ctx, sum, *lag);
}
mult(ctx, sum, ONE_PER_HISTORY)
};
let plausible = (spread.0 < STEADY_SPREAD.0 && above_floor && near_max)
|| (recently_voiced && near_newest)
|| (weakest_gain < WEAK_GAIN.0 && newest_gain.0 == weakest_gain && inside)
|| (spread.0 < LOOSE_SPREAD.0 && inside)
|| (received.0 > mean_lag.0 && received.0 < max_lag);
if plausible {
return received;
}
let chosen = if steady_and_voiced || recently_voiced {
newest_lag
} else {
self.weighted_guess(ctx)
};
clamp(chosen, min_lag, max_lag)
}
fn weighted_guess(&mut self, ctx: &mut DspContext) -> Word16 {
let mut sorted = self.lags.map(|w| w.0);
sorted.sort_unstable();
let mut gap = sub(ctx, Word16(sorted[4]), Word16(sorted[2]));
if gap.0 > MAX_JITTER.0 {
gap = MAX_JITTER;
}
let half = shr(ctx, gap, 1);
let draw = self.jitter.next(ctx);
let jitter = mult(ctx, half, draw);
let pair = add(ctx, Word16(sorted[2]), Word16(sorted[3]));
let upper = add(ctx, pair, Word16(sorted[4]));
let mean = mult(ctx, upper, ONE_PER_3);
add(ctx, mean, jitter)
}
}
const fn clamp(lag: Word16, min_lag: i16, max_lag: i16) -> Word16 {
if lag.0 > max_lag {
Word16(max_lag)
} else if lag.0 < min_lag {
Word16(min_lag)
} else {
lag
}
}
#[cfg(test)]
mod tests {
use super::*;
fn warm(erasure: &mut Erasure, lag: u16, gain: i16, count: usize) {
for _ in 0..count {
erasure.begin_frame(FrameQuality::Good);
erasure.note_good_subframe(lag, Word16(gain));
}
}
#[test]
fn severity_halves_on_a_good_frame_rather_than_clearing() {
let mut e = Erasure::new();
for _ in 0..4 {
e.begin_frame(FrameQuality::Bad);
}
assert_eq!(e.severity(), 4);
e.begin_frame(FrameQuality::Good);
assert_eq!(e.severity(), 2, "a good frame must halve, not clear");
e.begin_frame(FrameQuality::Good);
assert_eq!(e.severity(), 1);
e.begin_frame(FrameQuality::Good);
assert_eq!(e.severity(), 0);
}
#[test]
fn severity_saturates_at_six() {
let mut e = Erasure::new();
for _ in 0..20 {
e.begin_frame(FrameQuality::Bad);
}
assert_eq!(
e.severity(),
6,
"the attenuation tables only have seven rows"
);
}
#[test]
fn prev_bfi_covers_a_whole_frame_not_one_subframe() {
let mut e = Erasure::new();
e.begin_frame(FrameQuality::Good);
assert!(!e.previous_frame_was_bad());
e.begin_frame(FrameQuality::Bad);
assert!(
!e.previous_frame_was_bad(),
"the flag is about the frame before"
);
e.begin_frame(FrameQuality::Good);
assert!(e.previous_frame_was_bad());
e.begin_frame(FrameQuality::Good);
assert!(!e.previous_frame_was_bad());
}
#[test]
fn a_steady_voiced_history_holds_its_lag_through_a_loss() {
let mut ctx = DspContext::default();
let mut e = Erasure::new();
warm(&mut e, 80, 12000, 6);
e.begin_frame(FrameQuality::Unusable);
let (lag, frac) = e.pitch_lag(&mut ctx, 200, 3, FrameQuality::Unusable);
assert_eq!(lag, 80);
assert_eq!(
frac, 0,
"a guessed lag does not get a quarter-sample fraction"
);
}
#[test]
fn a_damaged_frames_lag_is_kept_when_the_history_agrees_with_it() {
let mut ctx = DspContext::default();
let mut e = Erasure::new();
for lag in [78u16, 79, 80, 81, 82] {
e.begin_frame(FrameQuality::Good);
e.note_good_subframe(lag, Word16(11000));
}
e.begin_frame(FrameQuality::Bad);
let (lag, _) = e.pitch_lag(&mut ctx, 81, 2, FrameQuality::Bad);
assert_eq!(lag, 81, "a lag inside the history's own range was rejected");
}
#[test]
fn a_damaged_frames_lag_is_rejected_when_it_is_nowhere_near_the_history() {
let mut ctx = DspContext::default();
let mut e = Erasure::new();
for lag in [40u16, 41, 42, 41, 40] {
e.begin_frame(FrameQuality::Good);
e.note_good_subframe(lag, Word16(2000));
}
e.begin_frame(FrameQuality::Bad);
let (lag, _) = e.pitch_lag(&mut ctx, 220, 1, FrameQuality::Bad);
assert!(
(40..=42).contains(&lag),
"a lag of 220 against a history of 40..42 survived as {lag}"
);
}
#[test]
fn a_substituted_lag_never_leaves_the_range_the_talker_used() {
let mut ctx = DspContext::default();
let mut e = Erasure::new();
for lag in [50u16, 90, 60, 120, 70] {
e.begin_frame(FrameQuality::Good);
e.note_good_subframe(lag, Word16(1000));
}
for _ in 0..30 {
e.begin_frame(FrameQuality::Unusable);
let (lag, _) = e.pitch_lag(&mut ctx, 0, 0, FrameQuality::Unusable);
assert!(
(50..=120).contains(&lag),
"substituted lag {lag} is outside the history's range"
);
}
}
#[test]
fn the_lag_jitter_and_the_innovation_come_from_different_generators() {
let mut ctx = DspContext::default();
let mut with_innovation = Erasure::new();
warm(&mut with_innovation, 45, 1000, 5);
let mut without = with_innovation.clone();
with_innovation.begin_frame(FrameQuality::Unusable);
let _ = with_innovation.lost_innovation(&mut ctx);
let a = with_innovation.pitch_lag(&mut ctx, 0, 0, FrameQuality::Unusable);
without.begin_frame(FrameQuality::Unusable);
let b = without.pitch_lag(&mut ctx, 0, 0, FrameQuality::Unusable);
assert_eq!(a, b, "drawing an innovation moved the lag generator");
}
#[test]
fn a_concealed_lag_never_enters_the_history() {
let mut ctx = DspContext::default();
let mut e = Erasure::new();
warm(&mut e, 100, 13000, 5);
for _ in 0..5 {
e.begin_frame(FrameQuality::Unusable);
let _ = e.pitch_lag(&mut ctx, 0, 0, FrameQuality::Unusable);
}
e.begin_frame(FrameQuality::Unusable);
let (lag, _) = e.pitch_lag(&mut ctx, 0, 0, FrameQuality::Unusable);
assert_eq!(
lag, 100,
"the history drifted, so concealed lags fed back into it"
);
}
#[test]
fn a_substituted_innovation_stays_within_the_codebooks_headroom() {
let mut ctx = DspContext::default();
let mut e = Erasure::new();
let code = e.lost_innovation(&mut ctx);
assert_eq!(code.len(), L_SUBFR);
assert!(
code.iter().all(|c| c.0.abs() <= 4096),
"the substituted innovation exceeds the codebook's Q9 headroom"
);
let distinct = code
.iter()
.map(|c| c.0)
.collect::<std::collections::BTreeSet<_>>();
assert!(
distinct.len() > 32,
"only {} distinct samples",
distinct.len()
);
}
}