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}