use crate::fixed_point::arith::{add, extract_l, mult, sub};
use crate::fixed_point::arith32::{l_add, l_deposit_l, l_mac};
use crate::fixed_point::shift::{l_shr, shl, shr};
use crate::fixed_point::types::{DspContext, Word16, Word32};
pub const DTX_HIST_SIZE: usize = 8;
const M: usize = 10;
const L_FRAME: usize = 160;
const DTX_HANG_CONST: Word16 = Word16(7);
const DTX_ELAPSED_FRAMES_THRESH: Word16 = Word16(24 + 7 - 1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TxDecision {
Speech,
ComfortNoise,
}
#[derive(Debug, Clone)]
pub struct DtxEncoder {
lsp_hist: [Word16; M * DTX_HIST_SIZE],
log_en_hist: [Word16; DTX_HIST_SIZE],
hist_ptr: usize,
log_en_index: Word16,
init_lsf_vq_index: Word16,
lsp_index: [Word16; 3],
dtx_hangover_count: Word16,
dec_ana_elapsed_count: Word16,
}
impl Default for DtxEncoder {
fn default() -> Self {
Self::new()
}
}
impl DtxEncoder {
#[must_use]
pub const fn new() -> Self {
Self {
lsp_hist: [Word16(0); M * DTX_HIST_SIZE],
log_en_hist: [Word16(0); DTX_HIST_SIZE],
hist_ptr: 0,
log_en_index: Word16(0),
init_lsf_vq_index: Word16(0),
lsp_index: [Word16(0); 3],
dtx_hangover_count: DTX_HANG_CONST,
dec_ana_elapsed_count: Word16(32767),
}
}
pub fn classify(&mut self, ctx: &mut DspContext, voice_active: bool) -> (TxDecision, bool) {
self.dec_ana_elapsed_count = add(ctx, self.dec_ana_elapsed_count, Word16(1));
if voice_active {
self.dtx_hangover_count = DTX_HANG_CONST;
return (TxDecision::Speech, false);
}
if self.dtx_hangover_count.0 == 0 {
self.dec_ana_elapsed_count = Word16(0);
return (TxDecision::ComfortNoise, true);
}
self.dtx_hangover_count = sub(ctx, self.dtx_hangover_count, Word16(1));
let staleness = add(ctx, self.dec_ana_elapsed_count, self.dtx_hangover_count);
if sub(ctx, staleness, DTX_ELAPSED_FRAMES_THRESH).0 < 0 {
(TxDecision::ComfortNoise, false)
} else {
(TxDecision::Speech, false)
}
}
pub fn buffer(&mut self, ctx: &mut DspContext, lsp: &[Word16; M], speech: &[Word16]) {
self.hist_ptr = (self.hist_ptr + 1) % DTX_HIST_SIZE;
let base = self.hist_ptr * M;
self.lsp_hist[base..base + M].copy_from_slice(lsp);
let mut energy = Word32(0);
for &sample in speech.iter().take(L_FRAME) {
energy = l_mac(ctx, energy, sample, sample);
}
let (exponent, mantissa) = super::super::math::log2(ctx, energy);
let mut log_en = shl(ctx, exponent, 10);
let fraction = shr(ctx, mantissa, 5);
log_en = add(ctx, log_en, fraction);
log_en = sub(ctx, log_en, Word16(8521));
self.log_en_hist[self.hist_ptr] = shr(ctx, log_en, 1);
}
pub fn average_history(&mut self, ctx: &mut DspContext) -> ([Word16; M], Word16) {
let mut log_en = Word16(0);
let mut sums = [Word32(0); M];
for frame in 0..DTX_HIST_SIZE {
let quarter = shr(ctx, self.log_en_hist[frame], 2);
log_en = add(ctx, log_en, quarter);
for (acc, &value) in sums.iter_mut().zip(&self.lsp_hist[frame * M..]) {
*acc = l_add(ctx, *acc, l_deposit_l(value));
}
}
log_en = shr(ctx, log_en, 1);
let mut lsp = [Word16(0); M];
for (slot, &sum) in lsp.iter_mut().zip(&sums) {
*slot = extract_l(l_shr(ctx, sum, 3));
}
let mut index = add(ctx, log_en, Word16(2560));
index = add(ctx, index, Word16(128));
index = shr(ctx, index, 8);
if index.0 > 63 {
index = Word16(63);
}
if index.0 < 0 {
index = Word16(0);
}
self.log_en_index = index;
(lsp, index)
}
#[must_use]
pub fn predictor_reset(&self, ctx: &mut DspContext) -> (Word16, Word16) {
let mut log_en = shl(ctx, self.log_en_index, -2 + 10);
log_en = sub(ctx, log_en, Word16(2560));
log_en = sub(ctx, log_en, Word16(9000));
if log_en.0 > 0 {
log_en = Word16(0);
}
if log_en.0 < -14436 {
log_en = Word16(-14436);
}
(log_en, mult(ctx, Word16(5443), log_en))
}
pub const fn set_indices(&mut self, init_index: Word16, lsp_index: [Word16; 3]) {
self.init_lsf_vq_index = init_index;
self.lsp_index = lsp_index;
}
#[must_use]
pub const fn sid_parameters(&self) -> [Word16; 5] {
[
self.init_lsf_vq_index,
self.lsp_index[0],
self.lsp_index[1],
self.lsp_index[2],
self.log_en_index,
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_hangover_runs_seven_frames_and_then_yields() {
let mut ctx = DspContext::default();
let mut dtx = DtxEncoder::new();
for _ in 0..3 {
assert_eq!(dtx.classify(&mut ctx, true).0, TxDecision::Speech);
}
let decisions: Vec<TxDecision> = (0..10).map(|_| dtx.classify(&mut ctx, false).0).collect();
let first_cn = decisions
.iter()
.position(|&d| d == TxDecision::ComfortNoise);
assert_eq!(first_cn, Some(7), "the hangover is seven frames");
}
#[test]
fn only_the_frame_out_of_hangover_may_recompute_the_description() {
let mut ctx = DspContext::default();
let mut dtx = DtxEncoder::new();
for _ in 0..3 {
dtx.classify(&mut ctx, true);
}
let mut recomputes = 0;
for _ in 0..10 {
let (decision, may_recompute) = dtx.classify(&mut ctx, false);
if may_recompute {
assert_eq!(decision, TxDecision::ComfortNoise);
recomputes += 1;
}
}
assert_eq!(
recomputes, 3,
"recomputation should be rare, not every frame"
);
}
#[test]
fn the_ring_starts_at_slot_one_and_wraps_at_eight() {
let mut ctx = DspContext::default();
let mut dtx = DtxEncoder::new();
let lsp = [Word16(1000); M];
let speech = [Word16(100); L_FRAME];
assert_eq!(dtx.hist_ptr, 0);
dtx.buffer(&mut ctx, &lsp, &speech);
assert_eq!(dtx.hist_ptr, 1);
for _ in 0..7 {
dtx.buffer(&mut ctx, &lsp, &speech);
}
assert_eq!(dtx.hist_ptr, 0);
}
#[test]
fn the_predictor_reset_is_clamped_at_both_ends() {
let mut ctx = DspContext::default();
let mut dtx = DtxEncoder::new();
dtx.log_en_index = Word16(63);
let (plain, scaled) = dtx.predictor_reset(&mut ctx);
assert_eq!(plain.0, 0, "a loud SID must not raise the predictor");
assert_eq!(scaled.0, 0);
dtx.log_en_index = Word16(0);
let (plain, scaled) = dtx.predictor_reset(&mut ctx);
assert_eq!(plain.0, -11560);
assert!(
scaled.0 > plain.0 && scaled.0 < 0,
"12.2 got {} from {}",
scaled.0,
plain.0
);
}
}