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}