Skip to main content

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}