Skip to main content

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}