Skip to main content

oxideav_opus/
celt_e_prob_model.rs

1//! CELT coarse-energy Laplace-model parameter surface
2//! (RFC 6716 §4.3.2.1, pp. 108–109).
3//!
4//! The §4.3.2.1 *coarse energy* of each CELT band is coded as the
5//! Laplace-distributed difference between the band's 6 dB-quantised
6//! log-energy and a 2-D predictor running both in time (across frames)
7//! and in frequency (across bands). The decoder needs three pieces of
8//! data to drive that decode:
9//!
10//! 1. The prediction coefficients `(alpha, beta)`. RFC 6716 §4.3.2.1
11//!    (p. 108) fixes the *intra* case at `alpha = 0` and
12//!    `beta = 4915 / 32768` (Q15). The *inter* coefficients depend on
13//!    the frame size; the RFC body states the dependency but defers
14//!    the numeric values to the normative Appendix A reference code
15//!    (`quant_bands.c`), which fixes them per `LM` at
16//!    `alpha = {29440, 26112, 21248, 16384} / 32768` and
17//!    `beta = {30147, 22282, 12124, 6554} / 32768` (Q15) for
18//!    `LM = 0..=3`. See [`INTER_PRED_ALPHA_Q15`] /
19//!    [`INTER_PRED_BETA_Q15`] and the [`energy_pred_coef`] accessor.
20//! 2. The `e_prob_model` table — the per-band, per-mode parameters of
21//!    the Laplace distribution. The RFC describes the table as keyed
22//!    by `(LM, intra, band)` where `LM = log2(frame_size / 120)` so
23//!    `LM = 0,1,2,3` selects the 120/240/480/960-sample CELT frame
24//!    sizes, `intra ∈ {0,1}` selects inter vs. intra mode, and `band
25//!    ∈ 0..21` indexes the §4.3 Table 55 MDCT bands. Each `(LM, intra,
26//!    band)` triple yields a `{probability, decay}` Q8 pair (the
27//!    probability of decoding a zero from the Laplace model, plus the
28//!    geometric-decay rate for non-zero values).
29//! 3. The `ec_laplace_decode` routine that actually consumes the
30//!    range-coded symbol. This module owns only the *parameter
31//!    surface* — the table lookup that hands `ec_laplace_decode` its
32//!    `(prob, decay)` Q8 pair. The Laplace decoder itself, the 2-D
33//!    predictor application, and the §4.3.2.2 fine-energy follow-up
34//!    are out of scope for this module.
35//!
36//! The §4.3.2.1 narrative is verbatim transcribed from RFC 6716,
37//! `docs/audio/opus/rfc6716-opus.txt`, pp. 108–109. The 336-byte
38//! `e_prob_model` table data is uncopyrightable numeric facts
39//! extracted into `docs/audio/celt/tables/e_prob_model.csv`
40//! (see `docs/audio/celt/spec/celt-coarse-energy-and-allocation.md`
41//! §1.2 for the canonical layout). The values are reproduced inline
42//! here so the table is available without filesystem I/O at runtime.
43//!
44//! The eight per-LM *inter* `(alpha, beta)` Q15 numerators are numeric
45//! facts read from the `pred_coef[4]` / `beta_coef[4]` declarations in
46//! `quant_bands.c` of the RFC 6716 Appendix A reference code, which is
47//! embedded in the staged RFC text itself (extracted per the §A.1
48//! procedure; tarball SHA-1 verified against the value printed in
49//! §A.1). RFC 6716 §A.2 states that "it is the code in this document
50//! that shall remain normative", and §1 includes Appendix A in the
51//! normative text, so these constants carry the same normative weight
52//! as the prose. The `beta_intra = 4915` declaration in the same file
53//! confirms the §4.3.2.1 p. 108 intra value.
54//!
55//! ## Layout
56//!
57//! [`E_PROB_MODEL`] is a `[[[u8; 42]; 2]; 4]`:
58//!
59//! * outer axis (`LM`): 4 CELT frame sizes (120/240/480/960 samples).
60//! * middle axis (`intra`): `0 = inter`, `1 = intra` per §4.3.2.1.
61//! * inner axis: the 21 Table 55 bands, with the two Q8 bytes
62//!   `[prob_band_0, decay_band_0, prob_band_1, decay_band_1, ..., prob_band_20, decay_band_20]`
63//!   packed in band-ascending order.
64//!
65//! The CSV row index `(2*LM + intra)` and the CSV column ordering
66//! `lm,intra,prob0,decay0,...,prob20,decay20` from the
67//! `e_prob_model.csv` extract correspond exactly to this layout.
68
69use crate::celt_band_layout::CELT_NUM_BANDS;
70
71/// Number of CELT frame sizes that index the `e_prob_model` outer axis
72/// (`LM ∈ {0,1,2,3}` per §4.3.2.1 = 2.5 / 5 / 10 / 20 ms).
73pub const E_PROB_MODEL_LM_COUNT: usize = 4;
74
75/// Number of prediction modes per (LM, band) cell (§4.3.2.1:
76/// `0 = inter`, `1 = intra`).
77pub const E_PROB_MODEL_MODE_COUNT: usize = 2;
78
79/// Index into `e_prob_model[LM][mode]` selecting the **inter**-frame
80/// prediction parameters (§4.3.2.1: the prior frame's final fine
81/// quantisation participates in the predictor).
82pub const E_PROB_MODEL_MODE_INTER: usize = 0;
83
84/// Index into `e_prob_model[LM][mode]` selecting the **intra**-frame
85/// prediction parameters (§4.3.2.1: `alpha = 0`, the prior frame
86/// drops out, only the in-frame frequency predictor runs).
87pub const E_PROB_MODEL_MODE_INTRA: usize = 1;
88
89/// Two bytes per band: `[prob, decay]` Q8 pair feeding
90/// `ec_laplace_decode` (§4.3.2.1).
91pub const E_PROB_MODEL_BYTES_PER_BAND: usize = 2;
92
93/// 42 bytes per `(LM, mode)` row = 21 bands × 2 bytes per band.
94pub const E_PROB_MODEL_BYTES_PER_ROW: usize = CELT_NUM_BANDS * E_PROB_MODEL_BYTES_PER_BAND;
95
96/// Total table footprint: 4 × 2 × 42 = 336 bytes.
97pub const E_PROB_MODEL_TOTAL_BYTES: usize =
98    E_PROB_MODEL_LM_COUNT * E_PROB_MODEL_MODE_COUNT * E_PROB_MODEL_BYTES_PER_ROW;
99
100/// §4.3.2.1 *intra-frame* prediction coefficient `beta`, fixed at
101/// `4915 / 32768` per RFC 6716 §4.3.2.1 (p. 108). Stored as the Q15
102/// numerator (denominator implicit).
103pub const INTRA_PRED_BETA_Q15: u16 = 4915;
104
105/// Q15 fixed-point denominator paired with [`INTRA_PRED_BETA_Q15`].
106pub const Q15_ONE: u32 = 32768;
107
108/// §4.3.2.1 *intra-frame* prediction coefficient `alpha`, fixed at
109/// `0` per RFC 6716 §4.3.2.1 (p. 108). Exposed as a Q15 numerator
110/// against [`Q15_ONE`] for symmetry with [`INTRA_PRED_BETA_Q15`].
111pub const INTRA_PRED_ALPHA_Q15: u16 = 0;
112
113/// §4.3.2.1 *inter-frame* prediction coefficient `alpha` per frame
114/// size, indexed by `LM = log2(frame_size / 120) ∈ 0..=3`. Stored as
115/// Q15 numerators against [`Q15_ONE`].
116///
117/// `alpha` weights the time-domain predictor (the prior frame's final
118/// fine-quantised energy) in the §4.3.2.1 2-D prediction filter
119/// `A(z_l, z_b)`. RFC 6716 §4.3.2.1 (p. 108) states the inter
120/// coefficients "depend on the frame size in use"; the numeric values
121/// are fixed by the normative Appendix A reference code
122/// (`pred_coef[4]` in `quant_bands.c`): `{29440, 26112, 21248, 16384}
123/// / 32768 ≈ {0.898, 0.797, 0.648, 0.500}`. The weight shrinks as
124/// the frame grows — at 20 ms (`LM = 3`) it is exactly `1/2` — because
125/// a longer gap between frames makes the previous frame's energy a
126/// weaker predictor.
127pub const INTER_PRED_ALPHA_Q15: [u16; E_PROB_MODEL_LM_COUNT] = [29440, 26112, 21248, 16384];
128
129/// §4.3.2.1 *inter-frame* prediction coefficient `beta` per frame
130/// size, indexed by `LM = log2(frame_size / 120) ∈ 0..=3`. Stored as
131/// Q15 numerators against [`Q15_ONE`].
132///
133/// `beta` is the leakage coefficient of the in-frame frequency
134/// predictor (the `1 / (1 - beta * z_b^-1)` denominator of the
135/// §4.3.2.1 2-D prediction filter). The numeric values are fixed by
136/// the normative Appendix A reference code (`beta_coef[4]` in
137/// `quant_bands.c`): `{30147, 22282, 12124, 6554} / 32768 ≈
138/// {0.920, 0.680, 0.370, 0.200}`.
139pub const INTER_PRED_BETA_Q15: [u16; E_PROB_MODEL_LM_COUNT] = [30147, 22282, 12124, 6554];
140
141/// §4.3.2.1 Laplace-model `(prob, decay)` Q8 pair for a single band.
142///
143/// `prob` is the probability of `0` returned by the Laplace decoder
144/// (in Q8, so `255 ≈ 0.996`); `decay` is the geometric-decay rate of
145/// the non-zero tail (also Q8). Both fields are unsigned bytes per
146/// the §4.3.2.1 narrative.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub struct EProbPair {
149    /// Probability of `0` returned by `ec_laplace_decode` (Q8).
150    pub prob: u8,
151    /// Geometric-decay rate of the Laplace tail (Q8).
152    pub decay: u8,
153}
154
155/// §4.3.2.1 coarse-energy prediction mode selector.
156///
157/// The §4.3.2.1 `intra` flag in the CELT header (decoded by
158/// [`crate::celt_header::CeltHeaderPrefix`]) routes to one of these
159/// two cases. The selector is the inner-axis index into
160/// [`E_PROB_MODEL`].
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum EnergyPredictionMode {
163    /// Inter-frame prediction (the default). §4.3.2.1: the predictor
164    /// runs across the prior frame's final fine quantisation; the
165    /// `(alpha, beta)` coefficients depend on the frame size — see
166    /// [`INTER_PRED_ALPHA_Q15`] / [`INTER_PRED_BETA_Q15`].
167    Inter,
168    /// Intra-frame prediction (the §4.3.2.1 carve-out signalled by the
169    /// CELT header `intra` flag). `alpha = 0` and `beta = 4915/32768`;
170    /// the prior frame drops out of the predictor entirely.
171    Intra,
172}
173
174impl EnergyPredictionMode {
175    /// Decode the §4.3.2.1 `intra` header bit into a mode selector.
176    ///
177    /// `intra_flag = true` → [`EnergyPredictionMode::Intra`];
178    /// `intra_flag = false` → [`EnergyPredictionMode::Inter`].
179    pub const fn from_intra_flag(intra_flag: bool) -> Self {
180        if intra_flag {
181            EnergyPredictionMode::Intra
182        } else {
183            EnergyPredictionMode::Inter
184        }
185    }
186
187    /// Inner-axis index into [`E_PROB_MODEL`].
188    pub const fn table_index(self) -> usize {
189        match self {
190            EnergyPredictionMode::Inter => E_PROB_MODEL_MODE_INTER,
191            EnergyPredictionMode::Intra => E_PROB_MODEL_MODE_INTRA,
192        }
193    }
194}
195
196/// §4.3.2.1 coarse-energy prediction coefficients `(alpha, beta)` for
197/// one `(LM, mode)` cell, as Q15 numerators against [`Q15_ONE`].
198///
199/// `alpha` weights the time-domain (previous-frame) predictor and
200/// `beta` the in-frame frequency-leakage term of the §4.3.2.1 2-D
201/// prediction filter `A(z_l, z_b)`. Obtain via [`energy_pred_coef`].
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub struct EnergyPredCoef {
204    /// Time-domain prediction weight (Q15). `0` in intra mode.
205    pub alpha_q15: u16,
206    /// Frequency-domain leakage coefficient (Q15).
207    pub beta_q15: u16,
208}
209
210impl EnergyPredCoef {
211    /// `alpha` as the exact binary fraction `alpha_q15 / 32768`.
212    pub fn alpha(self) -> f64 {
213        f64::from(self.alpha_q15) / f64::from(Q15_ONE)
214    }
215
216    /// `beta` as the exact binary fraction `beta_q15 / 32768`.
217    pub fn beta(self) -> f64 {
218        f64::from(self.beta_q15) / f64::from(Q15_ONE)
219    }
220}
221
222/// Look up the §4.3.2.1 prediction coefficients `(alpha, beta)` for a
223/// frame size and prediction mode.
224///
225/// `lm` is `log2(frame_size / 120) ∈ 0..=3`. In intra mode the result
226/// is the frame-size-independent pair `(0, 4915)` (RFC 6716 §4.3.2.1
227/// p. 108); `lm` is still range-checked so both modes share one
228/// contract. In inter mode the result is
229/// `(INTER_PRED_ALPHA_Q15[lm], INTER_PRED_BETA_Q15[lm])`, the per-LM
230/// pair fixed by the normative Appendix A reference code.
231pub fn energy_pred_coef(
232    lm: u32,
233    mode: EnergyPredictionMode,
234) -> Result<EnergyPredCoef, EProbModelError> {
235    if lm >= E_PROB_MODEL_LM_COUNT as u32 {
236        return Err(EProbModelError::LmOutOfRange { lm });
237    }
238    Ok(match mode {
239        EnergyPredictionMode::Inter => EnergyPredCoef {
240            alpha_q15: INTER_PRED_ALPHA_Q15[lm as usize],
241            beta_q15: INTER_PRED_BETA_Q15[lm as usize],
242        },
243        EnergyPredictionMode::Intra => EnergyPredCoef {
244            alpha_q15: INTRA_PRED_ALPHA_Q15,
245            beta_q15: INTRA_PRED_BETA_Q15,
246        },
247    })
248}
249
250/// §4.3.2.1 `e_prob_model` table — 4 frame sizes × 2 modes × 21 bands
251/// × `{prob, decay}` Q8 pair.
252///
253/// Indexing convention: `E_PROB_MODEL[LM][mode][band * 2 + 0]` = `prob`,
254/// `E_PROB_MODEL[LM][mode][band * 2 + 1]` = `decay`. Use
255/// [`e_prob_pair`] for a typed accessor.
256///
257/// Data provenance: `docs/audio/celt/tables/e_prob_model.csv` (Q8
258/// numeric facts; see the CSV's `.meta` sidecar for the canonical
259/// layout). RFC 6716 §4.3.2.1 names the table `e_prob_model` and
260/// describes it as held in `quant_bands.c`; only the numeric data is
261/// reproduced here.
262pub const E_PROB_MODEL: [[[u8; E_PROB_MODEL_BYTES_PER_ROW]; E_PROB_MODEL_MODE_COUNT];
263    E_PROB_MODEL_LM_COUNT] = [
264    // LM = 0 (120-sample frame, 2.5 ms at 48 kHz)
265    [
266        // inter
267        [
268            72, 127, 65, 129, 66, 128, 65, 128, 64, 128, 62, 128, 64, 128, 64, 128, 92, 78, 92, 79,
269            92, 78, 90, 79, 116, 41, 115, 40, 114, 40, 132, 26, 132, 26, 145, 17, 161, 12, 176, 10,
270            177, 11,
271        ],
272        // intra
273        [
274            24, 179, 48, 138, 54, 135, 54, 132, 53, 134, 56, 133, 55, 132, 55, 132, 61, 114, 70,
275            96, 74, 88, 75, 88, 87, 74, 89, 66, 91, 67, 100, 59, 108, 50, 120, 40, 122, 37, 97, 43,
276            78, 50,
277        ],
278    ],
279    // LM = 1 (240-sample frame, 5 ms at 48 kHz)
280    [
281        // inter
282        [
283            83, 78, 84, 81, 88, 75, 86, 74, 87, 71, 90, 73, 93, 74, 93, 74, 109, 40, 114, 36, 117,
284            34, 117, 34, 143, 17, 145, 18, 146, 19, 162, 12, 165, 10, 178, 7, 189, 6, 190, 8, 177,
285            9,
286        ],
287        // intra
288        [
289            23, 178, 54, 115, 63, 102, 66, 98, 69, 99, 74, 89, 71, 91, 73, 91, 78, 89, 86, 80, 92,
290            66, 93, 64, 102, 59, 103, 60, 104, 60, 117, 52, 123, 44, 138, 35, 133, 31, 97, 38, 77,
291            45,
292        ],
293    ],
294    // LM = 2 (480-sample frame, 10 ms at 48 kHz)
295    [
296        // inter
297        [
298            61, 90, 93, 60, 105, 42, 107, 41, 110, 45, 116, 38, 113, 38, 112, 38, 124, 26, 132, 27,
299            136, 19, 140, 20, 155, 14, 159, 16, 158, 18, 170, 13, 177, 10, 187, 8, 192, 6, 175, 9,
300            159, 10,
301        ],
302        // intra
303        [
304            21, 178, 59, 110, 71, 86, 75, 85, 84, 83, 91, 66, 88, 73, 87, 72, 92, 75, 98, 72, 105,
305            58, 107, 54, 115, 52, 114, 55, 112, 56, 129, 51, 132, 40, 150, 33, 140, 29, 98, 35, 77,
306            42,
307        ],
308    ],
309    // LM = 3 (960-sample frame, 20 ms at 48 kHz)
310    [
311        // inter
312        [
313            42, 121, 96, 66, 108, 43, 111, 40, 117, 44, 123, 32, 120, 36, 119, 33, 127, 33, 134,
314            34, 139, 21, 147, 23, 152, 20, 158, 25, 154, 26, 166, 21, 173, 16, 184, 13, 184, 10,
315            150, 13, 139, 15,
316        ],
317        // intra
318        [
319            22, 178, 63, 114, 74, 82, 84, 83, 92, 82, 103, 62, 96, 72, 96, 67, 101, 73, 107, 72,
320            113, 55, 118, 52, 125, 52, 118, 52, 117, 55, 135, 49, 137, 39, 157, 32, 145, 29, 97,
321            33, 77, 40,
322        ],
323    ],
324];
325
326/// Errors returned by [`e_prob_pair`] for out-of-range indices.
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328pub enum EProbModelError {
329    /// `LM` is outside `0..4` (§4.3.2.1 only defines four frame
330    /// sizes).
331    LmOutOfRange { lm: u32 },
332    /// `band` is outside `0..21` (the Table 55 band count).
333    BandOutOfRange { band: u32 },
334}
335
336/// Look up the Laplace `(prob, decay)` Q8 pair for one CELT band.
337///
338/// `lm` is `log2(frame_size/120) ∈ 0..=3`; `mode` selects inter vs.
339/// intra; `band` is the §4.3 Table 55 band index `0..=20`. Returns
340/// an [`EProbPair`] holding the pair the §4.3.2.1
341/// `ec_laplace_decode` would consume for this `(LM, mode, band)`.
342pub fn e_prob_pair(
343    lm: u32,
344    mode: EnergyPredictionMode,
345    band: u32,
346) -> Result<EProbPair, EProbModelError> {
347    if lm >= E_PROB_MODEL_LM_COUNT as u32 {
348        return Err(EProbModelError::LmOutOfRange { lm });
349    }
350    if band >= CELT_NUM_BANDS as u32 {
351        return Err(EProbModelError::BandOutOfRange { band });
352    }
353    let row = &E_PROB_MODEL[lm as usize][mode.table_index()];
354    let off = (band as usize) * E_PROB_MODEL_BYTES_PER_BAND;
355    Ok(EProbPair {
356        prob: row[off],
357        decay: row[off + 1],
358    })
359}
360
361/// Borrow the full 42-byte `(prob, decay)` row for a single
362/// `(LM, mode)` cell of [`E_PROB_MODEL`].
363///
364/// This is the §4.3.2.1 "one row of 21 `{prob,decay}` pairs"
365/// (`docs/audio/celt/tables/e_prob_model.csv` row layout). Returned
366/// as a borrowed slice so callers may iterate the band loop without
367/// re-indexing.
368pub fn e_prob_row(
369    lm: u32,
370    mode: EnergyPredictionMode,
371) -> Result<&'static [u8; E_PROB_MODEL_BYTES_PER_ROW], EProbModelError> {
372    if lm >= E_PROB_MODEL_LM_COUNT as u32 {
373        return Err(EProbModelError::LmOutOfRange { lm });
374    }
375    Ok(&E_PROB_MODEL[lm as usize][mode.table_index()])
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    // ---- Table-shape invariants ----
383
384    #[test]
385    fn table_shape_constants_match_struct() {
386        assert_eq!(E_PROB_MODEL_LM_COUNT, 4);
387        assert_eq!(E_PROB_MODEL_MODE_COUNT, 2);
388        assert_eq!(E_PROB_MODEL_BYTES_PER_BAND, 2);
389        assert_eq!(E_PROB_MODEL_BYTES_PER_ROW, 42);
390        assert_eq!(E_PROB_MODEL_TOTAL_BYTES, 336);
391    }
392
393    #[test]
394    fn table_inner_row_length_matches_band_count_times_two() {
395        for (lm, by_lm) in E_PROB_MODEL.iter().enumerate() {
396            for (mode, row) in by_lm.iter().enumerate() {
397                assert_eq!(
398                    row.len(),
399                    E_PROB_MODEL_BYTES_PER_ROW,
400                    "(lm={lm},mode={mode}) inner row length mismatch"
401                );
402                assert_eq!(
403                    row.len(),
404                    CELT_NUM_BANDS * 2,
405                    "row should be 21 bands × 2 bytes"
406                );
407            }
408        }
409    }
410
411    #[test]
412    fn table_total_bytes_matches_lm_times_mode_times_row() {
413        let total: usize = E_PROB_MODEL
414            .iter()
415            .map(|by_lm| by_lm.iter().map(|row| row.len()).sum::<usize>())
416            .sum();
417        assert_eq!(total, E_PROB_MODEL_TOTAL_BYTES);
418    }
419
420    // ---- Intra prediction coefficients (RFC 6716 §4.3.2.1 p.108) ----
421
422    #[test]
423    fn intra_alpha_is_zero_per_rfc() {
424        assert_eq!(INTRA_PRED_ALPHA_Q15, 0);
425    }
426
427    #[test]
428    fn intra_beta_is_4915_over_32768_per_rfc() {
429        assert_eq!(INTRA_PRED_BETA_Q15, 4915);
430        assert_eq!(Q15_ONE, 32768);
431        // The Q15 ratio 4915/32768 = 0.14999389648437500 — within
432        // ~6.1e-6 of the RFC's textual 0.15 approximation. We don't
433        // assert a float here; we pin the numerator/denominator.
434    }
435
436    // ---- Inter prediction coefficients (RFC 6716 §4.3.2.1 +
437    //      normative Appendix A `quant_bands.c` data) ----
438
439    #[test]
440    fn inter_alpha_q15_values_per_appendix_a() {
441        // Appendix A `pred_coef[4]` (quant_bands.c): one Q15 numerator
442        // per LM = 0..=3 (120/240/480/960-sample frames).
443        assert_eq!(INTER_PRED_ALPHA_Q15, [29440, 26112, 21248, 16384]);
444    }
445
446    #[test]
447    fn inter_beta_q15_values_per_appendix_a() {
448        // Appendix A `beta_coef[4]` (quant_bands.c).
449        assert_eq!(INTER_PRED_BETA_Q15, [30147, 22282, 12124, 6554]);
450    }
451
452    #[test]
453    fn inter_alpha_lm3_is_exactly_one_half() {
454        // 16384 / 32768 = 1/2 exactly — the 20 ms frame halves the
455        // previous-frame predictor weight.
456        assert_eq!(u32::from(INTER_PRED_ALPHA_Q15[3]) * 2, Q15_ONE);
457    }
458
459    #[test]
460    fn inter_coefficients_strictly_decrease_with_frame_size() {
461        // §4.3.2.1: longer frames lean less on both predictors; the
462        // Appendix A data is strictly decreasing in LM for alpha and
463        // beta alike.
464        for lm in 0..E_PROB_MODEL_LM_COUNT - 1 {
465            assert!(
466                INTER_PRED_ALPHA_Q15[lm] > INTER_PRED_ALPHA_Q15[lm + 1],
467                "alpha should strictly decrease between LM={lm} and LM={}",
468                lm + 1
469            );
470            assert!(
471                INTER_PRED_BETA_Q15[lm] > INTER_PRED_BETA_Q15[lm + 1],
472                "beta should strictly decrease between LM={lm} and LM={}",
473                lm + 1
474            );
475        }
476    }
477
478    #[test]
479    fn inter_beta_always_exceeds_intra_beta() {
480        // Even the weakest inter leakage (LM = 3, 6554) exceeds the
481        // intra constant 4915: with the time predictor active, the
482        // frequency predictor leaks more.
483        for &beta in &INTER_PRED_BETA_Q15 {
484            assert!(beta > INTRA_PRED_BETA_Q15);
485        }
486    }
487
488    #[test]
489    fn energy_pred_coef_inter_matches_tables_for_every_lm() {
490        for lm in 0..E_PROB_MODEL_LM_COUNT as u32 {
491            let c = energy_pred_coef(lm, EnergyPredictionMode::Inter).unwrap();
492            assert_eq!(c.alpha_q15, INTER_PRED_ALPHA_Q15[lm as usize]);
493            assert_eq!(c.beta_q15, INTER_PRED_BETA_Q15[lm as usize]);
494        }
495    }
496
497    #[test]
498    fn energy_pred_coef_intra_is_lm_independent() {
499        for lm in 0..E_PROB_MODEL_LM_COUNT as u32 {
500            let c = energy_pred_coef(lm, EnergyPredictionMode::Intra).unwrap();
501            assert_eq!(
502                c,
503                EnergyPredCoef {
504                    alpha_q15: 0,
505                    beta_q15: 4915,
506                }
507            );
508        }
509    }
510
511    #[test]
512    fn energy_pred_coef_rejects_lm_out_of_range_in_both_modes() {
513        for mode in [EnergyPredictionMode::Inter, EnergyPredictionMode::Intra] {
514            let err = energy_pred_coef(4, mode).unwrap_err();
515            assert_eq!(err, EProbModelError::LmOutOfRange { lm: 4 });
516            let err = energy_pred_coef(u32::MAX, mode).unwrap_err();
517            assert_eq!(err, EProbModelError::LmOutOfRange { lm: u32::MAX });
518        }
519    }
520
521    #[test]
522    fn energy_pred_coef_float_views_match_q15_ratios() {
523        let c = energy_pred_coef(3, EnergyPredictionMode::Inter).unwrap();
524        // 16384/32768 and 6554/32768 are exact binary fractions.
525        assert_eq!(c.alpha(), 0.5);
526        assert_eq!(c.beta(), 6554.0 / 32768.0);
527        let c = energy_pred_coef(0, EnergyPredictionMode::Intra).unwrap();
528        assert_eq!(c.alpha(), 0.0);
529        assert_eq!(c.beta(), 4915.0 / 32768.0);
530    }
531
532    #[test]
533    fn inter_q15_approximations_documented_in_doc_comments() {
534        // The doc comments cite ≈ {0.920, 0.680, 0.370, 0.200} and
535        // ≈ {0.898, 0.797, 0.648, 0.500}; pin them to 3 decimals.
536        let beta_approx = [0.920, 0.680, 0.370, 0.200];
537        let alpha_approx = [0.898, 0.797, 0.648, 0.500];
538        for lm in 0..E_PROB_MODEL_LM_COUNT {
539            let a = f64::from(INTER_PRED_ALPHA_Q15[lm]) / f64::from(Q15_ONE);
540            let b = f64::from(INTER_PRED_BETA_Q15[lm]) / f64::from(Q15_ONE);
541            assert!((a - alpha_approx[lm]).abs() < 5e-4, "alpha LM={lm}");
542            assert!((b - beta_approx[lm]).abs() < 5e-4, "beta LM={lm}");
543        }
544    }
545
546    // ---- EnergyPredictionMode mapping ----
547
548    #[test]
549    fn intra_flag_true_routes_to_intra() {
550        assert_eq!(
551            EnergyPredictionMode::from_intra_flag(true),
552            EnergyPredictionMode::Intra
553        );
554    }
555
556    #[test]
557    fn intra_flag_false_routes_to_inter() {
558        assert_eq!(
559            EnergyPredictionMode::from_intra_flag(false),
560            EnergyPredictionMode::Inter
561        );
562    }
563
564    #[test]
565    fn mode_table_indices_match_csv_layout() {
566        assert_eq!(EnergyPredictionMode::Inter.table_index(), 0);
567        assert_eq!(EnergyPredictionMode::Intra.table_index(), 1);
568        assert_eq!(
569            EnergyPredictionMode::Inter.table_index(),
570            E_PROB_MODEL_MODE_INTER
571        );
572        assert_eq!(
573            EnergyPredictionMode::Intra.table_index(),
574            E_PROB_MODEL_MODE_INTRA
575        );
576    }
577
578    // ---- Spot-check the Q8 values against the CSV extract ----
579    //
580    // These pins reproduce a hand-picked sample from
581    // `docs/audio/celt/tables/e_prob_model.csv` so a future edit that
582    // accidentally reorders the table or drops a byte trips the test
583    // suite. Each row references the CSV row + the column position of
584    // the byte.
585
586    #[test]
587    fn csv_row_0_lm0_inter_first_pair_band_0() {
588        // CSV row 0: "0,0,72,127,..." — LM=0, intra=0, band 0 = (72, 127).
589        let p = e_prob_pair(0, EnergyPredictionMode::Inter, 0).unwrap();
590        assert_eq!(
591            p,
592            EProbPair {
593                prob: 72,
594                decay: 127
595            }
596        );
597    }
598
599    #[test]
600    fn csv_row_0_lm0_inter_last_pair_band_20() {
601        // CSV row 0 final pair: "...,177,11" — band 20 = (177, 11).
602        let p = e_prob_pair(0, EnergyPredictionMode::Inter, 20).unwrap();
603        assert_eq!(
604            p,
605            EProbPair {
606                prob: 177,
607                decay: 11
608            }
609        );
610    }
611
612    #[test]
613    fn csv_row_1_lm0_intra_first_pair_band_0() {
614        // CSV row 1: "0,1,24,179,..." — LM=0, intra=1, band 0 = (24, 179).
615        let p = e_prob_pair(0, EnergyPredictionMode::Intra, 0).unwrap();
616        assert_eq!(
617            p,
618            EProbPair {
619                prob: 24,
620                decay: 179
621            }
622        );
623    }
624
625    #[test]
626    fn csv_row_3_lm1_intra_band_5() {
627        // CSV row 3: "1,1,23,178,54,115,63,102,66,98,69,99,74,89,..."
628        // → band 5 (the 6th band) `(prob, decay) = (74, 89)`.
629        let p = e_prob_pair(1, EnergyPredictionMode::Intra, 5).unwrap();
630        assert_eq!(
631            p,
632            EProbPair {
633                prob: 74,
634                decay: 89
635            }
636        );
637    }
638
639    #[test]
640    fn csv_row_4_lm2_inter_band_10() {
641        // CSV row 4: "2,0,61,90,93,60,105,42,107,41,110,45,116,38,113,38,112,38,124,26,132,27,136,19,..."
642        // → band 10 (11th band) = pair starting at column 22 → (136, 19).
643        let p = e_prob_pair(2, EnergyPredictionMode::Inter, 10).unwrap();
644        assert_eq!(
645            p,
646            EProbPair {
647                prob: 136,
648                decay: 19
649            }
650        );
651    }
652
653    #[test]
654    fn csv_row_6_lm3_inter_first_pair_band_0() {
655        // CSV row 6: "3,0,42,121,..." — LM=3, intra=0, band 0 = (42, 121).
656        let p = e_prob_pair(3, EnergyPredictionMode::Inter, 0).unwrap();
657        assert_eq!(
658            p,
659            EProbPair {
660                prob: 42,
661                decay: 121
662            }
663        );
664    }
665
666    #[test]
667    fn csv_row_7_lm3_intra_last_pair_band_20() {
668        // CSV row 7 final pair "...,77,40" — band 20 = (77, 40).
669        let p = e_prob_pair(3, EnergyPredictionMode::Intra, 20).unwrap();
670        assert_eq!(
671            p,
672            EProbPair {
673                prob: 77,
674                decay: 40
675            }
676        );
677    }
678
679    // ---- Error-path coverage ----
680
681    #[test]
682    fn e_prob_pair_rejects_lm_out_of_range() {
683        let err = e_prob_pair(4, EnergyPredictionMode::Inter, 0).unwrap_err();
684        assert_eq!(err, EProbModelError::LmOutOfRange { lm: 4 });
685        let err = e_prob_pair(u32::MAX, EnergyPredictionMode::Intra, 0).unwrap_err();
686        assert_eq!(err, EProbModelError::LmOutOfRange { lm: u32::MAX });
687    }
688
689    #[test]
690    fn e_prob_pair_rejects_band_out_of_range() {
691        let err = e_prob_pair(0, EnergyPredictionMode::Inter, 21).unwrap_err();
692        assert_eq!(err, EProbModelError::BandOutOfRange { band: 21 });
693        let err = e_prob_pair(2, EnergyPredictionMode::Intra, 100).unwrap_err();
694        assert_eq!(err, EProbModelError::BandOutOfRange { band: 100 });
695    }
696
697    #[test]
698    fn e_prob_row_returns_full_42_byte_row() {
699        let row = e_prob_row(0, EnergyPredictionMode::Inter).unwrap();
700        assert_eq!(row.len(), 42);
701        // First two bytes are the band-0 pair `(72, 127)`.
702        assert_eq!(row[0], 72);
703        assert_eq!(row[1], 127);
704        // Last two bytes are the band-20 pair `(177, 11)`.
705        assert_eq!(row[40], 177);
706        assert_eq!(row[41], 11);
707    }
708
709    #[test]
710    fn e_prob_row_rejects_lm_out_of_range() {
711        let err = e_prob_row(99, EnergyPredictionMode::Inter).unwrap_err();
712        assert_eq!(err, EProbModelError::LmOutOfRange { lm: 99 });
713    }
714
715    // ---- Property-style sweeps over the full table surface ----
716
717    #[test]
718    fn every_lm_mode_band_lookup_succeeds() {
719        for lm in 0..E_PROB_MODEL_LM_COUNT as u32 {
720            for mode in [EnergyPredictionMode::Inter, EnergyPredictionMode::Intra] {
721                for band in 0..CELT_NUM_BANDS as u32 {
722                    let p = e_prob_pair(lm, mode, band).unwrap_or_else(|e| {
723                        panic!("lookup failed for (lm={lm},mode={mode:?},band={band}): {e:?}")
724                    });
725                    // Sanity: prob and decay are stored as u8, so
726                    // each field naturally satisfies 0..=255; nothing
727                    // further to assert at the type level.
728                    let _ = p.prob;
729                    let _ = p.decay;
730                }
731            }
732        }
733    }
734
735    #[test]
736    fn pair_lookup_matches_row_lookup_for_every_cell() {
737        for lm in 0..E_PROB_MODEL_LM_COUNT as u32 {
738            for mode in [EnergyPredictionMode::Inter, EnergyPredictionMode::Intra] {
739                let row = e_prob_row(lm, mode).unwrap();
740                for band in 0..CELT_NUM_BANDS as u32 {
741                    let pair = e_prob_pair(lm, mode, band).unwrap();
742                    let off = (band as usize) * 2;
743                    assert_eq!(
744                        pair.prob, row[off],
745                        "(lm={lm},mode={mode:?},band={band}) prob mismatch"
746                    );
747                    assert_eq!(
748                        pair.decay,
749                        row[off + 1],
750                        "(lm={lm},mode={mode:?},band={band}) decay mismatch"
751                    );
752                }
753            }
754        }
755    }
756
757    #[test]
758    fn intra_rows_have_lower_band0_probability_than_inter() {
759        // Sanity property derived from §4.3.2.1: the intra rows are
760        // the "no time predictor" case, which leaves wider Laplace
761        // tails for the first band (prediction is least effective at
762        // band 0). The CSV-extracted data should reflect that —
763        // band-0 `prob` is markedly lower in the intra row than the
764        // inter row for every LM.
765        for lm in 0..E_PROB_MODEL_LM_COUNT as u32 {
766            let inter = e_prob_pair(lm, EnergyPredictionMode::Inter, 0).unwrap();
767            let intra = e_prob_pair(lm, EnergyPredictionMode::Intra, 0).unwrap();
768            assert!(
769                intra.prob < inter.prob,
770                "(lm={lm}) intra band-0 prob {} should be < inter band-0 prob {}",
771                intra.prob,
772                inter.prob
773            );
774        }
775    }
776}