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::range_encoder::RangeEncoder;
31use crate::toc::Bandwidth;
32use crate::Error;
33
34/// Decoded stereo prediction weights for one mid-channel SILK frame
35/// (RFC 6716 §4.2.7.1).
36///
37/// Both weights are in Q13 fixed-point. By construction
38/// `w0_Q13, w1_Q13 ∈ [-13732 - 0.1*(13732 - 10050), 13732 + ...]`,
39/// i.e. roughly `[-14_100, +14_100]` after the interpolation step.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct StereoPredictionWeights {
42 /// First weight, Q13. The decoded formula:
43 /// `w0 = w_Q13[wi0] + (((w_Q13[wi0+1] - w_Q13[wi0])*6554) >> 16)*(2*i1+1) - w1`.
44 pub w0_q13: i32,
45 /// Second weight, Q13. The decoded formula:
46 /// `w1 = w_Q13[wi1] + (((w_Q13[wi1+1] - w_Q13[wi1])*6554) >> 16)*(2*i3+1)`.
47 pub w1_q13: i32,
48}
49
50/// Signal type carried by the §4.2.7.3 frame-type symbol (Table 10).
51///
52/// Drives downstream LSF stage-1 PDF selection (§4.2.7.5.1) and the
53/// gain MSB PDF (§4.2.7.4).
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum SignalType {
56 /// Frame-type 0 or 1 (Table 10).
57 Inactive,
58 /// Frame-type 2 or 3 (Table 10).
59 Unvoiced,
60 /// Frame-type 4 or 5 (Table 10).
61 Voiced,
62}
63
64/// Quantization-offset type carried by the §4.2.7.3 frame-type symbol
65/// (Table 10).
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum QuantizationOffsetType {
68 /// Even frame-type values (0, 2, 4).
69 Low,
70 /// Odd frame-type values (1, 3, 5).
71 High,
72}
73
74/// Whether the current SILK frame is an LBRR frame or a regular SILK
75/// frame, and the VAD state of the corresponding time interval.
76///
77/// Drives which §4.2.7.3 PDF is used (Table 9) and whether the
78/// mid-only flag (§4.2.7.2) appears.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum FrameKind {
81 /// Regular SILK frame whose VAD flag is unset for this time
82 /// interval. Frame-type symbol uses the "Inactive" PDF in Table 9.
83 /// Decoded value is always 0 or 1.
84 RegularInactive,
85 /// Regular SILK frame whose VAD flag is set for this time
86 /// interval. Frame-type symbol uses the "Active" PDF in Table 9.
87 /// Decoded value lies in `2..=5`.
88 RegularActive,
89 /// LBRR frame. Per §4.2.7.3, LBRR frames also use the "Active"
90 /// PDF, since every LBRR frame is itself an active-coded frame.
91 Lbrr,
92}
93
94/// Configuration for the mid-only flag (§4.2.7.2).
95///
96/// The mid-only flag is present only on a mid-channel SILK frame of a
97/// stereo Opus frame when the corresponding side channel is not
98/// otherwise required. The caller decides whether this applies and
99/// passes the result via [`SilkFrameHeaderConfig::has_mid_only_flag`].
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct SilkFrameHeaderConfig {
102 /// True if this is the mid-channel SILK frame of a stereo Opus
103 /// frame. The stereo prediction weights (§4.2.7.1) are decoded
104 /// only when this is true.
105 pub stereo_mid_channel: bool,
106 /// True if this is a stereo Opus frame at all. When the stereo
107 /// bit of the TOC byte is 0, neither the stereo prediction
108 /// weights nor the mid-only flag is present.
109 pub stereo: bool,
110 /// True if the §4.2.7.2 mid-only flag should be decoded. Per
111 /// §4.2.7.2, this happens when (a) we are on the mid channel of
112 /// a stereo Opus frame, AND (b) the side channel of this time
113 /// interval is not otherwise required (regular frame with side
114 /// VAD == 0, or LBRR frame with side LBRR == 0).
115 pub has_mid_only_flag: bool,
116 /// Frame kind for the current SILK frame (regular vs LBRR; if
117 /// regular, the VAD state). Drives the §4.2.7.3 PDF selection.
118 pub kind: FrameKind,
119 /// Audio bandwidth of the SILK signal. Drives the §4.2.7.5.1 PDF
120 /// selection (NB / MB share a row in Table 14, WB has its own).
121 /// SWB and FB SILK do not exist; the caller passes the SILK-layer
122 /// bandwidth post-§4.2.2 split.
123 pub bandwidth: Bandwidth,
124}
125
126/// SILK frame header — the prefix of side-information that drives the
127/// rest of the SILK frame decoder. RFC 6716 §4.2.7.1 through §4.2.7.5.1.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct SilkFrameHeader {
130 /// Stereo prediction weights (§4.2.7.1) if this is a mid-channel
131 /// SILK frame of a stereo Opus frame; `None` otherwise.
132 pub stereo_pred: Option<StereoPredictionWeights>,
133 /// Mid-only flag (§4.2.7.2): `Some(true)` means the side channel
134 /// of this time interval is skipped; `Some(false)` means the side
135 /// channel is coded normally; `None` means the flag was not
136 /// present.
137 pub mid_only_flag: Option<bool>,
138 /// Raw §4.2.7.3 frame-type symbol value in `0..=5` (per Table 10).
139 pub frame_type: u8,
140 /// Decoded signal type (§4.2.7.3, Table 10).
141 pub signal_type: SignalType,
142 /// Decoded quantization-offset type (§4.2.7.3, Table 10).
143 pub qoff_type: QuantizationOffsetType,
144 /// Normalized LSF stage-1 codebook index `I1` (§4.2.7.5.1), in
145 /// `0..32`.
146 pub lsf_stage1: u8,
147}
148
149/// Table 6 stage-1 PDF (25 symbols) — `silk_stereo_pred_joint_iCDF`
150/// equivalent expressed as an inverse-CDF for the §4.1.3.3 primitive.
151///
152/// The PDF as stated in RFC 6716 Table 6:
153///
154/// ```text
155/// {7, 2, 1, 1, 1, 10, 24, 8, 1, 1, 3, 23, 92, 23, 3, 1, 1,
156/// 8, 24, 10, 1, 1, 1, 2, 7}/256
157/// ```
158///
159/// Cumulative `fh[k]` running sum:
160/// `[7,9,10,11,12,22,46,54,55,56,59,82,174,197,200,201,202,210,234,
161/// 244,245,246,247,249,256]`.
162/// `icdf[k] = 256 - fh[k]`, terminated by 0:
163const STEREO_STAGE1_ICDF: &[u8] = &[
164 249, 247, 246, 245, 244, 234, 210, 202, 201, 200, 197, 174, 82, 59, 56, 55, 54, 46, 22, 12, 11,
165 10, 9, 7, 0,
166];
167
168/// Table 6 stage-2 PDF — `{85, 86, 85}/256`. Cumulative `fh = [85, 171,
169/// 256]`. `icdf = [171, 85, 0]`.
170const STEREO_STAGE2_ICDF: &[u8] = &[171, 85, 0];
171
172/// Table 6 stage-3 PDF — `{51, 51, 52, 51, 51}/256`. Cumulative
173/// `fh = [51, 102, 154, 205, 256]`. `icdf = [205, 154, 102, 51, 0]`.
174const STEREO_STAGE3_ICDF: &[u8] = &[205, 154, 102, 51, 0];
175
176/// Table 7 — 16-entry weight table indexed by `wi0` / `wi1 + 1` in the
177/// `w0_Q13` / `w1_Q13` computation of §4.2.7.1.
178///
179/// Last entry is included even though `wi*` only ranges over 0..=14, so
180/// that the linear interpolation `w_Q13[wi+1] - w_Q13[wi]` is always
181/// defined.
182const STEREO_WEIGHT_Q13: [i32; 16] = [
183 -13732, -10050, -8266, -7526, -6500, -5000, -2950, -820, 820, 2950, 5000, 6500, 7526, 8266,
184 10050, 13732,
185];
186
187/// Table 8 mid-only flag PDF `{192, 64}/256`. Cumulative `fh = [192,
188/// 256]`. `icdf = [64, 0]`.
189const MID_ONLY_ICDF: &[u8] = &[64, 0];
190
191/// Table 9 inactive frame-type PDF `{26, 230, 0, 0, 0, 0}/256` — only
192/// indices 0 and 1 ever decode. Cumulative `fh = [26, 256]`. `icdf =
193/// [230, 0]`.
194const FRAME_TYPE_INACTIVE_ICDF: &[u8] = &[230, 0];
195
196/// Table 9 active frame-type PDF `{0, 0, 24, 74, 148, 10}/256` —
197/// indices 2..=5. Cumulative `fh = [0, 0, 24, 98, 246, 256]`. The
198/// §4.1.3.3 primitive needs the leading zero-mass cells to be
199/// representable; `icdf = [256, 256, 232, 158, 10, 0]` but 256 is not a
200/// valid `u8`. Instead, the §4.1.3.3 formulation handles the
201/// degenerate "this cell has probability zero" case naturally: the
202/// `s * icdf[k]` product equals `rng` when `icdf[k] == ft`, and
203/// `val < rng` always holds — so the search just falls through. We
204/// approximate the leading-256 entries with their wraparound `0u8`
205/// representation, since the §4.1.3.3 primitive compares `val >= next`
206/// where `next = s * icdf[k]`; for `icdf[k] = 0` this gives `next = 0`
207/// and the loop returns at this index. That is the WRONG behaviour
208/// for a leading zero-probability cell. The clean solution is to use
209/// a different `ftb` that excludes the zero-probability cells, which
210/// gives us a tight inverse-CDF: the active PDF "really" has support
211/// over {2, 3, 4, 5}, so transcribe it as a 4-entry table indexed
212/// `0..=3` and add the +2 offset in the caller. Cumulative
213/// `fh = [24, 98, 246, 256]`. `icdf = [232, 158, 10, 0]`.
214const FRAME_TYPE_ACTIVE_ICDF: &[u8] = &[232, 158, 10, 0];
215
216/// Table 14 LSF stage-1 PDF for NB/MB inactive-or-unvoiced. Sum of
217/// the 32 cells is 256 by construction. Build cumulative `fh` then
218/// `icdf = 256 - fh` with a trailing zero.
219const LSF_STAGE1_NB_MB_INACTIVE_PDF: [u8; 32] = [
220 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,
221 7, 2, 1,
222];
223
224/// Table 14 LSF stage-1 PDF for NB/MB voiced.
225const LSF_STAGE1_NB_MB_VOICED_PDF: [u8; 32] = [
226 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,
227 1, 6, 1, 6, 5,
228];
229
230/// Table 14 LSF stage-1 PDF for WB inactive-or-unvoiced.
231const LSF_STAGE1_WB_INACTIVE_PDF: [u8; 32] = [
232 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,
233 9, 8, 7, 3,
234];
235
236/// Table 14 LSF stage-1 PDF for WB voiced.
237const LSF_STAGE1_WB_VOICED_PDF: [u8; 32] = [
238 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,
239 10, 5, 1, 1, 1, 3,
240];
241
242/// Convert a 32-cell length-256 PDF into an iCDF (`256 - fh[k]`) with
243/// a trailing zero — the format consumed by [`RangeDecoder::dec_icdf`].
244const fn pdf_to_icdf32(pdf: &[u8; 32]) -> [u8; 33] {
245 let mut icdf = [0u8; 33];
246 let mut acc: u32 = 0;
247 let mut k = 0;
248 while k < 32 {
249 acc += pdf[k] as u32;
250 // `256 - acc` fits in u8 as long as acc <= 256, which it is
251 // for any well-formed Table-14 row (sum = 256).
252 icdf[k] = (256 - acc) as u8;
253 k += 1;
254 }
255 // trailing zero terminator
256 icdf[32] = 0;
257 icdf
258}
259
260/// LSF stage-1 iCDFs derived from the four PDF rows in Table 14.
261const LSF_STAGE1_ICDF_NB_MB_INACTIVE: [u8; 33] = pdf_to_icdf32(&LSF_STAGE1_NB_MB_INACTIVE_PDF);
262const LSF_STAGE1_ICDF_NB_MB_VOICED: [u8; 33] = pdf_to_icdf32(&LSF_STAGE1_NB_MB_VOICED_PDF);
263const LSF_STAGE1_ICDF_WB_INACTIVE: [u8; 33] = pdf_to_icdf32(&LSF_STAGE1_WB_INACTIVE_PDF);
264const LSF_STAGE1_ICDF_WB_VOICED: [u8; 33] = pdf_to_icdf32(&LSF_STAGE1_WB_VOICED_PDF);
265
266/// The §4.2.7.1–§4.2.7.3 header symbols that precede the §4.2.7.4
267/// subframe gains in the Table-5 read order, plus the derived signal /
268/// quantization-offset type.
269///
270/// Returned by [`SilkFrameHeader::decode_pre_gains`]; the caller reads
271/// the §4.2.7.4 gains next, then the §4.2.7.5.1 LSF stage-1 index via
272/// [`SilkFrameHeader::decode_lsf_stage1`].
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub struct SilkHeaderPreGains {
275 /// §4.2.7.1 stereo prediction weights (mid channel of a stereo Opus
276 /// frame); `None` otherwise.
277 pub stereo_pred: Option<StereoPredictionWeights>,
278 /// §4.2.7.2 mid-only flag; `None` when not present.
279 pub mid_only_flag: Option<bool>,
280 /// Raw §4.2.7.3 frame-type symbol in `0..=5`.
281 pub frame_type: u8,
282 /// Decoded signal type (§4.2.7.3, Table 10).
283 pub signal_type: SignalType,
284 /// Decoded quantization-offset type (§4.2.7.3, Table 10).
285 pub qoff_type: QuantizationOffsetType,
286}
287
288impl SilkFrameHeader {
289 /// Decode the §4.2.7.1–§4.2.7.5.1 header prefix from `rd`.
290 ///
291 /// The caller is responsible for telling us, via `cfg`, whether
292 /// the stereo prediction weights and the mid-only flag are
293 /// present, and whether the current frame is regular-inactive,
294 /// regular-active, or LBRR. The function does not consult the
295 /// §3.1 TOC byte or the §4.2.3/§4.2.4 packet-level header bits.
296 /// Decode the §4.2.7.1–§4.2.7.5.1 header fields as a single unit.
297 ///
298 /// **Note on read order:** this convenience entry reads the LSF
299 /// stage-1 index (§4.2.7.5.1) immediately after the frame type
300 /// (§4.2.7.3), i.e. it does *not* leave room for the §4.2.7.4
301 /// subframe gains that Table 5 places between them. It is therefore
302 /// only correct on a bitstream that has no gains symbol, and exists
303 /// as a header-field utility / test helper. A full SILK frame decode
304 /// must instead use the Table-5-ordered composable entries
305 /// [`Self::decode_pre_gains`] (stereo weights + mid-only + frame
306 /// type) and [`Self::decode_lsf_stage1`] (the §4.2.7.5.1 index),
307 /// reading the §4.2.7.4 gains in between — see
308 /// [`crate::silk_decode`].
309 pub fn decode(rd: &mut RangeDecoder<'_>, cfg: SilkFrameHeaderConfig) -> Result<Self, Error> {
310 let pre = Self::decode_pre_gains(rd, cfg)?;
311 let lsf_stage1 = Self::decode_lsf_stage1(rd, cfg.bandwidth, pre.signal_type)?;
312
313 if rd.has_error() {
314 return Err(Error::MalformedPacket);
315 }
316
317 Ok(Self {
318 stereo_pred: pre.stereo_pred,
319 mid_only_flag: pre.mid_only_flag,
320 frame_type: pre.frame_type,
321 signal_type: pre.signal_type,
322 qoff_type: pre.qoff_type,
323 lsf_stage1,
324 })
325 }
326
327 /// Decode the §4.2.7.1 stereo prediction weights, §4.2.7.2 mid-only
328 /// flag, and §4.2.7.3 frame type — every header symbol that precedes
329 /// the §4.2.7.4 subframe gains in the Table-5 read order.
330 ///
331 /// The returned [`SilkHeaderPreGains`] carries the decoded fields plus
332 /// the derived `(signal_type, qoff_type)` needed to choose the gains
333 /// PDF and the §4.2.7.5.1 LSF stage-1 PDF. The caller reads the gains
334 /// next, then calls [`Self::decode_lsf_stage1`].
335 pub fn decode_pre_gains(
336 rd: &mut RangeDecoder<'_>,
337 cfg: SilkFrameHeaderConfig,
338 ) -> Result<SilkHeaderPreGains, Error> {
339 // -------- §4.2.7.1 Stereo Prediction Weights --------
340 let stereo_pred = if cfg.stereo && cfg.stereo_mid_channel {
341 Some(Self::decode_stereo_pred(rd))
342 } else {
343 None
344 };
345
346 // -------- §4.2.7.2 Mid-Only Flag --------
347 // Per §4.2.7.2 the flag is present iff (stereo Opus frame) AND
348 // (mid channel) AND (side channel not otherwise required). We
349 // gate strictly on the caller's `has_mid_only_flag` to keep
350 // the LBRR / VAD logic out of the SILK frame decoder.
351 let mid_only_flag = if cfg.has_mid_only_flag {
352 // Table 8: P(0) = 192/256 = 3/4, P(1) = 64/256 = 1/4.
353 // dec_icdf returns the symbol index; index 0 => flag = 0,
354 // index 1 => flag = 1 ("mid only").
355 let v = rd.dec_icdf(MID_ONLY_ICDF, 8);
356 Some(v == 1)
357 } else {
358 None
359 };
360
361 // -------- §4.2.7.3 Frame Type --------
362 let frame_type_raw = match cfg.kind {
363 FrameKind::RegularInactive => {
364 // Inactive PDF — only indices 0 and 1 ever decode.
365 rd.dec_icdf(FRAME_TYPE_INACTIVE_ICDF, 8) as u8
366 }
367 FrameKind::RegularActive | FrameKind::Lbrr => {
368 // Active PDF — indices 2..=5. We use a 4-entry iCDF
369 // covering the support and shift by +2.
370 let k = rd.dec_icdf(FRAME_TYPE_ACTIVE_ICDF, 8) as u8;
371 k + 2
372 }
373 };
374 if frame_type_raw > 5 {
375 // Should not happen for well-formed PDFs; defend anyway.
376 return Err(Error::MalformedPacket);
377 }
378 let (signal_type, qoff_type) = frame_type_to_signal_qoff(frame_type_raw);
379
380 Ok(SilkHeaderPreGains {
381 stereo_pred,
382 mid_only_flag,
383 frame_type: frame_type_raw,
384 signal_type,
385 qoff_type,
386 })
387 }
388
389 /// Decode the §4.2.7.5.1 normalized LSF stage-1 index `I1 ∈ 0..32`.
390 ///
391 /// The PDF is selected by `(bandwidth, signal_type)` per Table 14;
392 /// `signal_type` comes from [`Self::decode_pre_gains`]. In the
393 /// Table-5 read order this symbol follows the §4.2.7.4 subframe
394 /// gains.
395 pub fn decode_lsf_stage1(
396 rd: &mut RangeDecoder<'_>,
397 bandwidth: Bandwidth,
398 signal_type: SignalType,
399 ) -> Result<u8, Error> {
400 let lsf_icdf: &[u8] = match (bandwidth, signal_type) {
401 (Bandwidth::Nb | Bandwidth::Mb, SignalType::Inactive | SignalType::Unvoiced) => {
402 &LSF_STAGE1_ICDF_NB_MB_INACTIVE
403 }
404 (Bandwidth::Nb | Bandwidth::Mb, SignalType::Voiced) => &LSF_STAGE1_ICDF_NB_MB_VOICED,
405 (Bandwidth::Wb, SignalType::Inactive | SignalType::Unvoiced) => {
406 &LSF_STAGE1_ICDF_WB_INACTIVE
407 }
408 (Bandwidth::Wb, SignalType::Voiced) => &LSF_STAGE1_ICDF_WB_VOICED,
409 // §2 — SILK does not operate on SWB or FB. Hybrid mode
410 // splits the signal so that the SILK layer always sees
411 // NB / MB / WB only. Reject anything else.
412 _ => return Err(Error::MalformedPacket),
413 };
414 let lsf_stage1 = rd.dec_icdf(lsf_icdf, 8) as u8;
415 if lsf_stage1 >= 32 {
416 return Err(Error::MalformedPacket);
417 }
418 Ok(lsf_stage1)
419 }
420
421 /// Internal: decode the five sub-symbols of §4.2.7.1 (`n`, `i0`,
422 /// `i1`, `i2`, `i3`) and compose them into `(w0_Q13, w1_Q13)`.
423 ///
424 /// Reads order is exactly the one stated in §4.2.7.1: "let i0
425 /// and i1 be indices decoded with the stage-2 and stage-3 PDFs in
426 /// Table 6, respectively, and let i2 and i3 be two more indices
427 /// decoded with the stage-2 and stage-3 PDFs, all in that order."
428 fn decode_stereo_pred(rd: &mut RangeDecoder<'_>) -> StereoPredictionWeights {
429 let n = rd.dec_icdf(STEREO_STAGE1_ICDF, 8) as u8;
430 let i0 = rd.dec_icdf(STEREO_STAGE2_ICDF, 8) as u8;
431 let i1 = rd.dec_icdf(STEREO_STAGE3_ICDF, 8) as u8;
432 let i2 = rd.dec_icdf(STEREO_STAGE2_ICDF, 8) as u8;
433 let i3 = rd.dec_icdf(STEREO_STAGE3_ICDF, 8) as u8;
434 StereoWeightSymbols { n, i0, i1, i2, i3 }.weights()
435 }
436
437 // ----- §4.2.7.1–§4.2.7.5.1 encode-side mirrors ------------------
438
439 /// Encode the §4.2.7.1 stereo prediction weights, §4.2.7.2 mid-only
440 /// flag, and §4.2.7.3 frame type — the exact write-side mirror of
441 /// [`Self::decode_pre_gains`].
442 ///
443 /// `symbols` supplies the raw symbol choices; `cfg` must describe
444 /// the same conditions the decoder will use, and the two must be
445 /// consistent (stereo weight symbols present iff `cfg.stereo &&
446 /// cfg.stereo_mid_channel`, a mid-only flag present iff
447 /// `cfg.has_mid_only_flag`, and a frame type inside the support of
448 /// the `cfg.kind`-selected Table 9 PDF). Returns the
449 /// [`SilkHeaderPreGains`] the decoder will reconstruct.
450 pub fn encode_pre_gains(
451 re: &mut RangeEncoder,
452 cfg: SilkFrameHeaderConfig,
453 symbols: &SilkHeaderSymbols,
454 ) -> Result<SilkHeaderPreGains, Error> {
455 // -------- §4.2.7.1 Stereo Prediction Weights --------
456 let want_stereo = cfg.stereo && cfg.stereo_mid_channel;
457 if want_stereo != symbols.stereo.is_some() {
458 return Err(Error::MalformedPacket);
459 }
460 let stereo_pred = match &symbols.stereo {
461 Some(s) => {
462 if s.n > 24 || s.i0 > 2 || s.i1 > 4 || s.i2 > 2 || s.i3 > 4 {
463 return Err(Error::MalformedPacket);
464 }
465 re.enc_icdf(s.n as usize, STEREO_STAGE1_ICDF, 8);
466 re.enc_icdf(s.i0 as usize, STEREO_STAGE2_ICDF, 8);
467 re.enc_icdf(s.i1 as usize, STEREO_STAGE3_ICDF, 8);
468 re.enc_icdf(s.i2 as usize, STEREO_STAGE2_ICDF, 8);
469 re.enc_icdf(s.i3 as usize, STEREO_STAGE3_ICDF, 8);
470 Some(s.weights())
471 }
472 None => None,
473 };
474
475 // -------- §4.2.7.2 Mid-Only Flag --------
476 if cfg.has_mid_only_flag != symbols.mid_only_flag.is_some() {
477 return Err(Error::MalformedPacket);
478 }
479 if let Some(flag) = symbols.mid_only_flag {
480 re.enc_icdf(flag as usize, MID_ONLY_ICDF, 8);
481 }
482
483 // -------- §4.2.7.3 Frame Type --------
484 match cfg.kind {
485 FrameKind::RegularInactive => {
486 if symbols.frame_type > 1 {
487 return Err(Error::MalformedPacket);
488 }
489 re.enc_icdf(symbols.frame_type as usize, FRAME_TYPE_INACTIVE_ICDF, 8);
490 }
491 FrameKind::RegularActive | FrameKind::Lbrr => {
492 if !(2..=5).contains(&symbols.frame_type) {
493 return Err(Error::MalformedPacket);
494 }
495 re.enc_icdf((symbols.frame_type - 2) as usize, FRAME_TYPE_ACTIVE_ICDF, 8);
496 }
497 }
498 let (signal_type, qoff_type) = frame_type_to_signal_qoff(symbols.frame_type);
499
500 Ok(SilkHeaderPreGains {
501 stereo_pred,
502 mid_only_flag: symbols.mid_only_flag,
503 frame_type: symbols.frame_type,
504 signal_type,
505 qoff_type,
506 })
507 }
508
509 /// Encode the §4.2.7.5.1 normalized LSF stage-1 index `I1 ∈ 0..32`
510 /// — the write-side mirror of [`Self::decode_lsf_stage1`]. The PDF
511 /// is selected by `(bandwidth, signal_type)` per Table 14.
512 pub fn encode_lsf_stage1(
513 re: &mut RangeEncoder,
514 bandwidth: Bandwidth,
515 signal_type: SignalType,
516 lsf_stage1: u8,
517 ) -> Result<(), Error> {
518 if lsf_stage1 >= 32 {
519 return Err(Error::MalformedPacket);
520 }
521 let lsf_icdf: &[u8] = match (bandwidth, signal_type) {
522 (Bandwidth::Nb | Bandwidth::Mb, SignalType::Inactive | SignalType::Unvoiced) => {
523 &LSF_STAGE1_ICDF_NB_MB_INACTIVE
524 }
525 (Bandwidth::Nb | Bandwidth::Mb, SignalType::Voiced) => &LSF_STAGE1_ICDF_NB_MB_VOICED,
526 (Bandwidth::Wb, SignalType::Inactive | SignalType::Unvoiced) => {
527 &LSF_STAGE1_ICDF_WB_INACTIVE
528 }
529 (Bandwidth::Wb, SignalType::Voiced) => &LSF_STAGE1_ICDF_WB_VOICED,
530 _ => return Err(Error::MalformedPacket),
531 };
532 re.enc_icdf(lsf_stage1 as usize, lsf_icdf, 8);
533 Ok(())
534 }
535}
536
537/// The raw §4.2.7.1 stereo-weight symbol quintuple `(n, i0, i1, i2,
538/// i3)` — the five Table-6 indices that jointly select the Q13
539/// prediction-weight pair. Used by the encode-side
540/// [`SilkFrameHeader::encode_pre_gains`].
541#[derive(Debug, Clone, Copy, PartialEq, Eq)]
542pub struct StereoWeightSymbols {
543 /// Stage-1 joint index, `0..=24`.
544 pub n: u8,
545 /// First stage-2 index, `0..=2`.
546 pub i0: u8,
547 /// First stage-3 index, `0..=4`.
548 pub i1: u8,
549 /// Second stage-2 index, `0..=2`.
550 pub i2: u8,
551 /// Second stage-3 index, `0..=4`.
552 pub i3: u8,
553}
554
555impl StereoWeightSymbols {
556 /// Compose the §4.2.7.1 weight pair from the five indices — the
557 /// normative reconstruction shared by the decode and encode paths.
558 pub fn weights(&self) -> StereoPredictionWeights {
559 let n = self.n as i32;
560 let i1 = self.i1 as i32;
561 let i3 = self.i3 as i32;
562 // §4.2.7.1: wi0 = i0 + 3*(n/5), wi1 = i2 + 3*(n%5); both fall
563 // in 0..=14.
564 let wi0 = (self.i0 as i32 + 3 * (n / 5)) as usize;
565 let wi1 = (self.i2 as i32 + 3 * (n % 5)) as usize;
566 // Defensive clamp: the spec guarantees wi* <= 14 for any
567 // (n, i0, i2) tuple, but we still saturate to keep the
568 // STEREO_WEIGHT_Q13[wi+1] lookup in-bounds even on a
569 // pathologically corrupt frame.
570 let wi0 = wi0.min(14);
571 let wi1 = wi1.min(14);
572
573 // w1 first (w0 depends on w1):
574 // w1 = w_Q13[wi1] + (((w_Q13[wi1+1] - w_Q13[wi1])*6554) >> 16)*(2*i3+1)
575 let step1: i32 =
576 (((STEREO_WEIGHT_Q13[wi1 + 1] - STEREO_WEIGHT_Q13[wi1]) * 6554) >> 16) * (2 * i3 + 1);
577 let w1_q13 = STEREO_WEIGHT_Q13[wi1] + step1;
578 // w0 = w_Q13[wi0] + (((w_Q13[wi0+1] - w_Q13[wi0])*6554) >> 16)*(2*i1+1) - w1
579 let step0: i32 =
580 (((STEREO_WEIGHT_Q13[wi0 + 1] - STEREO_WEIGHT_Q13[wi0]) * 6554) >> 16) * (2 * i1 + 1);
581 let w0_q13 = STEREO_WEIGHT_Q13[wi0] + step0 - w1_q13;
582 StereoPredictionWeights { w0_q13, w1_q13 }
583 }
584
585 /// Quantize a target §4.2.7.1 weight pair to the nearest coded
586 /// quintuple — the deterministic write-side inverse of
587 /// [`Self::weights`].
588 ///
589 /// The §4.2.7.1 codebook is small (25 stage-1 × 3×5 × 3×5 stage-2/3
590 /// index combinations = 5625 quintuples), so this is an exhaustive
591 /// argmin of the squared Q13 error `(w0 - t0)² + (w1 - t1)²` over
592 /// every quintuple, evaluated through the shared normative
593 /// reconstruction — no approximation, no reliance on any structure
594 /// beyond what [`Self::weights`] itself defines. Ties keep the
595 /// first candidate in `(n, i0, i1, i2, i3)` lexicographic order, so
596 /// the result is fully deterministic.
597 ///
598 /// The achieved pair is available as `quantize(t).weights()`; a
599 /// target that is exactly representable (any output of
600 /// [`Self::weights`]) reconstructs value-exactly.
601 pub fn quantize(target: StereoPredictionWeights) -> StereoWeightSymbols {
602 let mut best = StereoWeightSymbols {
603 n: 0,
604 i0: 0,
605 i1: 0,
606 i2: 0,
607 i3: 0,
608 };
609 let mut best_err = i64::MAX;
610 for n in 0..=24u8 {
611 for i0 in 0..=2u8 {
612 for i1 in 0..=4u8 {
613 for i2 in 0..=2u8 {
614 for i3 in 0..=4u8 {
615 let cand = StereoWeightSymbols { n, i0, i1, i2, i3 };
616 let w = cand.weights();
617 let e0 = (w.w0_q13 - target.w0_q13) as i64;
618 let e1 = (w.w1_q13 - target.w1_q13) as i64;
619 let err = e0 * e0 + e1 * e1;
620 if err < best_err {
621 best_err = err;
622 best = cand;
623 }
624 }
625 }
626 }
627 }
628 }
629 best
630 }
631}
632
633/// The raw pre-gains header symbol choices consumed by the encode-side
634/// [`SilkFrameHeader::encode_pre_gains`] — the §4.2.7.1 stereo-weight
635/// quintuple (mid channel of a stereo frame only), the §4.2.7.2
636/// mid-only flag (when signalled), and the §4.2.7.3 frame-type symbol.
637#[derive(Debug, Clone, Copy, PartialEq, Eq)]
638pub struct SilkHeaderSymbols {
639 /// §4.2.7.1 stereo-weight indices; must be `Some` iff the config
640 /// has `stereo && stereo_mid_channel`.
641 pub stereo: Option<StereoWeightSymbols>,
642 /// §4.2.7.2 mid-only flag; must be `Some` iff the config has
643 /// `has_mid_only_flag`.
644 pub mid_only_flag: Option<bool>,
645 /// §4.2.7.3 frame-type symbol in `0..=5`; `0..=1` for
646 /// [`FrameKind::RegularInactive`], `2..=5` for
647 /// [`FrameKind::RegularActive`] / [`FrameKind::Lbrr`].
648 pub frame_type: u8,
649}
650
651/// Map a frame-type symbol (0..=5) to `(signal_type, qoff_type)` per
652/// RFC 6716 §4.2.7.3 Table 10.
653fn frame_type_to_signal_qoff(frame_type: u8) -> (SignalType, QuantizationOffsetType) {
654 let signal = match frame_type {
655 0 | 1 => SignalType::Inactive,
656 2 | 3 => SignalType::Unvoiced,
657 4 | 5 => SignalType::Voiced,
658 _ => SignalType::Inactive, // unreachable in practice; defensive
659 };
660 let qoff = if frame_type % 2 == 0 {
661 QuantizationOffsetType::Low
662 } else {
663 QuantizationOffsetType::High
664 };
665 (signal, qoff)
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 // --- Table 6 / 7 / 8 / 9 / 14 PDF→iCDF transcription self-checks.
673 //
674 // These tests do not exercise the range decoder; they confirm
675 // the constant tables match the RFC by checking that each PDF row
676 // sums to 256 and that consecutive iCDF cells are strictly
677 // monotonically decreasing (a §4.1.3.3 precondition).
678
679 #[test]
680 fn stereo_stage1_pdf_sums_to_256() {
681 let pdf = [
682 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,
683 ];
684 let sum: u32 = pdf.iter().sum();
685 assert_eq!(sum, 256);
686 assert_eq!(STEREO_STAGE1_ICDF.len(), pdf.len());
687 // iCDF strictly monotone decreasing then terminator zero.
688 for w in STEREO_STAGE1_ICDF.windows(2) {
689 assert!(w[0] > w[1] || (w[0] == 0 && w[1] == 0));
690 }
691 assert_eq!(*STEREO_STAGE1_ICDF.last().unwrap(), 0);
692 }
693
694 #[test]
695 fn stereo_stage2_pdf_self_check() {
696 assert_eq!(STEREO_STAGE2_ICDF, &[171u8, 85, 0]);
697 assert_eq!(STEREO_STAGE3_ICDF, &[205u8, 154, 102, 51, 0]);
698 }
699
700 #[test]
701 fn mid_only_pdf_self_check() {
702 assert_eq!(MID_ONLY_ICDF, &[64u8, 0]);
703 }
704
705 #[test]
706 fn lsf_stage1_nb_mb_inactive_sums_to_256() {
707 let s: u32 = LSF_STAGE1_NB_MB_INACTIVE_PDF
708 .iter()
709 .map(|&x| x as u32)
710 .sum();
711 assert_eq!(s, 256);
712 }
713
714 #[test]
715 fn lsf_stage1_nb_mb_voiced_sums_to_256() {
716 let s: u32 = LSF_STAGE1_NB_MB_VOICED_PDF.iter().map(|&x| x as u32).sum();
717 assert_eq!(s, 256);
718 }
719
720 #[test]
721 fn lsf_stage1_wb_inactive_sums_to_256() {
722 let s: u32 = LSF_STAGE1_WB_INACTIVE_PDF.iter().map(|&x| x as u32).sum();
723 assert_eq!(s, 256);
724 }
725
726 #[test]
727 fn lsf_stage1_wb_voiced_sums_to_256() {
728 let s: u32 = LSF_STAGE1_WB_VOICED_PDF.iter().map(|&x| x as u32).sum();
729 assert_eq!(s, 256);
730 }
731
732 #[test]
733 fn lsf_stage1_icdf_terminator_is_zero() {
734 for icdf in [
735 &LSF_STAGE1_ICDF_NB_MB_INACTIVE,
736 &LSF_STAGE1_ICDF_NB_MB_VOICED,
737 &LSF_STAGE1_ICDF_WB_INACTIVE,
738 &LSF_STAGE1_ICDF_WB_VOICED,
739 ] {
740 assert_eq!(icdf[32], 0, "iCDF must terminate with zero");
741 assert_eq!(icdf.len(), 33);
742 // Strictly decreasing.
743 for w in icdf.windows(2) {
744 assert!(
745 w[0] >= w[1],
746 "iCDF must be monotone non-increasing: {:?} -> {:?}",
747 w[0],
748 w[1]
749 );
750 }
751 }
752 }
753
754 #[test]
755 fn stereo_weight_table_is_symmetric() {
756 // Table 7 is symmetric around the middle: w[15-k] == -w[k]
757 // for k in 0..=7.
758 for k in 0..8 {
759 assert_eq!(STEREO_WEIGHT_Q13[15 - k], -STEREO_WEIGHT_Q13[k]);
760 }
761 assert_eq!(STEREO_WEIGHT_Q13[0], -13732);
762 assert_eq!(STEREO_WEIGHT_Q13[15], 13732);
763 }
764
765 // --- Table 10 frame-type mapping --------
766
767 #[test]
768 fn frame_type_to_signal_qoff_table10() {
769 let expected = [
770 (0, SignalType::Inactive, QuantizationOffsetType::Low),
771 (1, SignalType::Inactive, QuantizationOffsetType::High),
772 (2, SignalType::Unvoiced, QuantizationOffsetType::Low),
773 (3, SignalType::Unvoiced, QuantizationOffsetType::High),
774 (4, SignalType::Voiced, QuantizationOffsetType::Low),
775 (5, SignalType::Voiced, QuantizationOffsetType::High),
776 ];
777 for (ft, sig, q) in expected {
778 assert_eq!(frame_type_to_signal_qoff(ft), (sig, q));
779 }
780 }
781
782 // --- End-to-end: decode against a hand-crafted RangeDecoder.
783 //
784 // We can't easily construct an arbitrary byte sequence that
785 // produces a specific symbol pattern without an encoder, but we
786 // CAN check round-trip behaviour: every decoded value must
787 // satisfy the spec's range bounds, and the function must not
788 // latch the corrupt-frame flag for a non-corrupt input.
789
790 fn mono_inactive_cfg(bw: Bandwidth) -> SilkFrameHeaderConfig {
791 SilkFrameHeaderConfig {
792 stereo_mid_channel: false,
793 stereo: false,
794 has_mid_only_flag: false,
795 kind: FrameKind::RegularInactive,
796 bandwidth: bw,
797 }
798 }
799
800 fn mono_active_cfg(bw: Bandwidth) -> SilkFrameHeaderConfig {
801 SilkFrameHeaderConfig {
802 stereo_mid_channel: false,
803 stereo: false,
804 has_mid_only_flag: false,
805 kind: FrameKind::RegularActive,
806 bandwidth: bw,
807 }
808 }
809
810 fn stereo_mid_active_cfg(bw: Bandwidth) -> SilkFrameHeaderConfig {
811 SilkFrameHeaderConfig {
812 stereo_mid_channel: true,
813 stereo: true,
814 has_mid_only_flag: true,
815 kind: FrameKind::RegularActive,
816 bandwidth: bw,
817 }
818 }
819
820 #[test]
821 fn mono_inactive_nb_decode_basic() {
822 // A long-enough buffer so the range decoder doesn't immediately
823 // start zero-extending past EOF.
824 let buf = [
825 0x55, 0xAA, 0x33, 0xCC, 0x7F, 0x80, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
826 0x12, 0x34,
827 ];
828 let mut rd = RangeDecoder::new(&buf);
829 let hdr = SilkFrameHeader::decode(&mut rd, mono_inactive_cfg(Bandwidth::Nb))
830 .expect("decode must succeed");
831 // No stereo content.
832 assert!(hdr.stereo_pred.is_none());
833 assert!(hdr.mid_only_flag.is_none());
834 // Inactive frame: ft must be 0 or 1.
835 assert!(hdr.frame_type <= 1, "ft={}", hdr.frame_type);
836 assert_eq!(hdr.signal_type, SignalType::Inactive);
837 assert!(hdr.lsf_stage1 < 32);
838 }
839
840 #[test]
841 fn mono_active_wb_decode_basic() {
842 let buf = [
843 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
844 0x77, 0x88,
845 ];
846 let mut rd = RangeDecoder::new(&buf);
847 let hdr = SilkFrameHeader::decode(&mut rd, mono_active_cfg(Bandwidth::Wb))
848 .expect("decode must succeed");
849 assert!(hdr.stereo_pred.is_none());
850 assert!(hdr.mid_only_flag.is_none());
851 // Active frame: ft must be 2, 3, 4, or 5.
852 assert!((2..=5).contains(&hdr.frame_type), "ft={}", hdr.frame_type);
853 assert!(matches!(
854 hdr.signal_type,
855 SignalType::Unvoiced | SignalType::Voiced
856 ));
857 assert!(hdr.lsf_stage1 < 32);
858 }
859
860 #[test]
861 fn stereo_mid_active_includes_pred_and_mid_only() {
862 let buf = [
863 0xC3, 0x18, 0x42, 0x7F, 0x55, 0xAA, 0x33, 0xCC, 0x77, 0x33, 0x11, 0xAA, 0xDE, 0xAD,
864 0xBE, 0xEF, 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE,
865 ];
866 let mut rd = RangeDecoder::new(&buf);
867 let hdr = SilkFrameHeader::decode(&mut rd, stereo_mid_active_cfg(Bandwidth::Mb))
868 .expect("decode must succeed");
869 let pred = hdr.stereo_pred.expect("stereo prediction must be present");
870 // w1 is one interpolated Table-7 entry (~±14_100 after the
871 // §4.2.7.1 interpolation step). w0 then subtracts w1 from
872 // another interpolated entry, so |w0| can reach ~ 28_000.
873 assert!(
874 (-30_000..=30_000).contains(&pred.w0_q13),
875 "w0={}",
876 pred.w0_q13
877 );
878 assert!(
879 (-15_000..=15_000).contains(&pred.w1_q13),
880 "w1={}",
881 pred.w1_q13
882 );
883 assert!(hdr.mid_only_flag.is_some());
884 assert!((2..=5).contains(&hdr.frame_type), "ft={}", hdr.frame_type);
885 assert!(hdr.lsf_stage1 < 32);
886 }
887
888 #[test]
889 fn stereo_side_no_prediction() {
890 // Side-channel SILK frame: NOT mid channel, so no stereo pred
891 // weights and no mid-only flag.
892 let cfg = SilkFrameHeaderConfig {
893 stereo_mid_channel: false,
894 stereo: true,
895 has_mid_only_flag: false,
896 kind: FrameKind::RegularActive,
897 bandwidth: Bandwidth::Wb,
898 };
899 let buf = [
900 0x37, 0x91, 0xC4, 0x18, 0xA2, 0x5D, 0x6E, 0xFF, 0x77, 0x33, 0x11, 0xAA,
901 ];
902 let mut rd = RangeDecoder::new(&buf);
903 let hdr = SilkFrameHeader::decode(&mut rd, cfg).expect("decode must succeed");
904 assert!(hdr.stereo_pred.is_none());
905 assert!(hdr.mid_only_flag.is_none());
906 }
907
908 #[test]
909 fn lbrr_frame_uses_active_pdf() {
910 // LBRR frames decode the frame-type symbol from the "Active"
911 // PDF irrespective of the (regular) VAD state.
912 let cfg = SilkFrameHeaderConfig {
913 stereo_mid_channel: false,
914 stereo: false,
915 has_mid_only_flag: false,
916 kind: FrameKind::Lbrr,
917 bandwidth: Bandwidth::Nb,
918 };
919 let buf = [
920 0x55, 0xAA, 0x33, 0xCC, 0x7F, 0x80, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
921 ];
922 let mut rd = RangeDecoder::new(&buf);
923 let hdr = SilkFrameHeader::decode(&mut rd, cfg).expect("decode must succeed");
924 assert!(
925 (2..=5).contains(&hdr.frame_type),
926 "lbrr ft must be active: {}",
927 hdr.frame_type
928 );
929 }
930
931 #[test]
932 fn pdf_to_icdf_terminates_and_decreases() {
933 // The const helper must produce a strictly-decreasing iCDF
934 // with a trailing zero, for any well-formed length-32
935 // length-256-sum PDF.
936 let pdf = [8u8; 32]; // uniform 32-way: sum = 256.
937 let icdf = pdf_to_icdf32(&pdf);
938 assert_eq!(icdf[32], 0);
939 for w in icdf.windows(2) {
940 assert!(w[0] >= w[1]);
941 }
942 // 256 - 8 = 248 (first cumulative-fh subtraction).
943 assert_eq!(icdf[0], 248);
944 // After all 32 cells: 256 - 32*8 = 0; uniform PDF sums to ft.
945 assert_eq!(icdf[31], 0);
946 }
947
948 #[test]
949 fn stereo_pred_wi_clamped_in_bounds() {
950 // Even in the pathological case where rd.has_error() is set,
951 // the wi0/wi1 clamps in decode_stereo_pred ensure we never
952 // index past STEREO_WEIGHT_Q13[15]. We can't directly inject
953 // n=24, but the clamp `.min(14)` is exercised by inspection;
954 // exercise it indirectly by running many random buffers.
955 for seed in 0..32u8 {
956 let buf = [
957 seed,
958 seed.wrapping_mul(3),
959 seed.wrapping_add(7),
960 seed ^ 0xA5,
961 seed.wrapping_mul(11),
962 seed.wrapping_add(13),
963 seed ^ 0x5A,
964 seed.wrapping_mul(17),
965 seed.wrapping_add(19),
966 seed ^ 0xC3,
967 seed.wrapping_mul(23),
968 seed.wrapping_add(29),
969 seed ^ 0x3C,
970 seed.wrapping_mul(31),
971 seed.wrapping_add(37),
972 seed ^ 0x55,
973 ];
974 let mut rd = RangeDecoder::new(&buf);
975 let pred = SilkFrameHeader::decode_stereo_pred(&mut rd);
976 // w1 is one interpolated table entry (~±14k); w0 is one
977 // interpolated entry MINUS w1 (~±28k worst case).
978 assert!(
979 (-30_000..=30_000).contains(&pred.w0_q13),
980 "seed={seed}, w0={}",
981 pred.w0_q13
982 );
983 assert!(
984 (-15_000..=15_000).contains(&pred.w1_q13),
985 "seed={seed}, w1={}",
986 pred.w1_q13
987 );
988 }
989 }
990
991 // ----- §4.2.7.1–§4.2.7.5.1 encode-side mirrors ------------------
992
993 /// A tiny deterministic LCG for the encode/decode roundtrip sweeps.
994 struct Lcg(u64);
995 impl Lcg {
996 fn next_u32(&mut self) -> u32 {
997 self.0 = self
998 .0
999 .wrapping_mul(6364136223846793005)
1000 .wrapping_add(1442695040888963407);
1001 (self.0 >> 32) as u32
1002 }
1003 fn below(&mut self, n: u32) -> u32 {
1004 self.next_u32() % n
1005 }
1006 }
1007
1008 /// encode_pre_gains → decode_pre_gains roundtrip over random symbol
1009 /// scripts covering mono / stereo-mid / mid-only-flag / all frame
1010 /// kinds: the decoder must reconstruct exactly the header the
1011 /// encoder predicted.
1012 #[test]
1013 fn header_encode_decode_roundtrip_random_scripts() {
1014 use crate::range_encoder::RangeEncoder;
1015 let mut rng = Lcg(0x00AC_E0F5_EED5);
1016 for _ in 0..500 {
1017 let stereo_mid = rng.below(2) == 0;
1018 let has_mid_only = stereo_mid && rng.below(2) == 0;
1019 let kind = match rng.below(3) {
1020 0 => FrameKind::RegularInactive,
1021 1 => FrameKind::RegularActive,
1022 _ => FrameKind::Lbrr,
1023 };
1024 let bandwidth = match rng.below(3) {
1025 0 => Bandwidth::Nb,
1026 1 => Bandwidth::Mb,
1027 _ => Bandwidth::Wb,
1028 };
1029 let cfg = SilkFrameHeaderConfig {
1030 stereo_mid_channel: stereo_mid,
1031 stereo: stereo_mid,
1032 has_mid_only_flag: has_mid_only,
1033 kind,
1034 bandwidth,
1035 };
1036 let symbols = SilkHeaderSymbols {
1037 stereo: stereo_mid.then(|| StereoWeightSymbols {
1038 n: rng.below(25) as u8,
1039 i0: rng.below(3) as u8,
1040 i1: rng.below(5) as u8,
1041 i2: rng.below(3) as u8,
1042 i3: rng.below(5) as u8,
1043 }),
1044 mid_only_flag: has_mid_only.then(|| rng.below(2) == 1),
1045 frame_type: match kind {
1046 FrameKind::RegularInactive => rng.below(2) as u8,
1047 _ => 2 + rng.below(4) as u8,
1048 },
1049 };
1050 let lsf_stage1 = rng.below(32) as u8;
1051
1052 let mut re = RangeEncoder::new();
1053 let predicted =
1054 SilkFrameHeader::encode_pre_gains(&mut re, cfg, &symbols).expect("encode");
1055 SilkFrameHeader::encode_lsf_stage1(
1056 &mut re,
1057 bandwidth,
1058 predicted.signal_type,
1059 lsf_stage1,
1060 )
1061 .expect("encode lsf1");
1062 let bytes = re.finish();
1063
1064 let mut rd = RangeDecoder::new(&bytes);
1065 let decoded = SilkFrameHeader::decode_pre_gains(&mut rd, cfg).expect("decode");
1066 assert_eq!(decoded, predicted, "symbols={symbols:?}");
1067 let got_lsf1 =
1068 SilkFrameHeader::decode_lsf_stage1(&mut rd, bandwidth, decoded.signal_type)
1069 .expect("decode lsf1");
1070 assert_eq!(got_lsf1, lsf_stage1);
1071 assert!(!rd.has_error());
1072 }
1073 }
1074
1075 /// The encode path rejects symbol/config mismatches and
1076 /// out-of-support values.
1077 #[test]
1078 fn header_encode_rejects_bad_symbols() {
1079 use crate::range_encoder::RangeEncoder;
1080 let mono_cfg = SilkFrameHeaderConfig {
1081 stereo_mid_channel: false,
1082 stereo: false,
1083 has_mid_only_flag: false,
1084 kind: FrameKind::RegularActive,
1085 bandwidth: Bandwidth::Nb,
1086 };
1087 let stereo_syms = SilkHeaderSymbols {
1088 stereo: Some(StereoWeightSymbols {
1089 n: 0,
1090 i0: 0,
1091 i1: 0,
1092 i2: 0,
1093 i3: 0,
1094 }),
1095 mid_only_flag: None,
1096 frame_type: 4,
1097 };
1098 // Stereo symbols on a mono config.
1099 let mut re = RangeEncoder::new();
1100 assert!(SilkFrameHeader::encode_pre_gains(&mut re, mono_cfg, &stereo_syms).is_err());
1101 // Inactive frame type on an active kind.
1102 let mut re = RangeEncoder::new();
1103 let bad_type = SilkHeaderSymbols {
1104 stereo: None,
1105 mid_only_flag: None,
1106 frame_type: 1,
1107 };
1108 assert!(SilkFrameHeader::encode_pre_gains(&mut re, mono_cfg, &bad_type).is_err());
1109 // Out-of-range stereo indices.
1110 let mut re = RangeEncoder::new();
1111 let stereo_cfg = SilkFrameHeaderConfig {
1112 stereo_mid_channel: true,
1113 stereo: true,
1114 has_mid_only_flag: false,
1115 kind: FrameKind::RegularActive,
1116 bandwidth: Bandwidth::Nb,
1117 };
1118 let bad_stereo = SilkHeaderSymbols {
1119 stereo: Some(StereoWeightSymbols {
1120 n: 25,
1121 i0: 0,
1122 i1: 0,
1123 i2: 0,
1124 i3: 0,
1125 }),
1126 mid_only_flag: None,
1127 frame_type: 4,
1128 };
1129 assert!(SilkFrameHeader::encode_pre_gains(&mut re, stereo_cfg, &bad_stereo).is_err());
1130 // Out-of-range LSF stage-1 index / SWB bandwidth.
1131 let mut re = RangeEncoder::new();
1132 assert!(
1133 SilkFrameHeader::encode_lsf_stage1(&mut re, Bandwidth::Nb, SignalType::Voiced, 32)
1134 .is_err()
1135 );
1136 let mut re = RangeEncoder::new();
1137 assert!(
1138 SilkFrameHeader::encode_lsf_stage1(&mut re, Bandwidth::Swb, SignalType::Voiced, 0)
1139 .is_err()
1140 );
1141 }
1142
1143 /// Enumerate every §4.2.7.1 quintuple in `(n, i0, i1, i2, i3)`
1144 /// lexicographic order.
1145 fn all_weight_quintuples() -> Vec<StereoWeightSymbols> {
1146 let mut v = Vec::with_capacity(5625);
1147 for n in 0..=24u8 {
1148 for i0 in 0..=2u8 {
1149 for i1 in 0..=4u8 {
1150 for i2 in 0..=2u8 {
1151 for i3 in 0..=4u8 {
1152 v.push(StereoWeightSymbols { n, i0, i1, i2, i3 });
1153 }
1154 }
1155 }
1156 }
1157 }
1158 v
1159 }
1160
1161 /// A representable target (any output of `weights()`) quantizes back
1162 /// value-exactly: the achieved pair equals the target pair. Sampled
1163 /// across the whole codebook (stride 7 keeps the exhaustive
1164 /// `quantize` affordable while touching every index dimension).
1165 #[test]
1166 fn stereo_weight_quantize_exact_on_representable_targets() {
1167 let all = all_weight_quintuples();
1168 for q in all.iter().step_by(7) {
1169 let target = q.weights();
1170 let quant = StereoWeightSymbols::quantize(target);
1171 assert_eq!(
1172 quant.weights(),
1173 target,
1174 "quintuple {q:?} did not roundtrip through quantize"
1175 );
1176 }
1177 }
1178
1179 /// `quantize` is a true argmin: for random unrepresentable targets
1180 /// no quintuple in the codebook achieves a strictly smaller squared
1181 /// Q13 error than the returned one, and repeated calls are
1182 /// deterministic.
1183 #[test]
1184 fn stereo_weight_quantize_is_argmin_and_deterministic() {
1185 let all = all_weight_quintuples();
1186 let err = |w: StereoPredictionWeights, t: StereoPredictionWeights| -> i64 {
1187 let e0 = (w.w0_q13 - t.w0_q13) as i64;
1188 let e1 = (w.w1_q13 - t.w1_q13) as i64;
1189 e0 * e0 + e1 * e1
1190 };
1191 // Small deterministic LCG.
1192 let mut s = 0x5EED_0385u64;
1193 let mut next = move || {
1194 s = s
1195 .wrapping_mul(6364136223846793005)
1196 .wrapping_add(1442695040888963407);
1197 ((s >> 33) as i32 % 60001) - 30000
1198 };
1199 for _ in 0..25 {
1200 let target = StereoPredictionWeights {
1201 w0_q13: next(),
1202 w1_q13: next(),
1203 };
1204 let quant = StereoWeightSymbols::quantize(target);
1205 let best = err(quant.weights(), target);
1206 for cand in &all {
1207 assert!(
1208 err(cand.weights(), target) >= best,
1209 "candidate {cand:?} beats quantize({target:?}) = {quant:?}"
1210 );
1211 }
1212 assert_eq!(StereoWeightSymbols::quantize(target), quant);
1213 }
1214 }
1215
1216 /// Extreme targets saturate to the codebook's extreme reachable
1217 /// weight pairs (computed from the codebook itself, not hard-coded).
1218 #[test]
1219 fn stereo_weight_quantize_saturates_at_extremes() {
1220 let all = all_weight_quintuples();
1221 let max_w1 = all.iter().map(|q| q.weights().w1_q13).max().unwrap();
1222 let min_w1 = all.iter().map(|q| q.weights().w1_q13).min().unwrap();
1223 // A target far above every reachable w1 (with w0 target 0) must
1224 // land on a maximal-w1 quintuple, and symmetrically below.
1225 let hi = StereoWeightSymbols::quantize(StereoPredictionWeights {
1226 w0_q13: 0,
1227 w1_q13: 1 << 20,
1228 });
1229 assert_eq!(hi.weights().w1_q13, max_w1);
1230 let lo = StereoWeightSymbols::quantize(StereoPredictionWeights {
1231 w0_q13: 0,
1232 w1_q13: -(1 << 20),
1233 });
1234 assert_eq!(lo.weights().w1_q13, min_w1);
1235 }
1236}