oxideav_opus/framing.rs
1//! Opus packet → SILK/CELT layer routing — RFC 6716 §3.1 Table 2,
2//! §4.2 / §4.3 dispatch.
3//!
4//! This module sits between [`crate::toc::OpusTocByte`] (which decodes
5//! the §3.1 `(mode, bandwidth, frame_size)` triple from the TOC byte)
6//! and the per-layer decoders ([`crate::silk_header`],
7//! [`crate::silk_frame`], [`crate::celt_header`], …). For every Opus
8//! frame the §4 decoder must answer three questions before it can read
9//! the first range-coded symbol:
10//!
11//! 1. **Does this Opus frame carry a SILK layer?** Yes for [`OperatingMode::SilkOnly`]
12//! and [`OperatingMode::Hybrid`]; no for [`OperatingMode::CeltOnly`]
13//! (RFC 6716 §3.1 Table 2, §4.2 first paragraph).
14//! 2. **Does this Opus frame carry a CELT layer?** Yes for [`OperatingMode::Hybrid`]
15//! and [`OperatingMode::CeltOnly`]; no for [`OperatingMode::SilkOnly`].
16//! 3. **When the SILK layer is present, at what audio bandwidth does it
17//! run internally?** Per RFC 6716 §4.2 ("When used in a SWB or FB
18//! Hybrid frame, the LP layer itself still only runs in WB"), the
19//! SILK internal bandwidth is the TOC bandwidth for SILK-only, but
20//! pinned to [`SilkBandwidth::Wb`] for Hybrid regardless of the TOC's
21//! SWB / FB.
22//!
23//! Wiring these three answers up consistently is currently a
24//! per-caller open-coded decision; this module turns it into a single
25//! `OpusFrameRouting::from_toc` call that every Opus frame's §4
26//! decoder runs first. Tables 2 and 3 (the §3.1 configuration table
27//! and the §4.2.2 SILK-layer organization) are both consumed here, so
28//! a downstream caller doesn't need to look at the TOC `config`
29//! directly to know e.g. "this is a 60 ms stereo Hybrid frame: 2
30//! channels × 3 SILK frames each, plus one CELT decode at WB / 20 ms".
31//!
32//! ## What this module does not own
33//!
34//! * The §4.1 range decoder primitive — see [`crate::range_decoder`].
35//! * The §4.2.3 / §4.2.4 SILK header bits — see [`crate::silk_header`].
36//! * The §4.3 / Table 56 CELT pre-band header — see
37//! [`crate::celt_header`].
38//! * Anything bitstream-level — this module is a pure-function lookup
39//! on `(mode, bandwidth, frame_size, channels)`. It reads no bytes.
40//!
41//! ## Provenance
42//!
43//! Tables 2 (§3.1, p. 14) and the §4.2.2 SILK-layer organization (p.
44//! 33) of RFC 6716 (September 2012) are the only sources; the SILK
45//! frame-count enumeration is the same one [`crate::silk_header::silk_frame_count`]
46//! already encodes. No external library source consulted.
47
48use crate::silk_header::silk_frame_count;
49use crate::toc::{Bandwidth, ChannelMapping, Mode, OpusTocByte};
50
51/// Operating mode for one Opus frame, as routed from the §3.1 TOC
52/// `config` field. Mirrors [`crate::toc::Mode`] under a more
53/// dispatch-flavoured name so consumers reading
54/// `routing.operating_mode` make the right cognitive distinction:
55/// this is the *dispatch decision* derived from the TOC, not the raw
56/// field.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum OperatingMode {
59 /// SILK-only — only the §4.2 LP decoder runs.
60 SilkOnly,
61 /// Hybrid — both §4.2 LP and §4.3 CELT decoders run, with SILK
62 /// pinned to WB regardless of the TOC bandwidth.
63 Hybrid,
64 /// CELT-only — only the §4.3 MDCT decoder runs.
65 CeltOnly,
66}
67
68impl From<Mode> for OperatingMode {
69 fn from(mode: Mode) -> Self {
70 match mode {
71 Mode::SilkOnly => OperatingMode::SilkOnly,
72 Mode::Hybrid => OperatingMode::Hybrid,
73 Mode::CeltOnly => OperatingMode::CeltOnly,
74 }
75 }
76}
77
78/// Audio bandwidth at which the SILK layer of a SILK-bearing Opus
79/// frame runs internally.
80///
81/// Per RFC 6716 §4.2, the SILK layer only runs at NB, MB, or WB —
82/// even when the Opus frame's TOC bandwidth is SWB or FB (Hybrid
83/// mode), the LP layer itself is pinned to WB and the CELT layer
84/// covers the higher frequencies.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum SilkBandwidth {
87 Nb,
88 Mb,
89 Wb,
90}
91
92impl SilkBandwidth {
93 /// Convert to the crate-wide [`Bandwidth`] enum, suitable for
94 /// passing into the per-stage SILK decoders that already key
95 /// off `Bandwidth::{Nb, Mb, Wb}`.
96 pub fn to_bandwidth(self) -> Bandwidth {
97 match self {
98 SilkBandwidth::Nb => Bandwidth::Nb,
99 SilkBandwidth::Mb => Bandwidth::Mb,
100 SilkBandwidth::Wb => Bandwidth::Wb,
101 }
102 }
103}
104
105/// Routing decision for one Opus frame, derived purely from the TOC
106/// byte.
107///
108/// Holds every dispatch-level fact a §4 decoder needs before it
109/// touches the range coder. Every field is derivable from
110/// [`OpusTocByte`]; bundling them here keeps the dispatch logic
111/// in one place (and one set of tests) instead of duplicated across
112/// every caller that constructs a SILK or CELT context.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct OpusFrameRouting {
115 /// §3.1 Table 2 mode (SILK / Hybrid / CELT-only).
116 pub operating_mode: OperatingMode,
117 /// §3.1 Table 2 audio bandwidth as signalled by the TOC. For a
118 /// Hybrid frame this is the SWB / FB the *output* covers, not the
119 /// (always-WB) internal SILK bandwidth — use [`Self::silk_bandwidth`]
120 /// when feeding the SILK decoder.
121 pub toc_bandwidth: Bandwidth,
122 /// §3.1 Table 2 Opus-frame duration, in tenths of a millisecond
123 /// (25, 50, 100, 200, 400, 600).
124 pub frame_size_tenths_ms: u16,
125 /// §3.1 `s` bit (mono vs stereo).
126 pub channels: ChannelMapping,
127 /// SILK-bearing flag (true for SILK-only or Hybrid).
128 pub silk_layer: bool,
129 /// CELT-bearing flag (true for Hybrid or CELT-only).
130 pub celt_layer: bool,
131 /// Internal SILK bandwidth (Some when [`silk_layer`] is true; None
132 /// for CELT-only). For Hybrid this is always [`SilkBandwidth::Wb`]
133 /// per RFC 6716 §4.2 first paragraph.
134 ///
135 /// [`silk_layer`]: Self::silk_layer
136 pub silk_bandwidth: Option<SilkBandwidth>,
137 /// Number of regular SILK frames per channel per Opus frame
138 /// (Some when [`silk_layer`] is true; None otherwise). Per §4.2.2:
139 /// 1 for 10 / 20 ms Opus frames, 2 for 40 ms, 3 for 60 ms.
140 ///
141 /// [`silk_layer`]: Self::silk_layer
142 pub silk_frames_per_channel: Option<u8>,
143}
144
145impl OpusFrameRouting {
146 /// Derive the routing for one Opus frame from its TOC byte.
147 ///
148 /// Total function — every [`OpusTocByte`] value produces a valid
149 /// routing, because the TOC byte parser has already constrained
150 /// `(mode, bandwidth, frame_size, channels)` to the §3.1 Table 2
151 /// + Table 3 legal grid.
152 pub fn from_toc(toc: OpusTocByte) -> Self {
153 let operating_mode = OperatingMode::from(toc.mode);
154 let silk_layer = matches!(
155 operating_mode,
156 OperatingMode::SilkOnly | OperatingMode::Hybrid
157 );
158 let celt_layer = matches!(
159 operating_mode,
160 OperatingMode::Hybrid | OperatingMode::CeltOnly
161 );
162
163 let silk_bandwidth = if silk_layer {
164 // §4.2 first paragraph: "When used in a SWB or FB Hybrid
165 // frame, the LP layer itself still only runs in WB".
166 match operating_mode {
167 OperatingMode::Hybrid => Some(SilkBandwidth::Wb),
168 OperatingMode::SilkOnly => Some(match toc.bandwidth {
169 Bandwidth::Nb => SilkBandwidth::Nb,
170 Bandwidth::Mb => SilkBandwidth::Mb,
171 Bandwidth::Wb => SilkBandwidth::Wb,
172 // Unreachable: Table 2 never pairs SILK-only with
173 // SWB / FB. Defensive fall-through still produces
174 // the safest WB pin.
175 Bandwidth::Swb | Bandwidth::Fb => SilkBandwidth::Wb,
176 }),
177 OperatingMode::CeltOnly => None,
178 }
179 } else {
180 None
181 };
182
183 let silk_frames_per_channel = if silk_layer {
184 // silk_frame_count returns None only for the 2.5 / 5 ms
185 // CELT-only durations (which we've already excluded
186 // because silk_layer is false there). Defensive default
187 // 1 keeps the routing total in pathological inputs.
188 Some(silk_frame_count(toc.frame_size_tenths_ms).unwrap_or(1))
189 } else {
190 None
191 };
192
193 Self {
194 operating_mode,
195 toc_bandwidth: toc.bandwidth,
196 frame_size_tenths_ms: toc.frame_size_tenths_ms,
197 channels: toc.channels,
198 silk_layer,
199 celt_layer,
200 silk_bandwidth,
201 silk_frames_per_channel,
202 }
203 }
204
205 /// Number of audio channels (1 for mono, 2 for stereo).
206 pub fn channel_count(&self) -> u8 {
207 match self.channels {
208 ChannelMapping::Mono => 1,
209 ChannelMapping::Stereo => 2,
210 }
211 }
212
213 /// Total regular-SILK-frame count for this Opus frame across both
214 /// channels (mono × `frames_per_channel`, or stereo × 2 ×
215 /// `frames_per_channel`). Returns 0 for CELT-only frames.
216 pub fn total_silk_frames(&self) -> u8 {
217 match self.silk_frames_per_channel {
218 Some(n) => self.channel_count() * n,
219 None => 0,
220 }
221 }
222
223 /// `true` iff this Opus frame is long enough to potentially carry
224 /// §4.2.4 per-frame LBRR flag bytes (i.e. 40 ms or 60 ms). Per
225 /// §4.2.4 the per-frame flags are only present when the global
226 /// LBRR flag is set, but the duration gate alone is a routing
227 /// concern.
228 pub fn has_per_frame_lbrr_bits(&self) -> bool {
229 self.silk_layer && matches!(self.frame_size_tenths_ms, 400 | 600)
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::toc::{Bandwidth, ChannelMapping, FrameCountCode, Mode};
237
238 fn route(config: u8, stereo: bool) -> OpusFrameRouting {
239 let byte = (config << 3) | (if stereo { 1 << 2 } else { 0 });
240 OpusFrameRouting::from_toc(OpusTocByte::from_byte(byte))
241 }
242
243 /// `OperatingMode::from(Mode)` is a total bijection onto the
244 /// three operating modes.
245 #[test]
246 fn operating_mode_from_mode_total() {
247 assert_eq!(OperatingMode::from(Mode::SilkOnly), OperatingMode::SilkOnly);
248 assert_eq!(OperatingMode::from(Mode::Hybrid), OperatingMode::Hybrid);
249 assert_eq!(OperatingMode::from(Mode::CeltOnly), OperatingMode::CeltOnly);
250 }
251
252 /// SilkBandwidth → Bandwidth lifts cleanly to the three SILK
253 /// internal rates.
254 #[test]
255 fn silk_bandwidth_to_bandwidth() {
256 assert_eq!(SilkBandwidth::Nb.to_bandwidth(), Bandwidth::Nb);
257 assert_eq!(SilkBandwidth::Mb.to_bandwidth(), Bandwidth::Mb);
258 assert_eq!(SilkBandwidth::Wb.to_bandwidth(), Bandwidth::Wb);
259 }
260
261 /// SILK-only configs 0..=11 produce a SILK layer, no CELT layer,
262 /// SILK internal bandwidth that follows the TOC bandwidth, and
263 /// the correct §4.2.2 SILK-frame count.
264 #[test]
265 fn silk_only_routing_matches_table2() {
266 // 12 SILK-only configs: NB × {10, 20, 40, 60 ms},
267 // MB × {…}, WB × {…}.
268 let expected: [(Bandwidth, u16, SilkBandwidth, u8); 12] = [
269 (Bandwidth::Nb, 100, SilkBandwidth::Nb, 1),
270 (Bandwidth::Nb, 200, SilkBandwidth::Nb, 1),
271 (Bandwidth::Nb, 400, SilkBandwidth::Nb, 2),
272 (Bandwidth::Nb, 600, SilkBandwidth::Nb, 3),
273 (Bandwidth::Mb, 100, SilkBandwidth::Mb, 1),
274 (Bandwidth::Mb, 200, SilkBandwidth::Mb, 1),
275 (Bandwidth::Mb, 400, SilkBandwidth::Mb, 2),
276 (Bandwidth::Mb, 600, SilkBandwidth::Mb, 3),
277 (Bandwidth::Wb, 100, SilkBandwidth::Wb, 1),
278 (Bandwidth::Wb, 200, SilkBandwidth::Wb, 1),
279 (Bandwidth::Wb, 400, SilkBandwidth::Wb, 2),
280 (Bandwidth::Wb, 600, SilkBandwidth::Wb, 3),
281 ];
282 for (config, &(bw, dur, silk_bw, n)) in expected.iter().enumerate() {
283 let r = route(config as u8, false);
284 assert_eq!(r.operating_mode, OperatingMode::SilkOnly, "config {config}");
285 assert_eq!(r.toc_bandwidth, bw, "config {config}");
286 assert_eq!(r.frame_size_tenths_ms, dur, "config {config}");
287 assert!(r.silk_layer, "config {config}");
288 assert!(!r.celt_layer, "config {config}");
289 assert_eq!(r.silk_bandwidth, Some(silk_bw), "config {config}");
290 assert_eq!(r.silk_frames_per_channel, Some(n), "config {config}");
291 }
292 }
293
294 /// Hybrid configs 12..=15 carry both layers, pin SILK to WB even
295 /// though the TOC bandwidth is SWB / FB, and produce the correct
296 /// §4.2.2 SILK-frame count.
297 #[test]
298 fn hybrid_routing_pins_silk_to_wb() {
299 let expected: [(Bandwidth, u16, u8); 4] = [
300 (Bandwidth::Swb, 100, 1),
301 (Bandwidth::Swb, 200, 1),
302 (Bandwidth::Fb, 100, 1),
303 (Bandwidth::Fb, 200, 1),
304 ];
305 for (i, &(bw, dur, n)) in expected.iter().enumerate() {
306 let config = 12 + i as u8;
307 let r = route(config, false);
308 assert_eq!(r.operating_mode, OperatingMode::Hybrid, "config {config}");
309 assert_eq!(r.toc_bandwidth, bw, "config {config}");
310 assert_eq!(r.frame_size_tenths_ms, dur, "config {config}");
311 assert!(r.silk_layer, "config {config}");
312 assert!(r.celt_layer, "config {config}");
313 // The §4.2 pin to WB applies for every Hybrid frame.
314 assert_eq!(r.silk_bandwidth, Some(SilkBandwidth::Wb), "config {config}");
315 assert_eq!(r.silk_frames_per_channel, Some(n), "config {config}");
316 }
317 }
318
319 /// CELT-only configs 16..=31 produce no SILK layer, just a CELT
320 /// decode, regardless of channel count.
321 #[test]
322 fn celt_only_routing_has_no_silk() {
323 for config in 16u8..32 {
324 for stereo in [false, true] {
325 let r = route(config, stereo);
326 assert_eq!(
327 r.operating_mode,
328 OperatingMode::CeltOnly,
329 "c={config} s={stereo}"
330 );
331 assert!(!r.silk_layer, "c={config} s={stereo}");
332 assert!(r.celt_layer, "c={config} s={stereo}");
333 assert_eq!(r.silk_bandwidth, None);
334 assert_eq!(r.silk_frames_per_channel, None);
335 assert_eq!(r.total_silk_frames(), 0);
336 assert!(!r.has_per_frame_lbrr_bits());
337 }
338 }
339 }
340
341 /// CELT-only 2.5 ms / 5 ms / 10 ms / 20 ms durations are all
342 /// represented (the four configs per bandwidth group cover the
343 /// four sizes in order).
344 #[test]
345 fn celt_only_frame_sizes_cover_25_to_200() {
346 // Config 16..=19 → NB CELT-only, sizes 25/50/100/200.
347 for (k, &dur) in [25u16, 50, 100, 200].iter().enumerate() {
348 let r = route(16 + k as u8, false);
349 assert_eq!(r.frame_size_tenths_ms, dur);
350 assert_eq!(r.silk_frames_per_channel, None);
351 }
352 }
353
354 /// Mono and stereo route the same way except for the channel
355 /// count and the resulting total SILK-frame count.
356 #[test]
357 fn channel_count_doubles_total_silk_frames_in_stereo() {
358 for config in 0u8..12 {
359 let mono = route(config, false);
360 let stereo = route(config, true);
361 assert_eq!(mono.channel_count(), 1);
362 assert_eq!(stereo.channel_count(), 2);
363 assert_eq!(mono.silk_frames_per_channel, stereo.silk_frames_per_channel);
364 assert_eq!(stereo.total_silk_frames(), 2 * mono.total_silk_frames());
365 }
366 }
367
368 /// §4.2.4 per-frame LBRR flag presence is gated on a SILK layer
369 /// and an Opus duration strictly greater than 20 ms. Verify the
370 /// gate against every Table 2 cell.
371 #[test]
372 fn per_frame_lbrr_gate_matches_section_4_2_4() {
373 for config in 0u8..32 {
374 let r = route(config, false);
375 let expected = r.silk_layer && matches!(r.frame_size_tenths_ms, 400 | 600);
376 assert_eq!(r.has_per_frame_lbrr_bits(), expected, "config {config}");
377 }
378 }
379
380 /// `total_silk_frames` matches `channel_count * silk_frames_per_channel`
381 /// for every SILK-bearing config × {mono, stereo}, and is zero
382 /// for every CELT-only config × {mono, stereo}.
383 #[test]
384 fn total_silk_frames_formula() {
385 for config in 0u8..32 {
386 for stereo in [false, true] {
387 let r = route(config, stereo);
388 match r.silk_frames_per_channel {
389 Some(n) => assert_eq!(
390 r.total_silk_frames(),
391 r.channel_count() * n,
392 "config {config} stereo {stereo}"
393 ),
394 None => assert_eq!(r.total_silk_frames(), 0),
395 }
396 }
397 }
398 }
399
400 /// Concrete dispatch: a 60 ms stereo Hybrid SWB frame
401 /// (config 13, s=1) implies 2 channels × 3 SILK frames each = 6
402 /// regular SILK frames, plus a CELT decode covering the SWB
403 /// bands. SILK runs internally at WB.
404 #[test]
405 fn worked_example_60ms_stereo_hybrid() {
406 // Table 2: config 13 is Hybrid SWB 20 ms (not 60 ms — Hybrid
407 // tops out at 20 ms per Table 2). The 60 ms / 40 ms cells
408 // are SILK-only, not Hybrid.
409 let r = route(13, true);
410 assert_eq!(r.operating_mode, OperatingMode::Hybrid);
411 assert_eq!(r.toc_bandwidth, Bandwidth::Swb);
412 assert_eq!(r.frame_size_tenths_ms, 200);
413 assert_eq!(r.silk_bandwidth, Some(SilkBandwidth::Wb));
414 assert_eq!(r.channel_count(), 2);
415 assert_eq!(r.silk_frames_per_channel, Some(1));
416 assert_eq!(r.total_silk_frames(), 2);
417 // 20 ms doesn't trigger §4.2.4 per-frame LBRR.
418 assert!(!r.has_per_frame_lbrr_bits());
419
420 // For a 60 ms stereo frame the only legal mode is SILK-only
421 // (configs 3, 7, 11). Verify the routing for config 11 (WB
422 // SILK-only 60 ms stereo).
423 let r60 = route(11, true);
424 assert_eq!(r60.operating_mode, OperatingMode::SilkOnly);
425 assert_eq!(r60.frame_size_tenths_ms, 600);
426 assert_eq!(r60.silk_bandwidth, Some(SilkBandwidth::Wb));
427 assert_eq!(r60.silk_frames_per_channel, Some(3));
428 assert_eq!(r60.channel_count(), 2);
429 assert_eq!(r60.total_silk_frames(), 6);
430 assert!(r60.has_per_frame_lbrr_bits());
431 }
432
433 /// `from_toc` passes the channel mapping and TOC bandwidth
434 /// straight through (these are direct copies, not derived
435 /// fields), regardless of the `c` frame-count bits.
436 #[test]
437 fn fields_passed_through_from_toc() {
438 // The `c` bits live in the bottom of the TOC byte. Toggle
439 // them and confirm the routing decision (which is
440 // independent of `c` per §3.2) does not move.
441 for config in 0u8..32 {
442 let base = OpusFrameRouting::from_toc(OpusTocByte::from_byte(config << 3));
443 for c in 1u8..=3 {
444 let r = OpusFrameRouting::from_toc(OpusTocByte::from_byte((config << 3) | c));
445 assert_eq!(r.operating_mode, base.operating_mode);
446 assert_eq!(r.toc_bandwidth, base.toc_bandwidth);
447 assert_eq!(r.frame_size_tenths_ms, base.frame_size_tenths_ms);
448 assert_eq!(r.silk_layer, base.silk_layer);
449 assert_eq!(r.celt_layer, base.celt_layer);
450 assert_eq!(r.silk_bandwidth, base.silk_bandwidth);
451 assert_eq!(r.silk_frames_per_channel, base.silk_frames_per_channel);
452 }
453 }
454
455 // And a sanity check that the `c` decode itself is preserved
456 // on the underlying TOC byte: routing doesn't touch that.
457 let toc_c3 = OpusTocByte::from_byte(0b11);
458 assert_eq!(toc_c3.frame_count_code, FrameCountCode::Arbitrary);
459 }
460
461 /// Stereo / mono flag preserved on the routing even for
462 /// CELT-only frames (which still distinguish mono vs stereo for
463 /// the §4.3.4 dual / intensity stereo decisions later).
464 #[test]
465 fn channel_mapping_preserved_for_celt_only() {
466 let mono = route(20, false);
467 let stereo = route(20, true);
468 assert_eq!(mono.channels, ChannelMapping::Mono);
469 assert_eq!(stereo.channels, ChannelMapping::Stereo);
470 assert_eq!(mono.channel_count(), 1);
471 assert_eq!(stereo.channel_count(), 2);
472 }
473
474 /// Every Table 2 cell produces a routing that satisfies the
475 /// "silk_layer XOR celt_only" / "celt_layer XOR silk_only"
476 /// structural invariants and that silk_bandwidth / frames are
477 /// `Some` iff silk_layer.
478 #[test]
479 fn invariants_hold_across_all_32_configs() {
480 for config in 0u8..32 {
481 for stereo in [false, true] {
482 let r = route(config, stereo);
483 // At least one layer is present.
484 assert!(r.silk_layer || r.celt_layer);
485 // SILK-bearing iff silk_bandwidth is Some.
486 assert_eq!(r.silk_layer, r.silk_bandwidth.is_some());
487 // SILK-bearing iff silk_frames_per_channel is Some.
488 assert_eq!(r.silk_layer, r.silk_frames_per_channel.is_some());
489 // CELT-only ⇔ no SILK.
490 assert_eq!(
491 matches!(r.operating_mode, OperatingMode::CeltOnly),
492 !r.silk_layer
493 );
494 // SILK-only ⇔ no CELT.
495 assert_eq!(
496 matches!(r.operating_mode, OperatingMode::SilkOnly),
497 !r.celt_layer
498 );
499 // Hybrid ⇔ both layers.
500 assert_eq!(
501 matches!(r.operating_mode, OperatingMode::Hybrid),
502 r.silk_layer && r.celt_layer
503 );
504 // Hybrid always pins SILK to WB.
505 if matches!(r.operating_mode, OperatingMode::Hybrid) {
506 assert_eq!(r.silk_bandwidth, Some(SilkBandwidth::Wb));
507 }
508 }
509 }
510 }
511}