oxideav_opus/silk_frame.rs
1//! SILK per-frame header decoding — RFC 6716 §4.2.7.1 through §4.2.7.5.1.
2//!
3//! Each regular SILK frame begins with a fixed prefix of side-information
4//! symbols that drive the subsequent gain / LSF / LTP / excitation
5//! stages. This module parses that prefix:
6//!
7//! * §4.2.7.1 — Stereo prediction weights (mid-channel of a stereo frame
8//! only). Three range-coded indices `n`, `(i0, i1)`, `(i2, i3)` are
9//! combined into a pair of Q13 prediction weights `(w0_Q13, w1_Q13)`
10//! per the formulas at the end of §4.2.7.1.
11//! * §4.2.7.2 — Mid-only flag (mid-channel of a stereo frame, when the
12//! side channel is not otherwise required).
13//! * §4.2.7.3 — Frame-type symbol, which jointly carries the signal type
14//! ([`SignalType`]) and the quantization-offset type
15//! ([`QuantizationOffsetType`]) per Table 10.
16//! * §4.2.7.5.1 — Normalized LSF stage-1 codebook index `I1` (0..32),
17//! PDF chosen from Table 14 by `(bandwidth, signal_type)`.
18//!
19//! All symbols are read from a [`RangeDecoder`] using the §4.1.3.3
20//! inverse-CDF primitive. The PDFs in Tables 6, 8, 9, and 14 are
21//! transcribed verbatim from RFC 6716.
22//!
23//! Higher-level SILK stages (subframe gains, LSF stage-2 residual, LTP
24//! parameters, LCG seed, excitation) are out of scope for round 4 — the
25//! goal here is to land the entry point onto the SILK frame body and
26//! the four structural decisions that everything downstream branches
27//! on.
28
29use crate::range_decoder::RangeDecoder;
30use crate::toc::Bandwidth;
31use crate::Error;
32
33/// Decoded stereo prediction weights for one mid-channel SILK frame
34/// (RFC 6716 §4.2.7.1).
35///
36/// Both weights are in Q13 fixed-point. By construction
37/// `w0_Q13, w1_Q13 ∈ [-13732 - 0.1*(13732 - 10050), 13732 + ...]`,
38/// i.e. roughly `[-14_100, +14_100]` after the interpolation step.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct StereoPredictionWeights {
41 /// First weight, Q13. The decoded formula:
42 /// `w0 = w_Q13[wi0] + (((w_Q13[wi0+1] - w_Q13[wi0])*6554) >> 16)*(2*i1+1) - w1`.
43 pub w0_q13: i32,
44 /// Second weight, Q13. The decoded formula:
45 /// `w1 = w_Q13[wi1] + (((w_Q13[wi1+1] - w_Q13[wi1])*6554) >> 16)*(2*i3+1)`.
46 pub w1_q13: i32,
47}
48
49/// Signal type carried by the §4.2.7.3 frame-type symbol (Table 10).
50///
51/// Drives downstream LSF stage-1 PDF selection (§4.2.7.5.1) and the
52/// gain MSB PDF (§4.2.7.4).
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum SignalType {
55 /// Frame-type 0 or 1 (Table 10).
56 Inactive,
57 /// Frame-type 2 or 3 (Table 10).
58 Unvoiced,
59 /// Frame-type 4 or 5 (Table 10).
60 Voiced,
61}
62
63/// Quantization-offset type carried by the §4.2.7.3 frame-type symbol
64/// (Table 10).
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum QuantizationOffsetType {
67 /// Even frame-type values (0, 2, 4).
68 Low,
69 /// Odd frame-type values (1, 3, 5).
70 High,
71}
72
73/// Whether the current SILK frame is an LBRR frame or a regular SILK
74/// frame, and the VAD state of the corresponding time interval.
75///
76/// Drives which §4.2.7.3 PDF is used (Table 9) and whether the
77/// mid-only flag (§4.2.7.2) appears.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum FrameKind {
80 /// Regular SILK frame whose VAD flag is unset for this time
81 /// interval. Frame-type symbol uses the "Inactive" PDF in Table 9.
82 /// Decoded value is always 0 or 1.
83 RegularInactive,
84 /// Regular SILK frame whose VAD flag is set for this time
85 /// interval. Frame-type symbol uses the "Active" PDF in Table 9.
86 /// Decoded value lies in `2..=5`.
87 RegularActive,
88 /// LBRR frame. Per §4.2.7.3, LBRR frames also use the "Active"
89 /// PDF, since every LBRR frame is itself an active-coded frame.
90 Lbrr,
91}
92
93/// Configuration for the mid-only flag (§4.2.7.2).
94///
95/// The mid-only flag is present only on a mid-channel SILK frame of a
96/// stereo Opus frame when the corresponding side channel is not
97/// otherwise required. The caller decides whether this applies and
98/// passes the result via [`SilkFrameHeaderConfig::has_mid_only_flag`].
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct SilkFrameHeaderConfig {
101 /// True if this is the mid-channel SILK frame of a stereo Opus
102 /// frame. The stereo prediction weights (§4.2.7.1) are decoded
103 /// only when this is true.
104 pub stereo_mid_channel: bool,
105 /// True if this is a stereo Opus frame at all. When the stereo
106 /// bit of the TOC byte is 0, neither the stereo prediction
107 /// weights nor the mid-only flag is present.
108 pub stereo: bool,
109 /// True if the §4.2.7.2 mid-only flag should be decoded. Per
110 /// §4.2.7.2, this happens when (a) we are on the mid channel of
111 /// a stereo Opus frame, AND (b) the side channel of this time
112 /// interval is not otherwise required (regular frame with side
113 /// VAD == 0, or LBRR frame with side LBRR == 0).
114 pub has_mid_only_flag: bool,
115 /// Frame kind for the current SILK frame (regular vs LBRR; if
116 /// regular, the VAD state). Drives the §4.2.7.3 PDF selection.
117 pub kind: FrameKind,
118 /// Audio bandwidth of the SILK signal. Drives the §4.2.7.5.1 PDF
119 /// selection (NB / MB share a row in Table 14, WB has its own).
120 /// SWB and FB SILK do not exist; the caller passes the SILK-layer
121 /// bandwidth post-§4.2.2 split.
122 pub bandwidth: Bandwidth,
123}
124
125/// SILK frame header — the prefix of side-information that drives the
126/// rest of the SILK frame decoder. RFC 6716 §4.2.7.1 through §4.2.7.5.1.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct SilkFrameHeader {
129 /// Stereo prediction weights (§4.2.7.1) if this is a mid-channel
130 /// SILK frame of a stereo Opus frame; `None` otherwise.
131 pub stereo_pred: Option<StereoPredictionWeights>,
132 /// Mid-only flag (§4.2.7.2): `Some(true)` means the side channel
133 /// of this time interval is skipped; `Some(false)` means the side
134 /// channel is coded normally; `None` means the flag was not
135 /// present.
136 pub mid_only_flag: Option<bool>,
137 /// Raw §4.2.7.3 frame-type symbol value in `0..=5` (per Table 10).
138 pub frame_type: u8,
139 /// Decoded signal type (§4.2.7.3, Table 10).
140 pub signal_type: SignalType,
141 /// Decoded quantization-offset type (§4.2.7.3, Table 10).
142 pub qoff_type: QuantizationOffsetType,
143 /// Normalized LSF stage-1 codebook index `I1` (§4.2.7.5.1), in
144 /// `0..32`.
145 pub lsf_stage1: u8,
146}
147
148/// Table 6 stage-1 PDF (25 symbols) — `silk_stereo_pred_joint_iCDF`
149/// equivalent expressed as an inverse-CDF for the §4.1.3.3 primitive.
150///
151/// The PDF as stated in RFC 6716 Table 6:
152///
153/// ```text
154/// {7, 2, 1, 1, 1, 10, 24, 8, 1, 1, 3, 23, 92, 23, 3, 1, 1,
155/// 8, 24, 10, 1, 1, 1, 2, 7}/256
156/// ```
157///
158/// Cumulative `fh[k]` running sum:
159/// `[7,9,10,11,12,22,46,54,55,56,59,82,174,197,200,201,202,210,234,
160/// 244,245,246,247,249,256]`.
161/// `icdf[k] = 256 - fh[k]`, terminated by 0:
162const STEREO_STAGE1_ICDF: &[u8] = &[
163 249, 247, 246, 245, 244, 234, 210, 202, 201, 200, 197, 174, 82, 59, 56, 55, 54, 46, 22, 12, 11,
164 10, 9, 7, 0,
165];
166
167/// Table 6 stage-2 PDF — `{85, 86, 85}/256`. Cumulative `fh = [85, 171,
168/// 256]`. `icdf = [171, 85, 0]`.
169const STEREO_STAGE2_ICDF: &[u8] = &[171, 85, 0];
170
171/// Table 6 stage-3 PDF — `{51, 51, 52, 51, 51}/256`. Cumulative
172/// `fh = [51, 102, 154, 205, 256]`. `icdf = [205, 154, 102, 51, 0]`.
173const STEREO_STAGE3_ICDF: &[u8] = &[205, 154, 102, 51, 0];
174
175/// Table 7 — 16-entry weight table indexed by `wi0` / `wi1 + 1` in the
176/// `w0_Q13` / `w1_Q13` computation of §4.2.7.1.
177///
178/// Last entry is included even though `wi*` only ranges over 0..=14, so
179/// that the linear interpolation `w_Q13[wi+1] - w_Q13[wi]` is always
180/// defined.
181const STEREO_WEIGHT_Q13: [i32; 16] = [
182 -13732, -10050, -8266, -7526, -6500, -5000, -2950, -820, 820, 2950, 5000, 6500, 7526, 8266,
183 10050, 13732,
184];
185
186/// Table 8 mid-only flag PDF `{192, 64}/256`. Cumulative `fh = [192,
187/// 256]`. `icdf = [64, 0]`.
188const MID_ONLY_ICDF: &[u8] = &[64, 0];
189
190/// Table 9 inactive frame-type PDF `{26, 230, 0, 0, 0, 0}/256` — only
191/// indices 0 and 1 ever decode. Cumulative `fh = [26, 256]`. `icdf =
192/// [230, 0]`.
193const FRAME_TYPE_INACTIVE_ICDF: &[u8] = &[230, 0];
194
195/// Table 9 active frame-type PDF `{0, 0, 24, 74, 148, 10}/256` —
196/// indices 2..=5. Cumulative `fh = [0, 0, 24, 98, 246, 256]`. The
197/// §4.1.3.3 primitive needs the leading zero-mass cells to be
198/// representable; `icdf = [256, 256, 232, 158, 10, 0]` but 256 is not a
199/// valid `u8`. Instead, the §4.1.3.3 formulation handles the
200/// degenerate "this cell has probability zero" case naturally: the
201/// `s * icdf[k]` product equals `rng` when `icdf[k] == ft`, and
202/// `val < rng` always holds — so the search just falls through. We
203/// approximate the leading-256 entries with their wraparound `0u8`
204/// representation, since the §4.1.3.3 primitive compares `val >= next`
205/// where `next = s * icdf[k]`; for `icdf[k] = 0` this gives `next = 0`
206/// and the loop returns at this index. That is the WRONG behaviour
207/// for a leading zero-probability cell. The clean solution is to use
208/// a different `ftb` that excludes the zero-probability cells, which
209/// gives us a tight inverse-CDF: the active PDF "really" has support
210/// over {2, 3, 4, 5}, so transcribe it as a 4-entry table indexed
211/// `0..=3` and add the +2 offset in the caller. Cumulative
212/// `fh = [24, 98, 246, 256]`. `icdf = [232, 158, 10, 0]`.
213const FRAME_TYPE_ACTIVE_ICDF: &[u8] = &[232, 158, 10, 0];
214
215/// Table 14 LSF stage-1 PDF for NB/MB inactive-or-unvoiced. Sum of
216/// the 32 cells is 256 by construction. Build cumulative `fh` then
217/// `icdf = 256 - fh` with a trailing zero.
218const LSF_STAGE1_NB_MB_INACTIVE_PDF: [u8; 32] = [
219 44, 34, 30, 19, 21, 12, 11, 3, 3, 2, 16, 2, 2, 1, 5, 2, 1, 3, 3, 1, 1, 2, 2, 2, 3, 1, 9, 9, 2,
220 7, 2, 1,
221];
222
223/// Table 14 LSF stage-1 PDF for NB/MB voiced.
224const LSF_STAGE1_NB_MB_VOICED_PDF: [u8; 32] = [
225 1, 10, 1, 8, 3, 8, 8, 14, 13, 14, 1, 14, 12, 13, 11, 11, 12, 11, 10, 10, 11, 8, 9, 8, 7, 8, 1,
226 1, 6, 1, 6, 5,
227];
228
229/// Table 14 LSF stage-1 PDF for WB inactive-or-unvoiced.
230const LSF_STAGE1_WB_INACTIVE_PDF: [u8; 32] = [
231 31, 21, 3, 17, 1, 8, 17, 4, 1, 18, 16, 4, 2, 3, 1, 10, 1, 3, 16, 11, 16, 2, 2, 3, 2, 11, 1, 4,
232 9, 8, 7, 3,
233];
234
235/// Table 14 LSF stage-1 PDF for WB voiced.
236const LSF_STAGE1_WB_VOICED_PDF: [u8; 32] = [
237 1, 4, 16, 5, 18, 11, 5, 14, 15, 1, 3, 12, 13, 14, 14, 6, 14, 12, 2, 6, 1, 12, 12, 11, 10, 3,
238 10, 5, 1, 1, 1, 3,
239];
240
241/// Convert a 32-cell length-256 PDF into an iCDF (`256 - fh[k]`) with
242/// a trailing zero — the format consumed by [`RangeDecoder::dec_icdf`].
243const fn pdf_to_icdf32(pdf: &[u8; 32]) -> [u8; 33] {
244 let mut icdf = [0u8; 33];
245 let mut acc: u32 = 0;
246 let mut k = 0;
247 while k < 32 {
248 acc += pdf[k] as u32;
249 // `256 - acc` fits in u8 as long as acc <= 256, which it is
250 // for any well-formed Table-14 row (sum = 256).
251 icdf[k] = (256 - acc) as u8;
252 k += 1;
253 }
254 // trailing zero terminator
255 icdf[32] = 0;
256 icdf
257}
258
259/// LSF stage-1 iCDFs derived from the four PDF rows in Table 14.
260const LSF_STAGE1_ICDF_NB_MB_INACTIVE: [u8; 33] = pdf_to_icdf32(&LSF_STAGE1_NB_MB_INACTIVE_PDF);
261const LSF_STAGE1_ICDF_NB_MB_VOICED: [u8; 33] = pdf_to_icdf32(&LSF_STAGE1_NB_MB_VOICED_PDF);
262const LSF_STAGE1_ICDF_WB_INACTIVE: [u8; 33] = pdf_to_icdf32(&LSF_STAGE1_WB_INACTIVE_PDF);
263const LSF_STAGE1_ICDF_WB_VOICED: [u8; 33] = pdf_to_icdf32(&LSF_STAGE1_WB_VOICED_PDF);
264
265impl SilkFrameHeader {
266 /// Decode the §4.2.7.1–§4.2.7.5.1 header prefix from `rd`.
267 ///
268 /// The caller is responsible for telling us, via `cfg`, whether
269 /// the stereo prediction weights and the mid-only flag are
270 /// present, and whether the current frame is regular-inactive,
271 /// regular-active, or LBRR. The function does not consult the
272 /// §3.1 TOC byte or the §4.2.3/§4.2.4 packet-level header bits.
273 pub fn decode(rd: &mut RangeDecoder<'_>, cfg: SilkFrameHeaderConfig) -> Result<Self, Error> {
274 // -------- §4.2.7.1 Stereo Prediction Weights --------
275 let stereo_pred = if cfg.stereo && cfg.stereo_mid_channel {
276 Some(Self::decode_stereo_pred(rd))
277 } else {
278 None
279 };
280
281 // -------- §4.2.7.2 Mid-Only Flag --------
282 // Per §4.2.7.2 the flag is present iff (stereo Opus frame) AND
283 // (mid channel) AND (side channel not otherwise required). We
284 // gate strictly on the caller's `has_mid_only_flag` to keep
285 // the LBRR / VAD logic out of the SILK frame decoder.
286 let mid_only_flag = if cfg.has_mid_only_flag {
287 // Table 8: P(0) = 192/256 = 3/4, P(1) = 64/256 = 1/4.
288 // dec_icdf returns the symbol index; index 0 => flag = 0,
289 // index 1 => flag = 1 ("mid only").
290 let v = rd.dec_icdf(MID_ONLY_ICDF, 8);
291 Some(v == 1)
292 } else {
293 None
294 };
295
296 // -------- §4.2.7.3 Frame Type --------
297 let frame_type_raw = match cfg.kind {
298 FrameKind::RegularInactive => {
299 // Inactive PDF — only indices 0 and 1 ever decode.
300 rd.dec_icdf(FRAME_TYPE_INACTIVE_ICDF, 8) as u8
301 }
302 FrameKind::RegularActive | FrameKind::Lbrr => {
303 // Active PDF — indices 2..=5. We use a 4-entry iCDF
304 // covering the support and shift by +2.
305 let k = rd.dec_icdf(FRAME_TYPE_ACTIVE_ICDF, 8) as u8;
306 k + 2
307 }
308 };
309 if frame_type_raw > 5 {
310 // Should not happen for well-formed PDFs; defend anyway.
311 return Err(Error::MalformedPacket);
312 }
313 let (signal_type, qoff_type) = frame_type_to_signal_qoff(frame_type_raw);
314
315 // -------- §4.2.7.5.1 LSF Stage-1 --------
316 let lsf_icdf: &[u8] = match (cfg.bandwidth, signal_type) {
317 (Bandwidth::Nb | Bandwidth::Mb, SignalType::Inactive | SignalType::Unvoiced) => {
318 &LSF_STAGE1_ICDF_NB_MB_INACTIVE
319 }
320 (Bandwidth::Nb | Bandwidth::Mb, SignalType::Voiced) => &LSF_STAGE1_ICDF_NB_MB_VOICED,
321 (Bandwidth::Wb, SignalType::Inactive | SignalType::Unvoiced) => {
322 &LSF_STAGE1_ICDF_WB_INACTIVE
323 }
324 (Bandwidth::Wb, SignalType::Voiced) => &LSF_STAGE1_ICDF_WB_VOICED,
325 // §2 — SILK does not operate on SWB or FB. Hybrid mode
326 // splits the signal so that the SILK layer always sees
327 // NB / MB / WB only. Reject anything else.
328 _ => return Err(Error::MalformedPacket),
329 };
330 let lsf_stage1 = rd.dec_icdf(lsf_icdf, 8) as u8;
331 if lsf_stage1 >= 32 {
332 return Err(Error::MalformedPacket);
333 }
334
335 if rd.has_error() {
336 return Err(Error::MalformedPacket);
337 }
338
339 Ok(Self {
340 stereo_pred,
341 mid_only_flag,
342 frame_type: frame_type_raw,
343 signal_type,
344 qoff_type,
345 lsf_stage1,
346 })
347 }
348
349 /// Internal: decode the five sub-symbols of §4.2.7.1 (`n`, `i0`,
350 /// `i1`, `i2`, `i3`) and compose them into `(w0_Q13, w1_Q13)`.
351 ///
352 /// Reads order is exactly the one stated in §4.2.7.1: "let i0
353 /// and i1 be indices decoded with the stage-2 and stage-3 PDFs in
354 /// Table 6, respectively, and let i2 and i3 be two more indices
355 /// decoded with the stage-2 and stage-3 PDFs, all in that order."
356 fn decode_stereo_pred(rd: &mut RangeDecoder<'_>) -> StereoPredictionWeights {
357 let n = rd.dec_icdf(STEREO_STAGE1_ICDF, 8) as i32;
358 let i0 = rd.dec_icdf(STEREO_STAGE2_ICDF, 8) as i32;
359 let i1 = rd.dec_icdf(STEREO_STAGE3_ICDF, 8) as i32;
360 let i2 = rd.dec_icdf(STEREO_STAGE2_ICDF, 8) as i32;
361 let i3 = rd.dec_icdf(STEREO_STAGE3_ICDF, 8) as i32;
362
363 // §4.2.7.1: wi0 = i0 + 3*(n/5), wi1 = i2 + 3*(n%5); both fall
364 // in 0..=14.
365 let wi0 = (i0 + 3 * (n / 5)) as usize;
366 let wi1 = (i2 + 3 * (n % 5)) as usize;
367 // Defensive clamp: the spec guarantees wi* <= 14 for any
368 // (n, i0, i2) tuple, but we still saturate to keep the
369 // STEREO_WEIGHT_Q13[wi+1] lookup in-bounds even on a
370 // pathologically corrupt frame.
371 let wi0 = wi0.min(14);
372 let wi1 = wi1.min(14);
373
374 // w1 first (w0 depends on w1):
375 // w1 = w_Q13[wi1] + (((w_Q13[wi1+1] - w_Q13[wi1])*6554) >> 16)*(2*i3+1)
376 let step1: i32 =
377 (((STEREO_WEIGHT_Q13[wi1 + 1] - STEREO_WEIGHT_Q13[wi1]) * 6554) >> 16) * (2 * i3 + 1);
378 let w1_q13 = STEREO_WEIGHT_Q13[wi1] + step1;
379 // w0 = w_Q13[wi0] + (((w_Q13[wi0+1] - w_Q13[wi0])*6554) >> 16)*(2*i1+1) - w1
380 let step0: i32 =
381 (((STEREO_WEIGHT_Q13[wi0 + 1] - STEREO_WEIGHT_Q13[wi0]) * 6554) >> 16) * (2 * i1 + 1);
382 let w0_q13 = STEREO_WEIGHT_Q13[wi0] + step0 - w1_q13;
383 StereoPredictionWeights { w0_q13, w1_q13 }
384 }
385}
386
387/// Map a frame-type symbol (0..=5) to `(signal_type, qoff_type)` per
388/// RFC 6716 §4.2.7.3 Table 10.
389fn frame_type_to_signal_qoff(frame_type: u8) -> (SignalType, QuantizationOffsetType) {
390 let signal = match frame_type {
391 0 | 1 => SignalType::Inactive,
392 2 | 3 => SignalType::Unvoiced,
393 4 | 5 => SignalType::Voiced,
394 _ => SignalType::Inactive, // unreachable in practice; defensive
395 };
396 let qoff = if frame_type % 2 == 0 {
397 QuantizationOffsetType::Low
398 } else {
399 QuantizationOffsetType::High
400 };
401 (signal, qoff)
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 // --- Table 6 / 7 / 8 / 9 / 14 PDF→iCDF transcription self-checks.
409 //
410 // These tests do not exercise the range decoder; they confirm
411 // the constant tables match the RFC by checking that each PDF row
412 // sums to 256 and that consecutive iCDF cells are strictly
413 // monotonically decreasing (a §4.1.3.3 precondition).
414
415 #[test]
416 fn stereo_stage1_pdf_sums_to_256() {
417 let pdf = [
418 7, 2, 1, 1, 1, 10, 24, 8, 1, 1, 3, 23, 92, 23, 3, 1, 1, 8, 24, 10, 1, 1, 1, 2, 7,
419 ];
420 let sum: u32 = pdf.iter().sum();
421 assert_eq!(sum, 256);
422 assert_eq!(STEREO_STAGE1_ICDF.len(), pdf.len());
423 // iCDF strictly monotone decreasing then terminator zero.
424 for w in STEREO_STAGE1_ICDF.windows(2) {
425 assert!(w[0] > w[1] || (w[0] == 0 && w[1] == 0));
426 }
427 assert_eq!(*STEREO_STAGE1_ICDF.last().unwrap(), 0);
428 }
429
430 #[test]
431 fn stereo_stage2_pdf_self_check() {
432 assert_eq!(STEREO_STAGE2_ICDF, &[171u8, 85, 0]);
433 assert_eq!(STEREO_STAGE3_ICDF, &[205u8, 154, 102, 51, 0]);
434 }
435
436 #[test]
437 fn mid_only_pdf_self_check() {
438 assert_eq!(MID_ONLY_ICDF, &[64u8, 0]);
439 }
440
441 #[test]
442 fn lsf_stage1_nb_mb_inactive_sums_to_256() {
443 let s: u32 = LSF_STAGE1_NB_MB_INACTIVE_PDF
444 .iter()
445 .map(|&x| x as u32)
446 .sum();
447 assert_eq!(s, 256);
448 }
449
450 #[test]
451 fn lsf_stage1_nb_mb_voiced_sums_to_256() {
452 let s: u32 = LSF_STAGE1_NB_MB_VOICED_PDF.iter().map(|&x| x as u32).sum();
453 assert_eq!(s, 256);
454 }
455
456 #[test]
457 fn lsf_stage1_wb_inactive_sums_to_256() {
458 let s: u32 = LSF_STAGE1_WB_INACTIVE_PDF.iter().map(|&x| x as u32).sum();
459 assert_eq!(s, 256);
460 }
461
462 #[test]
463 fn lsf_stage1_wb_voiced_sums_to_256() {
464 let s: u32 = LSF_STAGE1_WB_VOICED_PDF.iter().map(|&x| x as u32).sum();
465 assert_eq!(s, 256);
466 }
467
468 #[test]
469 fn lsf_stage1_icdf_terminator_is_zero() {
470 for icdf in [
471 &LSF_STAGE1_ICDF_NB_MB_INACTIVE,
472 &LSF_STAGE1_ICDF_NB_MB_VOICED,
473 &LSF_STAGE1_ICDF_WB_INACTIVE,
474 &LSF_STAGE1_ICDF_WB_VOICED,
475 ] {
476 assert_eq!(icdf[32], 0, "iCDF must terminate with zero");
477 assert_eq!(icdf.len(), 33);
478 // Strictly decreasing.
479 for w in icdf.windows(2) {
480 assert!(
481 w[0] >= w[1],
482 "iCDF must be monotone non-increasing: {:?} -> {:?}",
483 w[0],
484 w[1]
485 );
486 }
487 }
488 }
489
490 #[test]
491 fn stereo_weight_table_is_symmetric() {
492 // Table 7 is symmetric around the middle: w[15-k] == -w[k]
493 // for k in 0..=7.
494 for k in 0..8 {
495 assert_eq!(STEREO_WEIGHT_Q13[15 - k], -STEREO_WEIGHT_Q13[k]);
496 }
497 assert_eq!(STEREO_WEIGHT_Q13[0], -13732);
498 assert_eq!(STEREO_WEIGHT_Q13[15], 13732);
499 }
500
501 // --- Table 10 frame-type mapping --------
502
503 #[test]
504 fn frame_type_to_signal_qoff_table10() {
505 let expected = [
506 (0, SignalType::Inactive, QuantizationOffsetType::Low),
507 (1, SignalType::Inactive, QuantizationOffsetType::High),
508 (2, SignalType::Unvoiced, QuantizationOffsetType::Low),
509 (3, SignalType::Unvoiced, QuantizationOffsetType::High),
510 (4, SignalType::Voiced, QuantizationOffsetType::Low),
511 (5, SignalType::Voiced, QuantizationOffsetType::High),
512 ];
513 for (ft, sig, q) in expected {
514 assert_eq!(frame_type_to_signal_qoff(ft), (sig, q));
515 }
516 }
517
518 // --- End-to-end: decode against a hand-crafted RangeDecoder.
519 //
520 // We can't easily construct an arbitrary byte sequence that
521 // produces a specific symbol pattern without an encoder, but we
522 // CAN check round-trip behaviour: every decoded value must
523 // satisfy the spec's range bounds, and the function must not
524 // latch the corrupt-frame flag for a non-corrupt input.
525
526 fn mono_inactive_cfg(bw: Bandwidth) -> SilkFrameHeaderConfig {
527 SilkFrameHeaderConfig {
528 stereo_mid_channel: false,
529 stereo: false,
530 has_mid_only_flag: false,
531 kind: FrameKind::RegularInactive,
532 bandwidth: bw,
533 }
534 }
535
536 fn mono_active_cfg(bw: Bandwidth) -> SilkFrameHeaderConfig {
537 SilkFrameHeaderConfig {
538 stereo_mid_channel: false,
539 stereo: false,
540 has_mid_only_flag: false,
541 kind: FrameKind::RegularActive,
542 bandwidth: bw,
543 }
544 }
545
546 fn stereo_mid_active_cfg(bw: Bandwidth) -> SilkFrameHeaderConfig {
547 SilkFrameHeaderConfig {
548 stereo_mid_channel: true,
549 stereo: true,
550 has_mid_only_flag: true,
551 kind: FrameKind::RegularActive,
552 bandwidth: bw,
553 }
554 }
555
556 #[test]
557 fn mono_inactive_nb_decode_basic() {
558 // A long-enough buffer so the range decoder doesn't immediately
559 // start zero-extending past EOF.
560 let buf = [
561 0x55, 0xAA, 0x33, 0xCC, 0x7F, 0x80, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
562 0x12, 0x34,
563 ];
564 let mut rd = RangeDecoder::new(&buf);
565 let hdr = SilkFrameHeader::decode(&mut rd, mono_inactive_cfg(Bandwidth::Nb))
566 .expect("decode must succeed");
567 // No stereo content.
568 assert!(hdr.stereo_pred.is_none());
569 assert!(hdr.mid_only_flag.is_none());
570 // Inactive frame: ft must be 0 or 1.
571 assert!(hdr.frame_type <= 1, "ft={}", hdr.frame_type);
572 assert_eq!(hdr.signal_type, SignalType::Inactive);
573 assert!(hdr.lsf_stage1 < 32);
574 }
575
576 #[test]
577 fn mono_active_wb_decode_basic() {
578 let buf = [
579 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
580 0x77, 0x88,
581 ];
582 let mut rd = RangeDecoder::new(&buf);
583 let hdr = SilkFrameHeader::decode(&mut rd, mono_active_cfg(Bandwidth::Wb))
584 .expect("decode must succeed");
585 assert!(hdr.stereo_pred.is_none());
586 assert!(hdr.mid_only_flag.is_none());
587 // Active frame: ft must be 2, 3, 4, or 5.
588 assert!((2..=5).contains(&hdr.frame_type), "ft={}", hdr.frame_type);
589 assert!(matches!(
590 hdr.signal_type,
591 SignalType::Unvoiced | SignalType::Voiced
592 ));
593 assert!(hdr.lsf_stage1 < 32);
594 }
595
596 #[test]
597 fn stereo_mid_active_includes_pred_and_mid_only() {
598 let buf = [
599 0xC3, 0x18, 0x42, 0x7F, 0x55, 0xAA, 0x33, 0xCC, 0x77, 0x33, 0x11, 0xAA, 0xDE, 0xAD,
600 0xBE, 0xEF, 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE,
601 ];
602 let mut rd = RangeDecoder::new(&buf);
603 let hdr = SilkFrameHeader::decode(&mut rd, stereo_mid_active_cfg(Bandwidth::Mb))
604 .expect("decode must succeed");
605 let pred = hdr.stereo_pred.expect("stereo prediction must be present");
606 // w1 is one interpolated Table-7 entry (~±14_100 after the
607 // §4.2.7.1 interpolation step). w0 then subtracts w1 from
608 // another interpolated entry, so |w0| can reach ~ 28_000.
609 assert!(
610 (-30_000..=30_000).contains(&pred.w0_q13),
611 "w0={}",
612 pred.w0_q13
613 );
614 assert!(
615 (-15_000..=15_000).contains(&pred.w1_q13),
616 "w1={}",
617 pred.w1_q13
618 );
619 assert!(hdr.mid_only_flag.is_some());
620 assert!((2..=5).contains(&hdr.frame_type), "ft={}", hdr.frame_type);
621 assert!(hdr.lsf_stage1 < 32);
622 }
623
624 #[test]
625 fn stereo_side_no_prediction() {
626 // Side-channel SILK frame: NOT mid channel, so no stereo pred
627 // weights and no mid-only flag.
628 let cfg = SilkFrameHeaderConfig {
629 stereo_mid_channel: false,
630 stereo: true,
631 has_mid_only_flag: false,
632 kind: FrameKind::RegularActive,
633 bandwidth: Bandwidth::Wb,
634 };
635 let buf = [
636 0x37, 0x91, 0xC4, 0x18, 0xA2, 0x5D, 0x6E, 0xFF, 0x77, 0x33, 0x11, 0xAA,
637 ];
638 let mut rd = RangeDecoder::new(&buf);
639 let hdr = SilkFrameHeader::decode(&mut rd, cfg).expect("decode must succeed");
640 assert!(hdr.stereo_pred.is_none());
641 assert!(hdr.mid_only_flag.is_none());
642 }
643
644 #[test]
645 fn lbrr_frame_uses_active_pdf() {
646 // LBRR frames decode the frame-type symbol from the "Active"
647 // PDF irrespective of the (regular) VAD state.
648 let cfg = SilkFrameHeaderConfig {
649 stereo_mid_channel: false,
650 stereo: false,
651 has_mid_only_flag: false,
652 kind: FrameKind::Lbrr,
653 bandwidth: Bandwidth::Nb,
654 };
655 let buf = [
656 0x55, 0xAA, 0x33, 0xCC, 0x7F, 0x80, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
657 ];
658 let mut rd = RangeDecoder::new(&buf);
659 let hdr = SilkFrameHeader::decode(&mut rd, cfg).expect("decode must succeed");
660 assert!(
661 (2..=5).contains(&hdr.frame_type),
662 "lbrr ft must be active: {}",
663 hdr.frame_type
664 );
665 }
666
667 #[test]
668 fn pdf_to_icdf_terminates_and_decreases() {
669 // The const helper must produce a strictly-decreasing iCDF
670 // with a trailing zero, for any well-formed length-32
671 // length-256-sum PDF.
672 let pdf = [8u8; 32]; // uniform 32-way: sum = 256.
673 let icdf = pdf_to_icdf32(&pdf);
674 assert_eq!(icdf[32], 0);
675 for w in icdf.windows(2) {
676 assert!(w[0] >= w[1]);
677 }
678 // 256 - 8 = 248 (first cumulative-fh subtraction).
679 assert_eq!(icdf[0], 248);
680 // After all 32 cells: 256 - 32*8 = 0; uniform PDF sums to ft.
681 assert_eq!(icdf[31], 0);
682 }
683
684 #[test]
685 fn stereo_pred_wi_clamped_in_bounds() {
686 // Even in the pathological case where rd.has_error() is set,
687 // the wi0/wi1 clamps in decode_stereo_pred ensure we never
688 // index past STEREO_WEIGHT_Q13[15]. We can't directly inject
689 // n=24, but the clamp `.min(14)` is exercised by inspection;
690 // exercise it indirectly by running many random buffers.
691 for seed in 0..32u8 {
692 let buf = [
693 seed,
694 seed.wrapping_mul(3),
695 seed.wrapping_add(7),
696 seed ^ 0xA5,
697 seed.wrapping_mul(11),
698 seed.wrapping_add(13),
699 seed ^ 0x5A,
700 seed.wrapping_mul(17),
701 seed.wrapping_add(19),
702 seed ^ 0xC3,
703 seed.wrapping_mul(23),
704 seed.wrapping_add(29),
705 seed ^ 0x3C,
706 seed.wrapping_mul(31),
707 seed.wrapping_add(37),
708 seed ^ 0x55,
709 ];
710 let mut rd = RangeDecoder::new(&buf);
711 let pred = SilkFrameHeader::decode_stereo_pred(&mut rd);
712 // w1 is one interpolated table entry (~±14k); w0 is one
713 // interpolated entry MINUS w1 (~±28k worst case).
714 assert!(
715 (-30_000..=30_000).contains(&pred.w0_q13),
716 "seed={seed}, w0={}",
717 pred.w0_q13
718 );
719 assert!(
720 (-15_000..=15_000).contains(&pred.w1_q13),
721 "seed={seed}, w1={}",
722 pred.w1_q13
723 );
724 }
725 }
726}