use super::isf_noise;
use super::lp::autocorr::LP_ORDER;
use super::lp::isf_dequant::ISF_INIT;
use super::math::{dot_product12, isqrt_n, pow2};
use crate::fixed_point::arith::{add, extract_h, extract_l, mult, mult_r, sub};
use crate::fixed_point::arith32::{l_add, l_deposit_h, l_mac, l_mult, l_sub};
use crate::fixed_point::div::div_s;
use crate::fixed_point::shift::shr;
use crate::fixed_point::shift::{l_shl, l_shr, norm_l, shl};
use crate::fixed_point::types::{DspContext, Word16, Word32};
const DTX_HIST_SIZE: usize = 8;
const DTX_HANG_CONST: Word16 = Word16(7);
const DTX_ELAPSED_FRAMES_THRESH: Word16 = Word16(24 + 7 - 1);
const DTX_MAX_EMPTY_THRESH: Word16 = Word16(50);
const LOG_EN_INIT: Word16 = Word16(3500);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RxFrameType {
SpeechGood,
SpeechBad,
SpeechLost,
SidFirst,
SidUpdate,
SidBad,
NoData,
}
impl RxFrameType {
const fn is_sid(self) -> bool {
matches!(self, Self::SidFirst | Self::SidUpdate | Self::SidBad)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DtxState {
Speech,
ComfortNoise,
Muted,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct DtxDecoder {
since_last_sid: Word16,
true_sid_period_inv: Word16,
log_en: Word16,
old_log_en: Word16,
isf: [Word16; LP_ORDER],
isf_old: [Word16; LP_ORDER],
cng_seed: Word16,
isf_hist: [Word16; LP_ORDER * DTX_HIST_SIZE],
log_en_hist: [Word16; DTX_HIST_SIZE],
hist_ptr: usize,
dtx_hangover_count: Word16,
dec_ana_elapsed_count: Word16,
sid_frame: bool,
valid_data: bool,
dtx_hangover_added: bool,
global_state: DtxState,
data_updated: bool,
dither_seed: Word16,
dither: bool,
vad_history: Word16,
}
impl Default for DtxDecoder {
fn default() -> Self {
Self::new()
}
}
impl DtxDecoder {
#[must_use]
pub fn new() -> Self {
let mut isf = [Word16(0); LP_ORDER];
for (slot, &value) in isf.iter_mut().zip(&ISF_INIT) {
*slot = Word16(value);
}
let mut isf_hist = [Word16(0); LP_ORDER * DTX_HIST_SIZE];
for frame in isf_hist.chunks_exact_mut(LP_ORDER) {
frame.copy_from_slice(&isf);
}
Self {
since_last_sid: Word16(0),
true_sid_period_inv: Word16(8192),
log_en: LOG_EN_INIT,
old_log_en: LOG_EN_INIT,
isf,
isf_old: isf,
cng_seed: Word16(21845),
isf_hist,
log_en_hist: [Word16(LOG_EN_INIT.0 >> 3); DTX_HIST_SIZE],
hist_ptr: 0,
dtx_hangover_count: DTX_HANG_CONST,
dec_ana_elapsed_count: Word16(32767),
sid_frame: false,
valid_data: false,
dtx_hangover_added: false,
global_state: DtxState::Speech,
data_updated: false,
dither_seed: Word16(21845),
dither: false,
vad_history: Word16(0),
}
}
pub fn receive(&mut self, ctx: &mut DspContext, frame_type: RxFrameType) -> DtxState {
let continuing = matches!(self.global_state, DtxState::ComfortNoise | DtxState::Muted)
&& matches!(
frame_type,
RxFrameType::NoData | RxFrameType::SpeechBad | RxFrameType::SpeechLost
);
let state = if frame_type.is_sid() || continuing {
let stays_muted = self.global_state == DtxState::Muted
&& matches!(
frame_type,
RxFrameType::SidBad
| RxFrameType::SidFirst
| RxFrameType::SpeechLost
| RxFrameType::NoData
);
let mut state = if stays_muted {
DtxState::Muted
} else {
DtxState::ComfortNoise
};
self.since_last_sid = add(ctx, self.since_last_sid, Word16(1));
if frame_type != RxFrameType::SidUpdate
&& sub(ctx, self.since_last_sid, DTX_MAX_EMPTY_THRESH).0 > 0
{
state = DtxState::Muted;
}
state
} else {
self.since_last_sid = Word16(0);
DtxState::Speech
};
if !self.data_updated && frame_type == RxFrameType::SidUpdate {
self.dec_ana_elapsed_count = Word16(0);
}
self.dec_ana_elapsed_count = add(ctx, self.dec_ana_elapsed_count, Word16(1));
self.dtx_hangover_added = false;
let encoder_in_dtx = frame_type.is_sid()
|| (frame_type == RxFrameType::NoData
&& (self.global_state != DtxState::Speech
|| self.vad_history.0 >= DTX_HANG_CONST.0));
if encoder_in_dtx {
if sub(ctx, self.dec_ana_elapsed_count, DTX_ELAPSED_FRAMES_THRESH).0 > 0 {
self.dtx_hangover_added = true;
self.dec_ana_elapsed_count = Word16(0);
self.dtx_hangover_count = Word16(0);
} else if self.dtx_hangover_count.0 == 0 {
self.dec_ana_elapsed_count = Word16(0);
} else {
self.dtx_hangover_count = sub(ctx, self.dtx_hangover_count, Word16(1));
}
} else {
self.dtx_hangover_count = DTX_HANG_CONST;
}
if state != DtxState::Speech {
self.sid_frame = false;
self.valid_data = false;
match frame_type {
RxFrameType::SidFirst => self.sid_frame = true,
RxFrameType::SidUpdate => {
self.sid_frame = true;
self.valid_data = true;
}
RxFrameType::SidBad => {
self.sid_frame = true;
self.dtx_hangover_added = false;
}
_ => {}
}
}
state
}
pub fn observe_speech(
&mut self,
ctx: &mut DspContext,
isf: &[Word16; LP_ORDER],
excitation: &[Word16],
) {
self.hist_ptr = (self.hist_ptr + 1) % DTX_HIST_SIZE;
let base = self.hist_ptr * LP_ORDER;
self.isf_hist[base..base + LP_ORDER].copy_from_slice(isf);
let mut energy = Word32(0);
for &sample in excitation {
energy = l_mac(ctx, energy, sample, sample);
}
let energy = l_shr(ctx, energy, 1);
let (exponent, mantissa) = super::math::log2(ctx, energy);
let mut log_en = shl(ctx, Word16(exponent), 7);
let fraction = shr(ctx, mantissa, 8);
log_en = add(ctx, log_en, fraction);
self.log_en_hist[self.hist_ptr] = sub(ctx, log_en, Word16(1024));
}
pub fn observe_vad(&mut self, ctx: &mut DspContext, voice_active: bool) {
self.vad_history = if voice_active {
Word16(0)
} else {
add(ctx, self.vad_history, Word16(1))
};
}
pub fn comfort_noise(
&mut self,
ctx: &mut DspContext,
state: DtxState,
sid: Option<SidFields>,
) -> ([Word16; LP_ORDER], [Word16; 256]) {
if self.dtx_hangover_added && self.sid_frame {
self.analyse_backwards(ctx);
}
if self.sid_frame {
self.isf_old = self.isf;
self.old_log_en = self.log_en;
if let Some(fields) = sid.filter(|_| self.valid_data) {
let mut period = self.since_last_sid;
if period.0 > 32 {
period = Word16(32);
}
self.true_sid_period_inv = if period.0 >= 2 {
div_s(Word16(1 << 10), shl(ctx, period, 10))
} else {
Word16(1 << 14)
};
self.isf = isf_noise::dequantise(ctx, &fields.isf_indices);
let scaled = shl(ctx, fields.energy_index, 9);
self.log_en = mult(ctx, scaled, Word16(12483));
self.dither = fields.dither;
if !self.data_updated || self.global_state == DtxState::Speech {
self.isf_old = self.isf;
self.old_log_en = self.log_en;
}
}
}
if self.sid_frame && self.valid_data {
self.since_last_sid = Word16(0);
}
let mut factor = shl(ctx, self.since_last_sid, 10);
factor = mult(ctx, factor, self.true_sid_period_inv);
if factor.0 > 1024 {
factor = Word16(1024);
}
factor = shl(ctx, factor, 4);
let mut energy = l_mult(ctx, factor, self.log_en);
let mut isf = [Word16(0); LP_ORDER];
for (slot, &target) in isf.iter_mut().zip(&self.isf) {
*slot = mult(ctx, factor, target);
}
let complement = sub(ctx, Word16(16384), factor);
energy = l_mac(ctx, energy, complement, self.old_log_en);
for (slot, &previous) in isf.iter_mut().zip(&self.isf_old) {
let blended = mult(ctx, complement, previous);
let summed = add(ctx, *slot, blended);
*slot = shl(ctx, summed, 1);
}
if self.dither {
self.apply_dithering(ctx, &mut isf, &mut energy);
}
if state == DtxState::Muted {
let mut period = self.since_last_sid;
if period.0 > 32 {
period = Word16(32);
}
if period.0 <= 0 {
period = Word16(8);
}
self.true_sid_period_inv = div_s(Word16(1 << 10), shl(ctx, period, 10));
self.since_last_sid = Word16(0);
self.old_log_en = self.log_en;
self.log_en = sub(ctx, self.log_en, Word16(64));
}
let excitation = self.excitation(ctx, energy);
if self.sid_frame && (self.valid_data || self.dtx_hangover_added) {
self.since_last_sid = Word16(0);
self.data_updated = true;
}
(isf, excitation)
}
#[allow(clippy::missing_const_for_fn)]
pub fn commit(&mut self, state: DtxState) {
self.global_state = state;
}
fn analyse_backwards(&mut self, ctx: &mut DspContext) {
let newest = self.hist_ptr;
let duplicate = (newest + 1) % DTX_HIST_SIZE;
let (from, to) = (newest * LP_ORDER, duplicate * LP_ORDER);
let copy: [Word16; LP_ORDER] = self.isf_hist[from..from + LP_ORDER]
.try_into()
.expect("one spectrum");
self.isf_hist[to..to + LP_ORDER].copy_from_slice(©);
self.log_en_hist[duplicate] = self.log_en_hist[newest];
let mut log_en = Word16(0);
for &value in &self.log_en_hist {
log_en = add(ctx, log_en, value);
}
let mut sums = [Word32(0); LP_ORDER];
for frame in self.isf_hist.chunks_exact(LP_ORDER) {
for (acc, &value) in sums.iter_mut().zip(frame) {
*acc = l_add(ctx, *acc, Word32(i32::from(value.0)));
}
}
log_en = shr(ctx, log_en, 1);
log_en = add(ctx, log_en, Word16(1024));
if log_en.0 < 0 {
log_en = Word16(0);
}
self.log_en = log_en;
for (slot, &sum) in self.isf.iter_mut().zip(&sums) {
*slot = extract_l(l_shr(ctx, sum, 3));
}
}
fn excitation(&mut self, ctx: &mut DspContext, energy: Word32) -> [Word16; 256] {
let energy = l_shr(ctx, energy, 9);
let exponent = extract_h(energy);
let remainder = l_sub(ctx, energy, l_deposit_h(exponent));
let mantissa = extract_l(l_shr(ctx, remainder, 1));
let exponent = add(ctx, exponent, Word16(15));
let level32 = pow2(ctx, exponent.0, mantissa);
let shift = norm_l(level32);
let level = extract_h(l_shl(ctx, level32, shift));
let level_exp = sub(ctx, Word16(15), Word16(shift));
let mut excitation = [Word16(0); 256];
for slot in &mut excitation {
let sample = self.next_noise(ctx);
*slot = shr(ctx, sample, 4);
}
let measured = dot_product12(ctx, &excitation, &excitation);
let (energy, energy_exp) = isqrt_n(ctx, measured);
let gain = mult(ctx, level, extract_h(energy));
let combined = add(ctx, level_exp, Word16(energy_exp));
let total = add(ctx, combined, Word16(4));
for slot in &mut excitation {
let scaled = mult(ctx, *slot, gain);
*slot = shl(ctx, scaled, total.0);
}
excitation
}
fn next_noise(&mut self, ctx: &mut DspContext) -> Word16 {
let product = l_mult(ctx, self.cng_seed, Word16(31821));
let halved = l_shr(ctx, product, 1);
self.cng_seed = extract_l(l_add(ctx, halved, Word32(13849)));
self.cng_seed
}
fn next_dither(&mut self, ctx: &mut DspContext) -> Word16 {
let product = l_mult(ctx, self.dither_seed, Word16(31821));
let halved = l_shr(ctx, product, 1);
self.dither_seed = extract_l(l_add(ctx, halved, Word32(13849)));
self.dither_seed
}
fn apply_dithering(
&mut self,
ctx: &mut DspContext,
isf: &mut [Word16; LP_ORDER],
energy: &mut Word32,
) {
const GAIN_FACTOR: Word16 = Word16(75);
const ISF_FACTOR_LOW: Word16 = Word16(256);
const ISF_FACTOR_STEP: Word16 = Word16(2);
const ISF_DITH_GAP: Word16 = Word16(448);
const ISF_GAP: Word16 = Word16(128);
let triangular = |dtx: &mut Self, ctx: &mut DspContext| -> Word16 {
let first = dtx.next_dither(ctx);
let half_first = shr(ctx, first, 1);
let second = dtx.next_dither(ctx);
let half_second = shr(ctx, second, 1);
add(ctx, half_first, half_second)
};
let perturbation = triangular(self, ctx);
let widened = l_mult(ctx, perturbation, GAIN_FACTOR);
*energy = l_add(ctx, *energy, widened);
if energy.0 < 0 {
*energy = Word32(0);
}
let mut factor = ISF_FACTOR_LOW;
let perturbation = triangular(self, ctx);
let step = mult_r(ctx, perturbation, factor);
let candidate = add(ctx, isf[0], step);
isf[0] = if sub(ctx, candidate, ISF_GAP).0 < 0 {
ISF_GAP
} else {
candidate
};
for i in 1..LP_ORDER - 1 {
factor = add(ctx, factor, ISF_FACTOR_STEP);
let perturbation = triangular(self, ctx);
let step = mult_r(ctx, perturbation, factor);
let candidate = add(ctx, isf[i], step);
let spacing = sub(ctx, candidate, isf[i - 1]);
isf[i] = if sub(ctx, spacing, ISF_DITH_GAP).0 < 0 {
add(ctx, isf[i - 1], ISF_DITH_GAP)
} else {
candidate
};
}
if isf[LP_ORDER - 2].0 > 16384 {
isf[LP_ORDER - 2] = Word16(16384);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SidFields {
pub isf_indices: [u16; 5],
pub energy_index: Word16,
pub dither: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_handler_runs_on_speech_and_keeps_the_staleness_counter_moving() {
let mut ctx = DspContext::default();
let mut dtx = DtxDecoder::new();
for _ in 0..30 {
assert_eq!(
dtx.receive(&mut ctx, RxFrameType::SpeechGood),
DtxState::Speech
);
dtx.commit(DtxState::Speech);
}
assert_eq!(
dtx.dec_ana_elapsed_count,
Word16(32767),
"the counter must saturate rather than wrap"
);
assert_eq!(
dtx.receive(&mut ctx, RxFrameType::SidFirst),
DtxState::ComfortNoise
);
assert!(
dtx.dtx_hangover_added,
"a SID_FIRST after a talk spurt must permit the backward analysis"
);
}
#[test]
fn a_long_gap_without_an_update_fades_to_mute() {
let mut ctx = DspContext::default();
let mut dtx = DtxDecoder::new();
dtx.receive(&mut ctx, RxFrameType::SidFirst);
dtx.commit(DtxState::ComfortNoise);
let mut muted_at = None;
for frame in 0..70 {
let state = dtx.receive(&mut ctx, RxFrameType::NoData);
dtx.commit(state);
if state == DtxState::Muted && muted_at.is_none() {
muted_at = Some(frame);
}
}
assert_eq!(
muted_at,
Some(49),
"DTX_MAX_EMPTY_THRESH is 50 frames without an update"
);
}
#[test]
fn damaged_speech_leaves_the_mute_but_a_gap_does_not() {
let mut ctx = DspContext::default();
let mut dtx = DtxDecoder::new();
dtx.receive(&mut ctx, RxFrameType::SidFirst);
dtx.commit(DtxState::Muted);
assert_eq!(
dtx.receive(&mut ctx, RxFrameType::SpeechBad),
DtxState::ComfortNoise,
"SPEECH_BAD is absent from the stay-muted list"
);
dtx.commit(DtxState::Muted);
assert_eq!(dtx.receive(&mut ctx, RxFrameType::NoData), DtxState::Muted);
}
#[test]
fn an_update_renews_the_parameters_and_a_first_does_not() {
let mut ctx = DspContext::default();
let mut dtx = DtxDecoder::new();
dtx.receive(&mut ctx, RxFrameType::SidFirst);
assert!(dtx.sid_frame && !dtx.valid_data);
dtx.commit(DtxState::ComfortNoise);
dtx.receive(&mut ctx, RxFrameType::SidUpdate);
assert!(dtx.sid_frame && dtx.valid_data);
}
#[test]
fn the_state_machine_and_comfort_noise_match_the_reference() {
let text = include_str!("../testdata/wb_dtx_dec_vectors.txt");
let mut ctx = DspContext::default();
let mut dtx = DtxDecoder::new();
let mut compared = 0usize;
let mut rows = 0usize;
let mut states = std::collections::HashSet::new();
for line in text
.lines()
.filter(|l| !l.starts_with('#') && !l.trim().is_empty())
{
let fields: Vec<&str> = line.split('|').collect();
assert_eq!(fields.len(), 4, "malformed row `{line}`");
let head: Vec<i32> = fields[0]
.split_whitespace()
.map(|v| v.parse().expect("number"))
.collect();
let frame = usize::try_from(head[0]).expect("frame index");
let raw_type = head[1];
let want_state = match fields[1].trim() {
"0" => DtxState::Speech,
"1" => DtxState::ComfortNoise,
"2" => DtxState::Muted,
other => panic!("unknown state {other}"),
};
assert_eq!(frame, rows, "the fixture skipped a frame");
let frame_type = match raw_type {
0 => RxFrameType::SpeechGood,
2 => RxFrameType::SpeechLost,
3 => RxFrameType::SpeechBad,
4 => RxFrameType::SidFirst,
5 => RxFrameType::SidUpdate,
6 => RxFrameType::SidBad,
7 => RxFrameType::NoData,
other => panic!("unhandled RX type {other}"),
};
let state = dtx.receive(&mut ctx, frame_type);
assert_eq!(state, want_state, "frame {frame} state");
states.insert(state);
compared += 1;
if state == DtxState::Speech {
let mut isf = [Word16(0); LP_ORDER];
for (i, slot) in isf.iter_mut().enumerate() {
let i = i16::try_from(i).expect("order is 16");
let f = i16::try_from(frame).expect("short fixture");
*slot = Word16(500 + i * 900 + f * 7);
}
let f = i32::try_from(frame).expect("short fixture");
let excitation: Vec<Word16> = (0..256)
.map(|i: i32| {
let value = ((i * 37 + f * 11) % 401) - 200;
Word16(i16::try_from(value).expect("in range"))
})
.collect();
dtx.observe_speech(&mut ctx, &isf, &excitation);
} else {
let fields_in = SidFields {
isf_indices: [10, 20, 30, 5, 7],
energy_index: Word16(30),
dither: false,
};
let (isf, excitation) = dtx.comfort_noise(&mut ctx, state, Some(fields_in));
let want_isf: Vec<i16> = fields[2]
.split_whitespace()
.map(|v| v.parse().expect("isf"))
.collect();
let want_exc: Vec<i16> = fields[3]
.split_whitespace()
.map(|v| v.parse().expect("sample"))
.collect();
for (i, (&got, &want)) in isf.iter().zip(&want_isf).enumerate() {
assert_eq!(got.0, want, "frame {frame} ISF {i}");
compared += 1;
}
for (i, (&got, &want)) in excitation.iter().zip(&want_exc).enumerate() {
assert_eq!(got.0, want, "frame {frame} excitation sample {i}");
compared += 1;
}
}
dtx.commit(state);
rows += 1;
}
assert_eq!(rows, 89, "the fixture lost rows");
assert!(compared > 700, "only {compared} values compared");
assert_eq!(states.len(), 3, "the sequence did not visit every state");
}
#[test]
fn comfort_noise_is_deterministic_and_not_silent() {
let mut ctx = DspContext::default();
let mut dtx = DtxDecoder::new();
dtx.receive(&mut ctx, RxFrameType::SidUpdate);
let fields = SidFields {
isf_indices: [10, 20, 30, 5, 7],
energy_index: Word16(30),
dither: false,
};
let (isf, excitation) = dtx.comfort_noise(&mut ctx, DtxState::ComfortNoise, Some(fields));
assert!(excitation.iter().any(|s| s.0 != 0), "the noise is silent");
assert!(isf.iter().any(|s| s.0 != 0), "the spectrum is empty");
let mut again = DtxDecoder::new();
again.receive(&mut ctx, RxFrameType::SidUpdate);
let (isf2, exc2) = again.comfort_noise(&mut ctx, DtxState::ComfortNoise, Some(fields));
assert_eq!(isf, isf2);
assert_eq!(excitation, exc2, "the generator must be deterministic");
}
}