rvoip-codec-core 0.3.8

G.711 and optional G.729/Opus/AMR audio codec implementations for RVOIP
Documentation
//! The comfort-noise ISF quantiser, TS 26.173 `qisf_ns.c`.
//!
//! A SID frame carries 35 bits and spends 28 of them on the spectrum, against
//! the speech quantiser's 46. Two things follow, and both matter more than the
//! bit count:
//!
//! **It is memoryless.** The speech quantiser carries a moving-average
//! predictor across frames (`IsfQuantizer::past`); this one subtracts a
//! constant mean and quantises the remainder. Comfort noise has to decode
//! correctly after an arbitrary gap — the receiver may have heard nothing for
//! seconds — so there is nothing to predict from. A port that reused the
//! speech quantiser's predictor would produce a spectrum that drifts with how
//! much speech preceded the silence.
//!
//! **The mean vector is a different vector.** `MEAN_ISF_NOISE` and the speech
//! quantiser's `MEAN_ISF` are both sixteen `i16` in Q15 and differ in every
//! coefficient. Passing one where the other belongs compiles, and yields a
//! plausible noise spectrum that is wrong everywhere; the generator asserts
//! they share no coefficient for exactly that reason.
//!
//! The search itself is the same shape as the speech refinement stages —
//! exhaustive, ascending, ties to the lower index — so it reuses
//! [`nearest_entry`] rather than restating it.

use super::enc::isf_quant::{enforce_min_spacing, nearest_entry, ISF_GAP};
use super::isf_noise_tables::{DICO1_NS, DICO2_NS, DICO3_NS, DICO4_NS, DICO5_NS, MEAN_ISF_NOISE};
use super::lp::autocorr::LP_ORDER;
use crate::fixed_point::arith::{add, sub};
use crate::fixed_point::types::{DspContext, Word16};

/// The five splits: where each starts, how many ISFs it covers, and its book.
///
/// 6/6/6/5/5 bits, 28 in all. The widths are implied by the book lengths and
/// are not stored separately — a split whose index did not fit its field would
/// be a table error, and [`SPLITS`]'s own test asserts every book is exactly
/// the size its bit width allows.
const SPLITS: [(usize, usize, &[i16]); 5] = [
    (0, 2, &DICO1_NS),
    (2, 3, &DICO2_NS),
    (5, 3, &DICO3_NS),
    (8, 4, &DICO4_NS),
    (12, 4, &DICO5_NS),
];

/// The bit width of each split's index, in transmission order.
pub const SPLIT_BITS: [u8; 5] = [6, 6, 6, 5, 5];

/// Quantise one ISF vector for a SID frame — `Qisf_ns`.
///
/// Returns the five codebook indices. The reference writes the *decoded*
/// spectrum back over its input, and callers depend on that: the encoder's
/// local synthesis and its `isfold` memory both consume the quantised vector,
/// not the one that went in. This returns it explicitly instead of aliasing.
#[must_use]
pub fn quantise(ctx: &mut DspContext, isf: &[Word16; LP_ORDER]) -> ([u16; 5], [Word16; LP_ORDER]) {
    let mut residual = [Word16(0); LP_ORDER];
    for (slot, (&value, &mean)) in residual.iter_mut().zip(isf.iter().zip(&MEAN_ISF_NOISE)) {
        *slot = sub(ctx, value, Word16(mean));
    }

    let mut indices = [0u16; 5];
    for (index, &(offset, dim, book)) in indices.iter_mut().zip(&SPLITS) {
        let (best, _) = nearest_entry(ctx, &residual[offset..offset + dim], book);
        *index = best;
    }

    (indices, dequantise(ctx, &indices))
}

