Skip to main content

oxideav_opus/
packet_compose.rs

1//! Opus packet frame-packing **writer** (RFC 6716 §3.2 + Appendix B)
2//! — the write-side mirror of [`crate::frames::OpusPacket::parse`] and
3//! [`crate::framing_self_delim::parse_self_delimited`].
4//!
5//! Given a §3.1 TOC byte and the compressed frame payloads, these
6//! entry points emit the §3.2 framing layer around them:
7//!
8//! * **Code 0** (§3.2.2) — `TOC | frame`.
9//! * **Code 1** (§3.2.3) — `TOC | frame | frame` (equal sizes, R3).
10//! * **Code 2** (§3.2.4) — `TOC | len(frame1) | frame1 | frame2`.
11//! * **Code 3** (§3.2.5) — `TOC | count byte | [padding chain] |
12//!   [VBR lengths] | frames | [padding]`, with the `v` / `p` bits,
13//!   the §3.2.5 255-chained padding length, and — for VBR — the
14//!   `M - 1` §3.2.1 length sequences (the last frame's length stays
15//!   implicit).
16//!
17//! [`compose_self_delimited`] emits the Appendix-B variant instead:
18//! one extra §3.2.1 length field making the packet self-terminating
19//! (code 0: the frame length; code 1: the shared length; code 2: the
20//! *second* frame's length; code 3 CBR: the shared length; code 3
21//! VBR: the *last* frame's length — Figures 25-29), so packets can be
22//! chained back-to-back inside a multistream payload and re-split by
23//! [`crate::framing_self_delim::parse_self_delimited`].
24//!
25//! Every §3.2 requirement the parsers enforce is validated here
26//! before writing (R1-R7 as applicable): per-frame lengths within
27//! [`MAX_FRAME_BYTES`], code-1 length equality, code-3 frame counts
28//! in `1..=48` with the R5 120 ms packet-duration bound, and CBR
29//! length uniformity. Composed packets therefore always reparse to
30//! the same frames, padding, and TOC byte.
31
32use crate::frames::{MAX_FRAMES_PER_PACKET, MAX_FRAME_BYTES};
33use crate::toc::{FrameCountCode, OpusTocByte};
34use crate::Error;
35
36/// Encode one §3.2.1 length sequence (0..=1275) into `out`.
37///
38/// * `0..=251` — one byte, the value itself.
39/// * `252..=1275` — two bytes: `first ∈ 252..=255` with
40///   `first ≡ length (mod 4)` offset from 252, then
41///   `second = (length - first) / 4`, so that the §3.2.1 decode
42///   `second * 4 + first` reproduces `length`.
43///
44/// Errors when `length > 1275` (R2: the two-byte form tops out at
45/// `255 * 4 + 255`).
46pub fn encode_length(length: usize, out: &mut Vec<u8>) -> Result<(), Error> {
47    if length > MAX_FRAME_BYTES {
48        return Err(Error::MalformedPacket);
49    }
50    if length < 252 {
51        out.push(length as u8);
52    } else {
53        let first = 252 + ((length - 252) % 4);
54        let second = (length - first) / 4;
55        out.push(first as u8);
56        out.push(second as u8);
57    }
58    Ok(())
59}
60
61/// Write the §3.2.5 padding-length chain for `padding` trailing bytes
62/// into `out`: each `255` byte contributes 254 bytes and continues the
63/// chain; the closing byte (`0..=254`) contributes its value.
64fn write_padding_chain(mut padding: usize, out: &mut Vec<u8>) {
65    while padding >= 255 {
66        out.push(255);
67        padding -= 254;
68    }
69    out.push(padding as u8);
70}
71
72/// Validate the shared per-code frame-shape rules and return the
73/// parsed TOC (R2 per-frame bound; counts per code; code-1 equality).
74fn validate_shape(toc_byte: u8, frames: &[&[u8]]) -> Result<OpusTocByte, Error> {
75    let toc = OpusTocByte::from_byte(toc_byte);
76    for f in frames {
77        if f.len() > MAX_FRAME_BYTES {
78            return Err(Error::MalformedPacket);
79        }
80    }
81    match toc.frame_count_code {
82        FrameCountCode::One => {
83            if frames.len() != 1 {
84                return Err(Error::MalformedPacket);
85            }
86        }
87        FrameCountCode::TwoEqual => {
88            if frames.len() != 2 || frames[0].len() != frames[1].len() {
89                return Err(Error::MalformedPacket);
90            }
91        }
92        FrameCountCode::TwoUnequal => {
93            if frames.len() != 2 {
94                return Err(Error::MalformedPacket);
95            }
96        }
97        FrameCountCode::Arbitrary => {
98            let m = frames.len();
99            if m == 0 || m > MAX_FRAMES_PER_PACKET as usize {
100                return Err(Error::MalformedPacket);
101            }
102            // R5: the packet's audio duration MUST NOT exceed 120 ms.
103            if m as u32 * toc.frame_size_tenths_ms as u32 > 1200 {
104                return Err(Error::MalformedPacket);
105            }
106        }
107    }
108    Ok(toc)
109}
110
111/// Compose one regular (undelimited) Opus packet: the §3.2 framing
112/// layer around `frames`, selected by the TOC byte's frame-count code.
113///
114/// For a code-3 packet the CBR/VBR bit is chosen automatically: CBR
115/// when every frame has the same length, VBR otherwise. Use
116/// [`compose_packet_code3`] to force VBR framing or to append §3.2.5
117/// padding.
118///
119/// Errors on any §3.2 shape violation (see the module docs).
120pub fn compose_packet(toc_byte: u8, frames: &[&[u8]]) -> Result<Vec<u8>, Error> {
121    let toc = validate_shape(toc_byte, frames)?;
122    match toc.frame_count_code {
123        FrameCountCode::One => Ok([&[toc_byte], frames[0]].concat()),
124        FrameCountCode::TwoEqual => Ok([&[toc_byte], frames[0], frames[1]].concat()),
125        FrameCountCode::TwoUnequal => {
126            let mut out = Vec::with_capacity(3 + frames[0].len() + frames[1].len());
127            out.push(toc_byte);
128            encode_length(frames[0].len(), &mut out)?;
129            out.extend_from_slice(frames[0]);
130            out.extend_from_slice(frames[1]);
131            Ok(out)
132        }
133        FrameCountCode::Arbitrary => {
134            let cbr = frames.iter().all(|f| f.len() == frames[0].len());
135            compose_packet_code3(toc_byte, frames, !cbr, 0)
136        }
137    }
138}
139
140/// Compose one **code-3** Opus packet with explicit VBR / padding
141/// control (§3.2.5).
142///
143/// * `vbr` — write the `v` bit and the `M - 1` per-frame length
144///   sequences. With `vbr == false` (CBR) every frame must have the
145///   same length (R6).
146/// * `padding` — number of trailing padding bytes (zeros); the
147///   §3.2.5 255-chained padding-length header is derived from it and
148///   the `p` bit set when non-zero.
149///
150/// The TOC byte must carry frame-count code 3.
151pub fn compose_packet_code3(
152    toc_byte: u8,
153    frames: &[&[u8]],
154    vbr: bool,
155    padding: usize,
156) -> Result<Vec<u8>, Error> {
157    let toc = validate_shape(toc_byte, frames)?;
158    if toc.frame_count_code != FrameCountCode::Arbitrary {
159        return Err(Error::MalformedPacket);
160    }
161    if !vbr && frames.iter().any(|f| f.len() != frames[0].len()) {
162        return Err(Error::MalformedPacket);
163    }
164    let m = frames.len();
165    let mut out = Vec::new();
166    out.push(toc_byte);
167    out.push(((m as u8) << 2) | (u8::from(padding > 0) << 1) | u8::from(vbr));
168    if padding > 0 {
169        write_padding_chain(padding, &mut out);
170    }
171    if vbr {
172        // The last frame's length stays implicit (§3.2.5).
173        for f in &frames[..m - 1] {
174            encode_length(f.len(), &mut out)?;
175        }
176    }
177    for f in frames {
178        out.extend_from_slice(f);
179    }
180    out.resize(out.len() + padding, 0);
181    Ok(out)
182}
183
184/// Compose one **self-delimited** Opus packet (RFC 6716 Appendix B,
185/// Figures 25-29) — the write-side mirror of
186/// [`crate::framing_self_delim::parse_self_delimited`], for chaining
187/// the first `N - 1` streams of a multistream payload.
188///
189/// The extra Appendix-B length field is placed exactly where the
190/// parser expects it: after the TOC byte (codes 0/1), after the
191/// §3.2.4 first-frame length (code 2), or after the frame-count byte
192/// / padding chain / VBR inline lengths (code 3). `vbr` / `padding`
193/// apply to code-3 packets only (pass `false` / `0` otherwise; a
194/// non-code-3 TOC with padding or forced VBR errors). As in
195/// [`compose_packet`], a code-3 packet chooses CBR automatically when
196/// `vbr` is `false`, which then requires uniform frame lengths.
197pub fn compose_self_delimited(
198    toc_byte: u8,
199    frames: &[&[u8]],
200    vbr: bool,
201    padding: usize,
202) -> Result<Vec<u8>, Error> {
203    let toc = validate_shape(toc_byte, frames)?;
204    if toc.frame_count_code != FrameCountCode::Arbitrary && (vbr || padding > 0) {
205        return Err(Error::MalformedPacket);
206    }
207    let mut out = Vec::new();
208    out.push(toc_byte);
209    match toc.frame_count_code {
210        FrameCountCode::One => {
211            // Figure 25: TOC | N1 | frame.
212            encode_length(frames[0].len(), &mut out)?;
213            out.extend_from_slice(frames[0]);
214        }
215        FrameCountCode::TwoEqual => {
216            // Figure 26: TOC | N1 | frame | frame.
217            encode_length(frames[0].len(), &mut out)?;
218            out.extend_from_slice(frames[0]);
219            out.extend_from_slice(frames[1]);
220        }
221        FrameCountCode::TwoUnequal => {
222            // Figure 27: TOC | N1 | N2 | frame1 | frame2.
223            encode_length(frames[0].len(), &mut out)?;
224            encode_length(frames[1].len(), &mut out)?;
225            out.extend_from_slice(frames[0]);
226            out.extend_from_slice(frames[1]);
227        }
228        FrameCountCode::Arbitrary => {
229            if !vbr && frames.iter().any(|f| f.len() != frames[0].len()) {
230                return Err(Error::MalformedPacket);
231            }
232            let m = frames.len();
233            out.push(((m as u8) << 2) | (u8::from(padding > 0) << 1) | u8::from(vbr));
234            if padding > 0 {
235                write_padding_chain(padding, &mut out);
236            }
237            if vbr {
238                // Figure 29: M-1 inline lengths, then the Appendix-B
239                // length of the LAST frame.
240                for f in &frames[..m - 1] {
241                    encode_length(f.len(), &mut out)?;
242                }
243                encode_length(frames[m - 1].len(), &mut out)?;
244            } else {
245                // Figure 28: one Appendix-B length shared by all frames.
246                encode_length(frames[0].len(), &mut out)?;
247            }
248            for f in frames {
249                out.extend_from_slice(f);
250            }
251            out.resize(out.len() + padding, 0);
252        }
253    }
254    Ok(out)
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::frames::{decode_length, OpusPacket};
261    use crate::framing_self_delim::parse_self_delimited;
262    use crate::toc::{Bandwidth, Mode};
263
264    /// A tiny deterministic LCG.
265    struct Lcg(u64);
266    impl Lcg {
267        fn next_u32(&mut self) -> u32 {
268            self.0 = self
269                .0
270                .wrapping_mul(6364136223846793005)
271                .wrapping_add(1442695040888963407);
272            (self.0 >> 32) as u32
273        }
274        fn below(&mut self, n: u32) -> u32 {
275            self.next_u32() % n
276        }
277    }
278
279    fn random_frame(rng: &mut Lcg, max_len: u32) -> Vec<u8> {
280        let len = rng.below(max_len + 1) as usize;
281        (0..len).map(|_| rng.next_u32() as u8).collect()
282    }
283
284    fn toc(code: FrameCountCode) -> u8 {
285        // CELT-only FB 10 ms accommodates up to 12 frames under R5.
286        OpusTocByte::compose_byte(Mode::CeltOnly, Bandwidth::Fb, 100, false, code).unwrap()
287    }
288
289    /// §3.2.1 write/read: every legal length 0..=1275 roundtrips
290    /// through the shared decoder, and 1276 is rejected.
291    #[test]
292    fn encode_length_roundtrips_all_values() {
293        for len in 0..=MAX_FRAME_BYTES {
294            let mut buf = Vec::new();
295            encode_length(len, &mut buf).unwrap();
296            let (decoded, consumed) = decode_length(&buf).unwrap();
297            assert_eq!((decoded, consumed), (len, buf.len()), "length {len}");
298        }
299        let mut buf = Vec::new();
300        assert!(encode_length(MAX_FRAME_BYTES + 1, &mut buf).is_err());
301    }
302
303    /// Regular-framing roundtrip across all four codes: composed
304    /// packets reparse to the same TOC byte, frames, and padding.
305    #[test]
306    fn compose_parse_roundtrip_all_codes() {
307        let mut rng = Lcg(0x0385_C0DE);
308        for round in 0..200 {
309            let (toc_byte, frames): (u8, Vec<Vec<u8>>) = match rng.below(4) {
310                0 => (toc(FrameCountCode::One), vec![random_frame(&mut rng, 1275)]),
311                1 => {
312                    let f = random_frame(&mut rng, 1275);
313                    (toc(FrameCountCode::TwoEqual), vec![f.clone(), f])
314                }
315                2 => (
316                    toc(FrameCountCode::TwoUnequal),
317                    vec![random_frame(&mut rng, 1275), random_frame(&mut rng, 1275)],
318                ),
319                _ => {
320                    let m = 1 + rng.below(12) as usize;
321                    (
322                        toc(FrameCountCode::Arbitrary),
323                        (0..m).map(|_| random_frame(&mut rng, 300)).collect(),
324                    )
325                }
326            };
327            let slices: Vec<&[u8]> = frames.iter().map(|f| f.as_slice()).collect();
328            let packet = compose_packet(toc_byte, &slices).expect("compose");
329            let parsed = OpusPacket::parse(&packet).expect("parse");
330            assert_eq!(
331                parsed.toc,
332                OpusTocByte::from_byte(toc_byte),
333                "round {round}"
334            );
335            assert_eq!(parsed.frames(), &slices[..], "round {round}");
336            assert_eq!(parsed.padding, 0, "round {round}");
337        }
338    }
339
340    /// Code-3 with forced VBR (even for equal lengths) and padding
341    /// (including the 255-chained lengths) roundtrips, and the parser
342    /// reports the same trailing-padding byte count.
343    #[test]
344    fn compose_code3_vbr_and_padding_roundtrip() {
345        let mut rng = Lcg(0x0AD5_0385);
346        let toc_byte = toc(FrameCountCode::Arbitrary);
347        for &padding in &[0usize, 1, 42, 253, 254, 255, 300, 600] {
348            let m = 1 + rng.below(6) as usize;
349            let frames: Vec<Vec<u8>> = (0..m).map(|_| random_frame(&mut rng, 200)).collect();
350            let slices: Vec<&[u8]> = frames.iter().map(|f| f.as_slice()).collect();
351            for vbr in [false, true] {
352                if !vbr && !slices.iter().all(|f| f.len() == slices[0].len()) {
353                    continue;
354                }
355                let packet =
356                    compose_packet_code3(toc_byte, &slices, vbr, padding).expect("compose");
357                let parsed = OpusPacket::parse(&packet).expect("parse");
358                assert_eq!(parsed.frames(), &slices[..], "vbr={vbr} padding={padding}");
359                assert_eq!(parsed.padding, padding, "vbr={vbr} padding={padding}");
360            }
361        }
362    }
363
364    /// Appendix-B roundtrip: self-delimited packets of every code
365    /// reparse identically AND report `consumed == len`, and three
366    /// packets chained back-to-back split correctly.
367    #[test]
368    fn compose_self_delimited_roundtrip_and_chain() {
369        let mut rng = Lcg(0x5E1F_DE11);
370        let mut chained = Vec::new();
371        let mut expected: Vec<(u8, Vec<Vec<u8>>)> = Vec::new();
372        for code_pick in 0..4u32 {
373            let (toc_byte, frames, vbr, padding): (u8, Vec<Vec<u8>>, bool, usize) = match code_pick
374            {
375                0 => (
376                    toc(FrameCountCode::One),
377                    vec![random_frame(&mut rng, 400)],
378                    false,
379                    0,
380                ),
381                1 => {
382                    let f = random_frame(&mut rng, 400);
383                    (toc(FrameCountCode::TwoEqual), vec![f.clone(), f], false, 0)
384                }
385                2 => (
386                    toc(FrameCountCode::TwoUnequal),
387                    vec![random_frame(&mut rng, 400), random_frame(&mut rng, 400)],
388                    false,
389                    0,
390                ),
391                _ => (
392                    toc(FrameCountCode::Arbitrary),
393                    (0..5).map(|_| random_frame(&mut rng, 300)).collect(),
394                    true,
395                    77,
396                ),
397            };
398            let slices: Vec<&[u8]> = frames.iter().map(|f| f.as_slice()).collect();
399            let packet =
400                compose_self_delimited(toc_byte, &slices, vbr, padding).expect("compose sd");
401            // Individual roundtrip: exact consumption.
402            let parsed = parse_self_delimited(&packet).expect("parse sd");
403            assert_eq!(parsed.consumed, packet.len(), "code {code_pick}");
404            assert_eq!(parsed.packet.frames(), &slices[..], "code {code_pick}");
405            assert_eq!(parsed.packet.padding, padding, "code {code_pick}");
406            chained.extend_from_slice(&packet);
407            expected.push((toc_byte, frames));
408        }
409        // Chained: parse packets back-to-back from one buffer.
410        let mut cursor = 0usize;
411        for (toc_byte, frames) in &expected {
412            let parsed = parse_self_delimited(&chained[cursor..]).expect("chained parse");
413            assert_eq!(parsed.packet.toc, OpusTocByte::from_byte(*toc_byte));
414            let slices: Vec<&[u8]> = frames.iter().map(|f| f.as_slice()).collect();
415            assert_eq!(parsed.packet.frames(), &slices[..]);
416            cursor += parsed.consumed;
417        }
418        assert_eq!(cursor, chained.len());
419    }
420
421    /// Shape violations are rejected: frame-count mismatches, unequal
422    /// code-1 frames, oversize frames, empty / oversized code-3 frame
423    /// lists, the R5 duration bound, CBR with non-uniform lengths, and
424    /// padding / VBR options on non-code-3 packets.
425    #[test]
426    fn compose_rejects_shape_violations() {
427        let f10 = vec![0u8; 10];
428        let f11 = vec![0u8; 11];
429        let big = vec![0u8; MAX_FRAME_BYTES + 1];
430        // Frame-count mismatches per code.
431        assert!(compose_packet(toc(FrameCountCode::One), &[&f10, &f10]).is_err());
432        assert!(compose_packet(toc(FrameCountCode::TwoEqual), &[&f10]).is_err());
433        assert!(compose_packet(toc(FrameCountCode::TwoUnequal), &[&f10]).is_err());
434        assert!(compose_packet(toc(FrameCountCode::Arbitrary), &[]).is_err());
435        // R3: code-1 frames must be equal-size.
436        assert!(compose_packet(toc(FrameCountCode::TwoEqual), &[&f10, &f11]).is_err());
437        // R2: per-frame bound.
438        assert!(compose_packet(toc(FrameCountCode::One), &[&big]).is_err());
439        // Code-3 frame-count / duration caps: 49 frames trip both the
440        // M <= 48 bound and (at 2.5 ms) the R5 duration bound, while
441        // 48 x 2.5 ms = 120 ms is exactly legal.
442        let toc_25 = OpusTocByte::compose_byte(
443            Mode::CeltOnly,
444            Bandwidth::Fb,
445            25,
446            false,
447            FrameCountCode::Arbitrary,
448        )
449        .unwrap();
450        let many: Vec<&[u8]> = (0..49).map(|_| f10.as_slice()).collect();
451        assert!(compose_packet(toc_25, &many).is_err());
452        assert!(compose_packet(toc_25, &many[..48]).is_ok());
453        // R5: 3 × 60 ms = 180 ms > 120 ms.
454        let toc_60 = OpusTocByte::compose_byte(
455            Mode::SilkOnly,
456            Bandwidth::Nb,
457            600,
458            false,
459            FrameCountCode::Arbitrary,
460        )
461        .unwrap();
462        assert!(compose_packet(toc_60, &[&f10, &f10, &f10]).is_err());
463        assert!(compose_packet(toc_60, &[&f10, &f10]).is_ok());
464        // CBR with non-uniform lengths.
465        assert!(
466            compose_packet_code3(toc(FrameCountCode::Arbitrary), &[&f10, &f11], false, 0).is_err()
467        );
468        // compose_packet_code3 demands a code-3 TOC.
469        assert!(compose_packet_code3(toc(FrameCountCode::One), &[&f10], false, 0).is_err());
470        // Self-delimited: padding / VBR only for code 3.
471        assert!(compose_self_delimited(toc(FrameCountCode::One), &[&f10], false, 4).is_err());
472        assert!(compose_self_delimited(toc(FrameCountCode::One), &[&f10], true, 0).is_err());
473        assert!(
474            compose_self_delimited(toc(FrameCountCode::Arbitrary), &[&f10, &f11], false, 0)
475                .is_err()
476        );
477    }
478
479    /// Owns one deterministic 20 ms NB SILK frame script's buffers.
480    struct SilkScript {
481        frame_type: u8,
482        gains: Vec<crate::silk_gains::GainSymbol>,
483        i2: Vec<i8>,
484        lsb: Vec<u8>,
485        e_raw: Vec<i32>,
486    }
487
488    impl SilkScript {
489        /// A simple valid NB 20 ms frame: 4 gain symbols, zero LSF
490        /// stage-2 residual, no LTP (unvoiced/inactive types only),
491        /// `pulses` unit pulses at each shell block's first sample.
492        fn new(frame_type: u8, pulses: i32) -> Self {
493            use crate::silk_excitation::{shell_block_count, SilkFrameSize, SHELL_BLOCK_SAMPLES};
494            use crate::silk_gains::GainSymbol;
495            assert!(frame_type < 4, "voiced scripts would need LTP symbols");
496            let blocks = shell_block_count(Bandwidth::Nb, SilkFrameSize::TwentyMs).unwrap();
497            let mut e_raw = vec![0i32; blocks * SHELL_BLOCK_SAMPLES];
498            for b in 0..blocks {
499                e_raw[b * SHELL_BLOCK_SAMPLES] = pulses;
500            }
501            SilkScript {
502                frame_type,
503                gains: vec![
504                    GainSymbol::Independent(40),
505                    GainSymbol::Delta(10),
506                    GainSymbol::Delta(15),
507                    GainSymbol::Delta(20),
508                ],
509                i2: vec![0i8; 10],
510                lsb: vec![0u8; blocks],
511                e_raw,
512            }
513        }
514
515        fn symbols(&self) -> crate::silk_decode::SilkFrameSymbols<'_> {
516            crate::silk_decode::SilkFrameSymbols {
517                header: crate::silk_frame::SilkHeaderSymbols {
518                    stereo: None,
519                    mid_only_flag: None,
520                    frame_type: self.frame_type,
521                },
522                gains: &self.gains,
523                lsf_stage1: 5,
524                lsf_stage2_i2: &self.i2,
525                lsf_interp_w_q2: Some(4),
526                ltp: None,
527                lcg_seed: 1,
528                excitation: crate::silk_excitation::ExcitationSymbols {
529                    rate_level: 3,
530                    lsb_counts: &self.lsb,
531                    e_raw: &self.e_raw,
532                },
533            }
534        }
535    }
536
537    /// End-to-end §3.2 + §4.5: two independently encoded 20 ms mono
538    /// SILK frame bodies packed as code-1 / code-2 / code-3-VBR
539    /// packets decode through `OpusDecoder::decode_packet` as two real
540    /// SILK frames with the exact combined sample count.
541    #[test]
542    fn composed_multiframe_silk_packets_decode_end_to_end() {
543        use crate::decoder::{FrameDecodeStatus, OpusDecoder};
544        use crate::silk_packet_encode::encode_silk_only_packet_mono;
545
546        // Reuse the packet encoder for two frame bodies: encode two
547        // single-frame packets and strip their TOC bytes. The bodies
548        // are self-contained §4.2 frames (each starts its own range
549        // coder), which is exactly what a multi-frame packet carries.
550        let script1 = SilkScript::new(0, 1);
551        let script2 = SilkScript::new(2, 7);
552        let (p1, _) = encode_silk_only_packet_mono(Bandwidth::Nb, 200, &[script1.symbols()])
553            .expect("encode 1");
554        let (p2, _) = encode_silk_only_packet_mono(Bandwidth::Nb, 200, &[script2.symbols()])
555            .expect("encode 2");
556        let body1 = &p1[1..];
557        let body2 = &p2[1..];
558
559        let toc_code2 = OpusTocByte::compose_byte(
560            Mode::SilkOnly,
561            Bandwidth::Nb,
562            200,
563            false,
564            FrameCountCode::TwoUnequal,
565        )
566        .unwrap();
567        let toc_code3 = OpusTocByte::compose_byte(
568            Mode::SilkOnly,
569            Bandwidth::Nb,
570            200,
571            false,
572            FrameCountCode::Arbitrary,
573        )
574        .unwrap();
575        let toc_code1 = OpusTocByte::compose_byte(
576            Mode::SilkOnly,
577            Bandwidth::Nb,
578            200,
579            false,
580            FrameCountCode::TwoEqual,
581        )
582        .unwrap();
583
584        let mut candidates: Vec<Vec<u8>> = vec![
585            compose_packet(toc_code2, &[body1, body2]).unwrap(),
586            compose_packet_code3(toc_code3, &[body1, body2], true, 9).unwrap(),
587        ];
588        // Code 1 needs equal sizes: pack the same body twice.
589        candidates.push(compose_packet(toc_code1, &[body1, body1]).unwrap());
590
591        for (idx, packet) in candidates.iter().enumerate() {
592            let mut dec = OpusDecoder::new();
593            let out = dec.decode_packet(packet).expect("decode");
594            assert_eq!(out.frame_outcomes.len(), 2, "candidate {idx}");
595            for fo in &out.frame_outcomes {
596                assert_eq!(
597                    fo.status,
598                    FrameDecodeStatus::SilkParamsDecoded,
599                    "candidate {idx}"
600                );
601            }
602            assert_eq!(out.samples_per_channel(), 2 * 960, "candidate {idx}");
603        }
604    }
605}