Skip to main content

oxideav_opus/
celt_static_alloc.rs

1//! CELT §4.3.3 static allocation table (RFC 6716 §4.3.3, p. 112).
2//!
3//! The §4.3.3 *Bit Allocation* procedure pins each band's "static"
4//! shape allocation to a hard-coded table indexed by `(band, q)` where
5//! `band` is the §4.3 Table 55 band index and `q ∈ 0..=10` is a
6//! quality column. RFC 6716 §4.3.3 (p. 112) names the table `alloc[][]`
7//! and gives its 21×11 grid as Table 57: every cell stores a Q5 value
8//! in 1/32-bit per MDCT bin units.
9//!
10//! The §4.3.3 narrative (RFC 6716 §4.3.3, p. 111, lines 6223–6230) reads:
11//!
12//! > The "static" bit allocation (in 1/8 bits) for a quality q,
13//! > excluding the minimums, maximums, tilt and boosts, is equal to
14//! > `channels*N*alloc[band][q]<<LM>>2`, where `alloc[][]` is given in
15//! > Table 57 and `LM=log2(frame_size/120)`. The allocation is
16//! > obtained by linearly interpolating between two values of `q` (in
17//! > steps of 1/64) to find the highest allocation that does not exceed
18//! > the number of bits remaining.
19//!
20//! ## §4.3.3 unit conversion
21//!
22//! For each band `b`, with `channels ∈ {1, 2}` the frame's channel
23//! count, `N = bins_per_channel(b, frame_size)` the §4.3 Table 55
24//! per-channel MDCT-bin count for `b` at the frame's LM, and
25//! `q ∈ {0, …, 10}` a quality column:
26//!
27//! ```text
28//! static_alloc[b][q] = (channels * N * STATIC_ALLOC[b][q]) << LM >> 2
29//! ```
30//!
31//! All arithmetic is unsigned. The output is in 1/8 bits, the same
32//! units every other §4.3.3 budget quantity uses (compatible with the
33//! round-34 [`crate::celt_reservations`] output, the round-35
34//! [`crate::celt_band_thresh`] floor, the round-36
35//! [`crate::celt_trim_offsets`] tilt bias, the round-33 boosts, and
36//! the round-31 [`crate::celt_cache_caps50`] per-band cap at the
37//! consumer site).
38//!
39//! ## §4.3.3 column structure
40//!
41//! The 11 quality columns embed a coarse-to-fine quality ladder:
42//!
43//! * Column `0` is the §4.3.3 "no allocation" column — every cell is
44//!   zero. A band whose interpolated `q` lands at column 0 receives
45//!   no static shape allocation; this is the band-skip floor the
46//!   §4.3.3 search uses when even the §4.3.3 minimum threshold cannot
47//!   be hit.
48//! * Columns `1..=9` are the §4.3.3 working quality range. The
49//!   §4.3.3 RFC text describes the search as "the highest allocation
50//!   that does not exceed the number of bits remaining" — so the
51//!   §4.3.3 allocator picks a `q` whose interpolated allocation fits
52//!   the budget, scaled by the per-band `n_bins` factor.
53//! * Column `10` is the §4.3.3 "saturation" column — every cell is
54//!   `200` or close to it. A band whose interpolated `q` reaches
55//!   column 10 has hit the highest static allocation the table
56//!   defines; the §4.3.3 per-band cap from
57//!   [`crate::celt_cache_caps50::cap_for_band_bits`] takes over
58//!   when the static allocation exceeds the cap.
59//!
60//! Within each row the entries are monotone non-decreasing in `q`:
61//! a higher quality column never reduces the allocation for the same
62//! band (with the carve-out at high bands where columns 0..=K are all
63//! zero before the first non-zero entry — once the column is non-zero
64//! it is monotone non-decreasing).
65//!
66//! ## §4.3.3 1/64-step linear interpolation
67//!
68//! The §4.3.3 search picks a quality `q` whose interpolated allocation
69//! fits the working budget. Interpolation is in *steps of 1/64* between
70//! two integer quality columns. That choice converts the per-band Q5
71//! table to a Q11 interpolated allocation (= Q5 × Q6) before the §4.3.3
72//! `<< LM >> 2` step folds it back to Q3 (1/8-bit) units. The §4.3.3
73//! RFC narrative is explicit: every other unit in the §4.3.3 procedure
74//! is 1/8 bit, so the table-step interpolation has to be at finer
75//! precision than the output to keep the search well-conditioned.
76//!
77//! This module owns only the §4.3.3 *parameter surface*: the 231-cell
78//! `STATIC_ALLOC` table reproduced inline, the typed accessors that
79//! pair it with the §4.3.3 indexing rule, and the
80//! [`static_alloc_eighth_bits`] conversion that folds in the
81//! `(channels, n_bins, LM)` scale. The §4.3.3 search itself — the
82//! 1/64-step interpolation that converges on `q` for a given budget —
83//! runs at the §4.3.3 allocator's consumer site (the round that lands
84//! the orchestrated allocation search). That search consumes:
85//!
86//! * the per-band [`crate::celt_cache_caps50::cap_for_band_bits`]
87//!   per-band cap (round 31),
88//! * the per-band [`crate::celt_band_thresh::band_min_thresh`] floor
89//!   (round 35),
90//! * the per-band [`crate::celt_trim_offsets::band_trim_offset`] tilt
91//!   bias (round 36),
92//! * the per-band [`crate::celt_band_boost`] boosts (round 33), and
93//! * the working budget from
94//!   [`crate::celt_reservations::ReservationOutcome::total_remaining_eighth_bits`]
95//!   (round 34).
96//!
97//! ## Provenance
98//!
99//! The §4.3.3 narrative is transcribed from RFC 6716,
100//! `docs/audio/opus/rfc6716-opus.txt`, pp. 111–112. The 231-cell
101//! `STATIC_ALLOC` table is the numeric content of RFC 6716 §4.3.3
102//! Table 57 (p. 112): 21 band-rows × 11 quality-columns, in 1/32 bit
103//! per MDCT bin Q5 units. The numeric values are uncopyrightable facts
104//! under Feist v. Rural; the §4.3.3 RFC text identifies the table by
105//! its `(band, q)` indexing rule and gives the values directly in the
106//! standards-track text. Reproduced inline here so the table is
107//! available without filesystem I/O at runtime.
108
109use crate::celt_band_layout::CELT_NUM_BANDS;
110
111/// Number of quality columns in [`STATIC_ALLOC`] (RFC 6716 §4.3.3
112/// Table 57, p. 112).
113///
114/// The §4.3.3 RFC text describes the column index as
115/// `q ∈ {0, 1, …, 10}` — eleven columns total. Column `0` is the
116/// §4.3.3 "no allocation" floor (every cell zero); column `10` is
117/// the §4.3.3 saturation column (every cell `200` or close); columns
118/// `1..=9` are the §4.3.3 working quality range the allocator
119/// interpolates over in 1/64-step increments.
120pub const STATIC_ALLOC_Q_COUNT: usize = 11;
121
122/// Minimum value the §4.3.3 quality column index can take
123/// (RFC 6716 §4.3.3 Table 57, p. 112: `q = 0`).
124pub const STATIC_ALLOC_Q_MIN: u32 = 0;
125
126/// Maximum value the §4.3.3 quality column index can take
127/// (RFC 6716 §4.3.3 Table 57, p. 112: `q = 10`).
128pub const STATIC_ALLOC_Q_MAX: u32 = 10;
129
130/// §4.3.3 unit-conversion shift offset (RFC 6716 §4.3.3, p. 111,
131/// `<< LM >> 2`).
132///
133/// The §4.3.3 conversion `channels * N * alloc[band][q] << LM >> 2`
134/// applies a net `LM − 2` shift to fold the Q5 (1/32-bit per MDCT bin)
135/// table value into Q3 (1/8-bit) per-band units. We expose the `>> 2`
136/// half as a named constant; the `<< LM` half is data-dependent.
137pub const STATIC_ALLOC_RIGHT_SHIFT: u32 = 2;
138
139/// §4.3.3 interpolation step denominator (RFC 6716 §4.3.3, p. 111:
140/// "in steps of 1/64").
141///
142/// The §4.3.3 1/64-step interpolation between adjacent quality columns
143/// keeps the search finer-grained than the output unit (1/8 bit). The
144/// orchestrated allocator consumer multiplies by this in the Q11
145/// arithmetic before the `<< LM >> 2` step folds the result back to
146/// Q3.
147pub const STATIC_ALLOC_INTERP_STEPS: u32 = 64;
148
149/// Total cells in [`STATIC_ALLOC`]: 21 bands × 11 quality columns =
150/// 231 entries.
151pub const STATIC_ALLOC_TOTAL_CELLS: usize = CELT_NUM_BANDS * STATIC_ALLOC_Q_COUNT;
152
153/// §4.3.3 `alloc[][]` static allocation table (RFC 6716 §4.3.3
154/// Table 57, p. 112).
155///
156/// 21-row × 11-column grid. Rows index the §4.3 Table 55 band index
157/// `b ∈ 0..=20`; columns index the §4.3.3 quality parameter
158/// `q ∈ 0..=10`. Every cell is a Q5 value in 1/32-bit per MDCT bin
159/// units. Linear indexing into the flattened array is
160/// `i = band * STATIC_ALLOC_Q_COUNT + q`.
161///
162/// The §4.3.3 search converts each cell to a per-band bit allocation
163/// in 1/8 bits via
164/// `(channels * N * STATIC_ALLOC[band][q]) << LM >> 2`, then linearly
165/// interpolates between two adjacent columns in steps of 1/64 to find
166/// the highest allocation that does not exceed the working budget.
167///
168/// Layout invariants the §4.3.3 narrative imposes and that
169/// [`STATIC_ALLOC`] honours:
170///
171/// * Column `0` is uniformly zero (the §4.3.3 "no allocation" floor).
172/// * Each row is monotone non-decreasing in `q` (a higher quality
173///   column never reduces the allocation).
174/// * Column `10` is `200` for the first 12 rows (bands `0..=11`) and
175///   declines monotonically thereafter as the higher bands consume
176///   more raw bits per cell at saturation.
177///
178/// Numeric provenance: RFC 6716 §4.3.3 Table 57 (p. 112), held in-repo
179/// at `docs/audio/opus/rfc6716-opus.txt`.
180#[rustfmt::skip]
181pub const STATIC_ALLOC: [[u8; STATIC_ALLOC_Q_COUNT]; CELT_NUM_BANDS] = [
182    // band 0
183    [0, 90, 110, 118, 126, 134, 144, 152, 162, 172, 200],
184    // band 1
185    [0, 80, 100, 110, 119, 127, 137, 145, 155, 165, 200],
186    // band 2
187    [0, 75,  90, 103, 112, 120, 130, 138, 148, 158, 200],
188    // band 3
189    [0, 69,  84,  93, 104, 114, 124, 132, 142, 152, 200],
190    // band 4
191    [0, 63,  78,  86,  95, 103, 113, 123, 133, 143, 200],
192    // band 5
193    [0, 56,  71,  80,  89,  97, 107, 117, 127, 137, 200],
194    // band 6
195    [0, 49,  65,  75,  83,  91, 101, 111, 121, 131, 200],
196    // band 7
197    [0, 40,  58,  70,  78,  85,  95, 105, 115, 125, 200],
198    // band 8
199    [0, 34,  51,  65,  72,  78,  88,  98, 108, 118, 198],
200    // band 9
201    [0, 29,  45,  59,  66,  72,  82,  92, 102, 112, 193],
202    // band 10
203    [0, 20,  39,  53,  60,  66,  76,  86,  96, 106, 188],
204    // band 11
205    [0, 18,  32,  47,  54,  60,  70,  80,  90, 100, 183],
206    // band 12
207    [0, 10,  26,  40,  47,  54,  64,  74,  84,  94, 178],
208    // band 13
209    [0,  0,  20,  31,  39,  47,  57,  67,  77,  87, 173],
210    // band 14
211    [0,  0,  12,  23,  32,  41,  51,  61,  71,  81, 168],
212    // band 15
213    [0,  0,   0,  15,  25,  35,  45,  55,  65,  75, 163],
214    // band 16
215    [0,  0,   0,   4,  17,  29,  39,  49,  59,  69, 158],
216    // band 17
217    [0,  0,   0,   0,  12,  23,  33,  43,  53,  63, 153],
218    // band 18
219    [0,  0,   0,   0,   1,  16,  26,  36,  46,  56, 148],
220    // band 19
221    [0,  0,   0,   0,   0,  10,  15,  20,  30,  45, 129],
222    // band 20
223    [0,  0,   0,   0,   0,   1,   1,   1,   1,  20, 104],
224];
225
226/// Errors returned by the [`STATIC_ALLOC`] accessors when their
227/// `(band, q)` inputs sit outside the §4.3.3 Table 57 grid.
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum StaticAllocError {
230    /// `band` is outside `0..21` — the §4.3 Table 55 band count.
231    BandOutOfRange { band: u32 },
232    /// `q` is outside `0..11` — the §4.3.3 quality column range.
233    QualityOutOfRange { q: u32 },
234    /// `channels` is outside `1..=2` — RFC 6716 §4.3.3 (p. 111)
235    /// declares `channels ∈ {1, 2}` for the unit conversion.
236    ChannelsOutOfRange { channels: u32 },
237    /// `lm` is outside `0..4` — RFC 6716 §4.3 only defines four CELT
238    /// frame sizes (`LM ∈ {0, 1, 2, 3}`).
239    LmOutOfRange { lm: u32 },
240}
241
242/// Look up the raw Q5 (1/32-bit per MDCT bin) cell of [`STATIC_ALLOC`]
243/// for one `(band, q)` pair (RFC 6716 §4.3.3 Table 57, p. 112).
244///
245/// This is the §4.3.3 step "look up `alloc[band][q]`" — the lookup
246/// itself, before the `(channels * N) << LM >> 2` unit conversion.
247/// Callers who only need the raw Q5 byte (e.g. cross-checks of the
248/// table layout) use this; callers who need the per-band bit
249/// allocation in 1/8 bits call [`static_alloc_eighth_bits`].
250pub fn static_alloc_cell(band: u32, q: u32) -> Result<u8, StaticAllocError> {
251    if band >= CELT_NUM_BANDS as u32 {
252        return Err(StaticAllocError::BandOutOfRange { band });
253    }
254    if q >= STATIC_ALLOC_Q_COUNT as u32 {
255        return Err(StaticAllocError::QualityOutOfRange { q });
256    }
257    Ok(STATIC_ALLOC[band as usize][q as usize])
258}
259
260/// Borrow the full 11-cell row of [`STATIC_ALLOC`] for one `band`
261/// (RFC 6716 §4.3.3 Table 57, p. 112).
262///
263/// Useful when the §4.3.3 search iterates the quality columns for one
264/// band without re-indexing per call (the natural inner-loop shape of
265/// the 1/64-step interpolation).
266pub fn static_alloc_row(
267    band: u32,
268) -> Result<&'static [u8; STATIC_ALLOC_Q_COUNT], StaticAllocError> {
269    if band >= CELT_NUM_BANDS as u32 {
270        return Err(StaticAllocError::BandOutOfRange { band });
271    }
272    Ok(&STATIC_ALLOC[band as usize])
273}
274
275/// Apply the §4.3.3 `channels * N * alloc[band][q] << LM >> 2`
276/// conversion to a single Q5 cell (RFC 6716 §4.3.3, p. 111).
277///
278/// Returns the per-band shape allocation in 1/8 bits — the same units
279/// every other §4.3.3 budget quantity uses (compatible with the
280/// round-34 [`crate::celt_reservations::ReservationOutcome::total_remaining_eighth_bits`]
281/// working budget, the round-35
282/// [`crate::celt_band_thresh::band_min_thresh`] floor, the round-36
283/// [`crate::celt_trim_offsets::band_trim_offset`] tilt bias, and the
284/// round-31 [`crate::celt_cache_caps50::cap_for_band_bits`] cap at
285/// the §4.3.3 allocator's consumer site).
286///
287/// `channels ∈ {1, 2}` is the frame's channel count, `n_bins` is the
288/// §4.3 Table 55 per-channel MDCT-bin count for the band at the
289/// frame's LM, and `lm ∈ {0, 1, 2, 3}` is the §4.3 frame-size scale.
290///
291/// The conversion is performed in `u32` to keep intermediate
292/// arithmetic well-defined: the largest cell value `200` times the
293/// largest reachable `(channels * N) = (2 * 176)` times `(1 << 3) =
294/// 8` is `563_200`, comfortably inside `u32` headroom. The `>> 2`
295/// step is the §4.3.3 final unit-folding from Q5 to Q3.
296pub fn static_alloc_eighth_bits(
297    band: u32,
298    q: u32,
299    channels: u32,
300    n_bins: u32,
301    lm: u32,
302) -> Result<u32, StaticAllocError> {
303    if channels == 0 || channels > 2 {
304        return Err(StaticAllocError::ChannelsOutOfRange { channels });
305    }
306    if lm >= 4 {
307        return Err(StaticAllocError::LmOutOfRange { lm });
308    }
309    let cell = static_alloc_cell(band, q)? as u32;
310    // Per RFC 6716 §4.3.3 p. 111: channels * N * alloc[band][q] << LM >> 2.
311    // u32 arithmetic; all intermediate products fit.
312    let scaled = channels * n_bins * cell;
313    Ok((scaled << lm) >> STATIC_ALLOC_RIGHT_SHIFT)
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::celt_band_layout::{celt_band_bins_per_channel, CeltFrameSize};
320
321    // ---- Table-shape invariants ----
322
323    #[test]
324    fn table_shape_constants_match_struct() {
325        assert_eq!(STATIC_ALLOC_Q_COUNT, 11);
326        assert_eq!(STATIC_ALLOC_Q_MIN, 0);
327        assert_eq!(STATIC_ALLOC_Q_MAX, 10);
328        assert_eq!(STATIC_ALLOC_TOTAL_CELLS, 231);
329        assert_eq!(CELT_NUM_BANDS, 21);
330        assert_eq!(STATIC_ALLOC.len(), CELT_NUM_BANDS);
331        for row in STATIC_ALLOC.iter() {
332            assert_eq!(row.len(), STATIC_ALLOC_Q_COUNT);
333        }
334    }
335
336    #[test]
337    fn unit_conversion_constants_match_rfc() {
338        assert_eq!(STATIC_ALLOC_RIGHT_SHIFT, 2);
339        assert_eq!(STATIC_ALLOC_INTERP_STEPS, 64);
340    }
341
342    // ---- Spot-check Q5 cells against RFC 6716 §4.3.3 Table 57 ----
343    //
344    // These pins reproduce hand-picked rows / corners from
345    // `docs/audio/opus/rfc6716-opus.txt` pp. 112 so a future edit that
346    // reorders / drops / typos an entry trips the suite.
347
348    #[test]
349    fn corner_top_left_is_zero() {
350        // §4.3.3 Table 57: band 0, q 0 = 0 (no-allocation floor).
351        assert_eq!(STATIC_ALLOC[0][0], 0);
352        assert_eq!(static_alloc_cell(0, 0).unwrap(), 0);
353    }
354
355    #[test]
356    fn corner_top_right_is_two_hundred() {
357        // §4.3.3 Table 57: band 0, q 10 = 200 (saturation column).
358        assert_eq!(STATIC_ALLOC[0][10], 200);
359        assert_eq!(static_alloc_cell(0, 10).unwrap(), 200);
360    }
361
362    #[test]
363    fn corner_bottom_left_is_zero() {
364        // §4.3.3 Table 57: band 20, q 0 = 0 (every column-0 cell is
365        // the no-allocation floor).
366        assert_eq!(STATIC_ALLOC[20][0], 0);
367        assert_eq!(static_alloc_cell(20, 0).unwrap(), 0);
368    }
369
370    #[test]
371    fn corner_bottom_right_is_one_hundred_four() {
372        // §4.3.3 Table 57: band 20, q 10 = 104. The saturation column
373        // declines from 200 at the low bands to 104 at band 20.
374        assert_eq!(STATIC_ALLOC[20][10], 104);
375        assert_eq!(static_alloc_cell(20, 10).unwrap(), 104);
376    }
377
378    #[test]
379    fn band_0_q_1_is_ninety() {
380        // §4.3.3 Table 57: band 0, q 1 = 90 — the first non-zero entry
381        // of the first row.
382        assert_eq!(STATIC_ALLOC[0][1], 90);
383    }
384
385    #[test]
386    fn band_8_q_10_is_one_hundred_ninety_eight() {
387        // §4.3.3 Table 57: band 8, q 10 = 198 — the first row where
388        // the saturation column drops below 200.
389        assert_eq!(STATIC_ALLOC[8][10], 198);
390    }
391
392    #[test]
393    fn band_13_q_1_is_zero() {
394        // §4.3.3 Table 57: band 13, q 1 = 0 — the first row whose
395        // q = 1 entry is still in the no-allocation regime.
396        assert_eq!(STATIC_ALLOC[13][1], 0);
397        // q = 2 picks up.
398        assert_eq!(STATIC_ALLOC[13][2], 20);
399    }
400
401    #[test]
402    fn band_20_low_q_columns_pin_to_one() {
403        // §4.3.3 Table 57: band 20, q 5..=8 = 1 — the highest band
404        // sits at 1 across four consecutive working columns before
405        // ramping to q = 10's 104.
406        assert_eq!(STATIC_ALLOC[20][5], 1);
407        assert_eq!(STATIC_ALLOC[20][6], 1);
408        assert_eq!(STATIC_ALLOC[20][7], 1);
409        assert_eq!(STATIC_ALLOC[20][8], 1);
410        assert_eq!(STATIC_ALLOC[20][9], 20);
411    }
412
413    // ---- Row monotonicity invariant ----
414    //
415    // The §4.3.3 narrative phrases the quality columns as a ladder.
416    // Each row must be monotone non-decreasing in q — a higher quality
417    // column may never reduce the allocation for the same band.
418
419    #[test]
420    fn every_row_is_monotone_non_decreasing_in_q() {
421        for (band, row) in STATIC_ALLOC.iter().enumerate() {
422            for w in row.windows(2) {
423                assert!(
424                    w[0] <= w[1],
425                    "STATIC_ALLOC[{band}] not monotone non-decreasing at pair {w:?}",
426                );
427            }
428        }
429    }
430
431    #[test]
432    fn column_zero_is_uniformly_zero() {
433        for (band, row) in STATIC_ALLOC.iter().enumerate() {
434            assert_eq!(row[0], 0, "STATIC_ALLOC[{band}][0] != 0");
435        }
436    }
437
438    #[test]
439    fn column_ten_top_twelve_bands_are_two_hundred() {
440        // §4.3.3 Table 57: bands 0..=7 saturation column is 200; band
441        // 8 drops to 198. Pin the band-7 / band-8 boundary so a
442        // future edit can't push the drop into a different row.
443        for (band, row) in STATIC_ALLOC.iter().enumerate().take(8) {
444            assert_eq!(
445                row[10], 200,
446                "STATIC_ALLOC[{band}][10] expected 200 at saturation"
447            );
448        }
449        assert_eq!(STATIC_ALLOC[8][10], 198);
450    }
451
452    // ---- Accessor parity ----
453
454    #[test]
455    fn cell_accessor_matches_array_indexing() {
456        for band in 0..(CELT_NUM_BANDS as u32) {
457            for q in 0..(STATIC_ALLOC_Q_COUNT as u32) {
458                assert_eq!(
459                    static_alloc_cell(band, q).unwrap(),
460                    STATIC_ALLOC[band as usize][q as usize],
461                );
462            }
463        }
464    }
465
466    #[test]
467    fn row_accessor_borrows_full_row() {
468        for band in 0..(CELT_NUM_BANDS as u32) {
469            let row = static_alloc_row(band).unwrap();
470            assert_eq!(row.len(), STATIC_ALLOC_Q_COUNT);
471            assert_eq!(row, &STATIC_ALLOC[band as usize]);
472        }
473    }
474
475    // ---- Out-of-range guards ----
476
477    #[test]
478    fn cell_rejects_band_out_of_range() {
479        assert_eq!(
480            static_alloc_cell(21, 0).unwrap_err(),
481            StaticAllocError::BandOutOfRange { band: 21 },
482        );
483        assert_eq!(
484            static_alloc_cell(u32::MAX, 0).unwrap_err(),
485            StaticAllocError::BandOutOfRange { band: u32::MAX },
486        );
487    }
488
489    #[test]
490    fn cell_rejects_quality_out_of_range() {
491        assert_eq!(
492            static_alloc_cell(0, 11).unwrap_err(),
493            StaticAllocError::QualityOutOfRange { q: 11 },
494        );
495        assert_eq!(
496            static_alloc_cell(0, u32::MAX).unwrap_err(),
497            StaticAllocError::QualityOutOfRange { q: u32::MAX },
498        );
499    }
500
501    #[test]
502    fn row_rejects_band_out_of_range() {
503        assert_eq!(
504            static_alloc_row(21).unwrap_err(),
505            StaticAllocError::BandOutOfRange { band: 21 },
506        );
507    }
508
509    // ---- §4.3.3 unit conversion ----
510    //
511    // Pin the `channels * N * cell << LM >> 2` arithmetic with worked
512    // examples that the §4.3.3 narrative can be hand-traced through.
513
514    #[test]
515    fn unit_conversion_q_zero_is_zero() {
516        // §4.3.3 column 0 is the no-allocation floor; any conversion
517        // through it must yield 0 regardless of channels / n_bins / LM.
518        for band in 0..(CELT_NUM_BANDS as u32) {
519            for &channels in &[1u32, 2] {
520                for &n_bins in &[1u32, 4, 88, 176] {
521                    for lm in 0..4 {
522                        assert_eq!(
523                            static_alloc_eighth_bits(band, 0, channels, n_bins, lm).unwrap(),
524                            0,
525                            "band {band} q 0 channels {channels} n_bins {n_bins} lm {lm}",
526                        );
527                    }
528                }
529            }
530        }
531    }
532
533    #[test]
534    fn unit_conversion_worked_example_lm_zero() {
535        // §4.3.3 cell band 0 q 1 = 90 (Q5 bits/bin). Convert with
536        // channels = 1, n_bins = 1, lm = 0:
537        // (1 * 1 * 90) << 0 >> 2 = 90 / 4 = 22 (integer division).
538        assert_eq!(static_alloc_eighth_bits(0, 1, 1, 1, 0).unwrap(), 22);
539
540        // Same cell with channels = 2 (stereo): scales by 2.
541        // (2 * 1 * 90) >> 2 = 45.
542        assert_eq!(static_alloc_eighth_bits(0, 1, 2, 1, 0).unwrap(), 45);
543
544        // Same cell with n_bins = 4 (band-loop natural width):
545        // (1 * 4 * 90) >> 2 = 90.
546        assert_eq!(static_alloc_eighth_bits(0, 1, 1, 4, 0).unwrap(), 90);
547    }
548
549    #[test]
550    fn unit_conversion_worked_example_lm_three() {
551        // §4.3.3 cell band 0 q 10 = 200 with channels = 2, n_bins = 4
552        // (band 0 width at 20 ms is 4 per channel), LM = 3 (20 ms):
553        // (2 * 4 * 200) << 3 >> 2 = 1600 * 8 / 4 = 3200.
554        assert_eq!(static_alloc_eighth_bits(0, 10, 2, 4, 3).unwrap(), 3200);
555    }
556
557    #[test]
558    fn unit_conversion_lm_shift_doubles_per_step() {
559        // The §4.3.3 << LM step multiplies the allocation by 2^LM.
560        // Pin the doubling across the four CELT frame sizes for a
561        // representative cell.
562        let cell = STATIC_ALLOC[5][5] as u32; // 97
563        let channels = 1u32;
564        let n_bins = 4u32;
565        let base = (channels * n_bins * cell) >> 2; // LM = 0 value
566        for lm in 0..4u32 {
567            let got = static_alloc_eighth_bits(5, 5, channels, n_bins, lm).unwrap();
568            assert_eq!(got, base << lm, "LM = {lm} doubling failed");
569        }
570    }
571
572    #[test]
573    fn unit_conversion_against_band_layout_widths() {
574        // Cross-check against the §4.3 Table 55 widths (round 24).
575        // Pin two cells against the round-24 widths so a future edit
576        // that drifts the band layout table trips the suite.
577        let n0_lm3 = celt_band_bins_per_channel(0, CeltFrameSize::Ms20).unwrap() as u32;
578        assert_eq!(n0_lm3, 8);
579        let got = static_alloc_eighth_bits(0, 5, 2, n0_lm3, 3).unwrap();
580        // (2 * 8 * STATIC_ALLOC[0][5]) << 3 >> 2 = (2 * 8 * 134) * 8 / 4 = 4288
581        assert_eq!(got, 4288);
582
583        let n20_lm3 = celt_band_bins_per_channel(20, CeltFrameSize::Ms20).unwrap() as u32;
584        assert_eq!(n20_lm3, 176);
585        let got = static_alloc_eighth_bits(20, 9, 1, n20_lm3, 3).unwrap();
586        // (1 * 176 * STATIC_ALLOC[20][9]) << 3 >> 2 = (176 * 20) * 8 / 4 = 7040
587        assert_eq!(got, 7040);
588    }
589
590    #[test]
591    fn unit_conversion_rejects_channels_out_of_range() {
592        assert_eq!(
593            static_alloc_eighth_bits(0, 1, 0, 1, 0).unwrap_err(),
594            StaticAllocError::ChannelsOutOfRange { channels: 0 },
595        );
596        assert_eq!(
597            static_alloc_eighth_bits(0, 1, 3, 1, 0).unwrap_err(),
598            StaticAllocError::ChannelsOutOfRange { channels: 3 },
599        );
600    }
601
602    #[test]
603    fn unit_conversion_rejects_lm_out_of_range() {
604        assert_eq!(
605            static_alloc_eighth_bits(0, 1, 1, 1, 4).unwrap_err(),
606            StaticAllocError::LmOutOfRange { lm: 4 },
607        );
608        assert_eq!(
609            static_alloc_eighth_bits(0, 1, 1, 1, u32::MAX).unwrap_err(),
610            StaticAllocError::LmOutOfRange { lm: u32::MAX },
611        );
612    }
613
614    #[test]
615    fn unit_conversion_propagates_band_and_quality_errors() {
616        assert_eq!(
617            static_alloc_eighth_bits(21, 0, 1, 1, 0).unwrap_err(),
618            StaticAllocError::BandOutOfRange { band: 21 },
619        );
620        assert_eq!(
621            static_alloc_eighth_bits(0, 11, 1, 1, 0).unwrap_err(),
622            StaticAllocError::QualityOutOfRange { q: 11 },
623        );
624    }
625
626    // ---- Cross-row plausibility ----
627    //
628    // The §4.3.3 narrative doesn't pin a monotonicity across rows, but
629    // the table's structure (low bands carry more of the rate at low
630    // q, high bands shed allocation faster) shows up empirically. Pin
631    // a few worked invariants the §4.3.3 search assumes.
632
633    #[test]
634    fn low_band_dominates_high_band_at_low_q() {
635        // At q = 1, band 0 = 90 vs band 13..=20 = 0. Pin the spread —
636        // a future re-typing that introduced a non-zero into a
637        // higher-band q=1 cell would break the §4.3.3 search's
638        // low-q "concentrate on low bands" assumption.
639        assert_eq!(STATIC_ALLOC[0][1], 90);
640        for (band, row) in STATIC_ALLOC.iter().enumerate().skip(13) {
641            assert_eq!(row[1], 0, "STATIC_ALLOC[{band}][1] expected 0 at low q",);
642        }
643    }
644
645    #[test]
646    fn saturation_column_declines_after_band_seven() {
647        // §4.3.3 Table 57 saturation column: bands 0..=7 = 200,
648        // band 8 = 198, band 20 = 104. Pin the overall decline.
649        assert_eq!(STATIC_ALLOC[7][10], 200);
650        assert_eq!(STATIC_ALLOC[8][10], 198);
651        assert!(
652            STATIC_ALLOC[8][10] < STATIC_ALLOC[7][10],
653            "saturation column failed to decline at band 7→8",
654        );
655        for band in 8..20 {
656            assert!(
657                STATIC_ALLOC[band + 1][10] <= STATIC_ALLOC[band][10],
658                "saturation column non-monotone at band {} → {}",
659                band,
660                band + 1,
661            );
662        }
663        assert_eq!(STATIC_ALLOC[20][10], 104);
664    }
665}