/// Reconstruct the ISF vector from five indices — `Disf_ns`.
///
/// # Panics
/// If any index is outside its codebook.
#[must_use]
pub fn dequantise(ctx: &mut DspContext, indices: &[u16; 5]) -> [Word16; LP_ORDER] {
    let mut isf = [Word16(0); LP_ORDER];
    for (&index, &(offset, dim, book)) in indices.iter().zip(&SPLITS) {
        let start = usize::from(index) * dim;
        let entry = book
            .get(start..start + dim)
            .expect("comfort-noise codebook index out of range");
        for (slot, &value) in isf[offset..offset + dim].iter_mut().zip(entry) {
            *slot = Word16(value);
        }
    }
    for (slot, &mean) in isf.iter_mut().zip(&MEAN_ISF_NOISE) {
        *slot = add(ctx, *slot, Word16(mean));
    }
    // `Reorder_isf(isf_q, ISF_GAP, ORDER)`: the same minimum spacing the speech
    // quantiser enforces, and the same constant. Without it a codebook pair can
    // produce a non-monotonic set the ISP conversion turns into an unstable
    // filter.
    debug_assert_eq!(ISF_GAP, Word16(128));
    enforce_min_spacing(ctx, &mut isf);
    isf
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn each_book_is_exactly_the_size_its_bit_width_allows() {
        // A book one entry short of its field would let an index escape it; one
        // entry long would make an index unreachable. Either is a table error
        // that no round trip would catch, because the search never emits the
        // out-of-range value itself.
        let mut counted = 0;
        for (&(_, dim, book), &bits) in SPLITS.iter().zip(&SPLIT_BITS) {
            assert_eq!(book.len() % dim, 0, "book is not a whole number of vectors");
            assert_eq!(book.len() / dim, 1usize << bits);
            counted += 1;
        }
        assert_eq!(counted, 5);
        assert_eq!(SPLITS.iter().map(|s| s.1).sum::<usize>(), LP_ORDER);
    }

    #[test]
    fn the_splits_tile_the_vector_without_gaps_or_overlap() {
        let mut covered = [false; LP_ORDER];
        for &(offset, dim, _) in &SPLITS {
            for slot in &mut covered[offset..offset + dim] {
                assert!(!*slot, "split overlap at {offset}");
                *slot = true;
            }
        }
        assert!(covered.iter().all(|&c| c));
    }

    #[test]
    fn quantising_the_mean_selects_whatever_is_nearest_the_origin() {
        // The mean vector quantises to a residual of zero, so each split picks
        // the codevector closest to the origin -- and the round trip must land
        // back near the mean rather than anywhere else.
        let mut ctx = DspContext::default();
        let mean = {
            let mut m = [Word16(0); LP_ORDER];
            for (slot, &value) in m.iter_mut().zip(&MEAN_ISF_NOISE) {
                *slot = Word16(value);
            }
            m
        };
        let (indices, decoded) = quantise(&mut ctx, &mean);
        assert_eq!(decoded, dequantise(&mut ctx, &indices));
        // Every coefficient within one codebook step of the mean. The last is
        // excluded: `enforce_min_spacing` never touches it, but nor is it
        // bounded by the same argument.
        for (i, (&got, &want)) in decoded.iter().zip(&MEAN_ISF_NOISE).enumerate().take(15) {
            assert!(
                (i32::from(got.0) - i32::from(want)).abs() < 1500,
                "coefficient {i} landed at {} for a mean of {want}",
                got.0
            );
        }
    }

    /// Against TS 26.173's own `Qisf_ns`, over a sweep that covers the books.
    ///
    /// The vectors come from a probe linked against the reference rather than
    /// captured from one stream: a captured set exercises whatever spectrum
    /// that signal happened to have, and would leave most of the five
    /// codebooks unvisited. Sixty-four ascending vectors with pseudo-random
    /// steps reach 48 distinct indices across the five splits.
    #[test]
    fn indices_and_decoded_spectrum_match_the_reference() {
        let text = include_str!("../testdata/wb_isf_noise_vectors.txt");
        let mut ctx = DspContext::default();
        let mut compared = 0usize;
        let mut cases = 0usize;
        let mut seen = std::collections::HashSet::new();

        for line in text
            .lines()
            .filter(|l| !l.starts_with('#') && !l.trim().is_empty())
        {
            let mut parts = line.split('|');
            let read = |field: Option<&str>| -> Vec<i16> {
                field
                    .expect("row is missing a field")
                    .split_whitespace()
                    .map(|v| v.parse().expect("not a number"))
                    .collect()
            };
            let input = read(parts.next());
            let want_indices = read(parts.next());
            let want_decoded = read(parts.next());
            assert_eq!(input.len(), LP_ORDER);
            assert_eq!(want_indices.len(), 5);
            assert_eq!(want_decoded.len(), LP_ORDER);

            let mut isf = [Word16(0); LP_ORDER];
            for (slot, &value) in isf.iter_mut().zip(&input) {
                *slot = Word16(value);
            }
            let (indices, decoded) = quantise(&mut ctx, &isf);

            for (split, (&got, &want)) in indices.iter().zip(&want_indices).enumerate() {
                assert_eq!(
                    i32::from(got),
                    i32::from(want),
                    "split {split} index differs on `{line}`"
                );
                seen.insert((split, got));
                compared += 1;
            }
            for (i, (&got, &want)) in decoded.iter().zip(&want_decoded).enumerate() {
                assert_eq!(got.0, want, "decoded coefficient {i} differs on `{line}`");
                compared += 1;
            }
            cases += 1;
        }

        assert_eq!(cases, 64, "the fixture lost rows");
        assert_eq!(compared, cases * (5 + LP_ORDER));
        assert!(
            seen.len() >= 40,
            "only {} distinct indices exercised; the sweep stopped covering the books",
            seen.len()
        );
    }

    #[test]
    fn the_output_is_always_monotonic_by_at_least_the_gap() {
        // Reorder_isf is the whole reason an arbitrary index combination is
        // safe to decode. Sweep every index in each split against a fixed
        // remainder and assert the invariant survives.
        let mut ctx = DspContext::default();
        let mut checked = 0;
        for (split, &bits) in SPLIT_BITS.iter().enumerate() {
            for index in 0..(1u16 << bits) {
                let mut indices = [0u16; 5];
                indices[split] = index;
                let isf = dequantise(&mut ctx, &indices);
                for window in isf[..LP_ORDER - 1].windows(2) {
                    assert!(
                        i32::from(window[1].0) - i32::from(window[0].0) >= i32::from(ISF_GAP.0)
                            || window[1].0 >= window[0].0,
                        "split {split} index {index} produced {:?}",
                        &isf[..]
                    );
                }
                checked += 1;
            }
        }
        assert_eq!(checked, 64 + 64 + 64 + 32 + 32);
    }
}