Skip to content

HYBRID-mode decode is not bit-exact vs libopus (SNR 18–54 dB; SILK-only path is bit-exact) #8

Description

@ekhodzitsky

HYBRID-mode decode is not bit-exact vs libopus (SNR 18–54 dB; SILK-only path is bit-exact)

Summary

Differential testing against C libopus 1.6.1 on real speech shows that opus-rs 0.1.23 decodes HYBRID-mode (SWB/FB) packets approximately: after compensating a constant −3-sample offset (see "alignment" below), correlation with libopus output is 0.99–1.0 but SNR is only 18–54 dB, i.e. the output is audibly close but not conformance-grade. For comparison, the SILK-only path in the same build is bit-exact (infinite SNR) against libopus on identical source material, so the divergence appears to live in the CELT layer that hybrid frames add on top of SILK.

Environment

  • opus-rs 0.1.23 (crates.io tarball, sha256 1cc978697f3ada0ad98b323b9d85e85c6b08d478830148636c19b6d5f2ac9b85, verified against the sparse index; used as a path dep with identical content)
  • rustc 1.96.0, macOS aarch64 (Apple Silicon), release build
  • ffmpeg 8.1.1 with libopus 1.6.1 (reference encoder + decoder)
  • Source speech: Russian speech WAV (16 kHz mono), from the Golos corpus

Reproduction

  1. Generate a hybrid-mode file (48 kHz mono 32 kbps maps almost entirely to hybrid packets):

    ffmpeg -y -i speech.wav -c:a libopus -ar 48000 -ac 1 -b:a 32k hybrid.ogg
    # reference decode with C libopus:
    ffmpeg -y -c:a libopus -i hybrid.ogg -f f32le -acodec pcm_f32le -ar 48000 ref.f32
  2. Decode the same packets with opus-rs (packets.bin = [u16le len][packet]… audio packets in stream order, extracted from the ogg with the ~15-line demuxer in "Reproduction assets" below; ref.f32 comes from the ffmpeg reference command above):

    use opus_rs::OpusDecoder;
    
    fn main() {
        let a: Vec<String> = std::env::args().collect();
        let raw = std::fs::read(&a[1]).unwrap();           // packets.bin
        let refr: Vec<f32> = std::fs::read(&a[2]).unwrap() // ref.f32
            .chunks_exact(4).map(|c| f32::from_le_bytes(c.try_into().unwrap())).collect();
        let mut dec = OpusDecoder::new(48000, 1).unwrap();
        let mut out: Vec<f32> = Vec::new();
        let mut pos = 0;
        while pos + 2 <= raw.len() {
            let n = u16::from_le_bytes([raw[pos], raw[pos + 1]]) as usize;
            pos += 2;
            let pkt = &raw[pos..pos + n];
            pos += n;
            let mut buf = vec![0.0f32; 960];      // every packet here is 20 ms @ 48 kHz
            let got = dec.decode(pkt, 960, &mut buf).unwrap();
            out.extend_from_slice(&buf[..got]);
        }
        let out = &out[312..];                     // OpusHead pre-skip
        let n = out.len().min(refr.len());
        let (mut dot, mut ex, mut ey, mut sx, mut sy) = (0f64, 0f64, 0f64, 0f64, 0f64);
        for i in 0..n {
            let (o, r) = (out[i] as f64, refr[i] as f64);
            dot += o * r; ex += (o - r).powi(2); ey += r * r; sx += o * o; sy += r * r;
        }
        println!("corr={:.4} snr_db={:.1}", dot / (sx.sqrt() * sy.sqrt()), 10.0 * (ey / ex).log10());
    }
  3. Optionally compensate the constant −3-sample offset (opus-rs SILK/hybrid output leads libopus by 3 samples @ 48 kHz) before comparing — e.g. out = &out[..n-3]; refr = &refr[3..n];.

Expected vs actual

For a faithful port of libopus 1.6, decoding the same packets should be bit-identical (or within float rounding, SNR > 80 dB).

Actual on 4 different speech files (48 kHz mono 32 kbps, hybrid packets; opus-rs vs libopus 1.6.1):

file corr raw SNR raw best lag corr@lag SNR@lag
golos_00 0.8936 6.7 dB −3 0.9997 31.8 dB
golos_01 0.9862 15.6 dB −3 1.0000 53.5 dB
golos_02 0.9883 16.3 dB −1 0.9921 18.0 dB
golos_03 0.9116 7.5 dB −3 0.9999 37.6 dB

Fresh reproduction run (golos_00): samples=192000 corr=0.8936 snr_db=6.7 raw; corr=0.9997 SNR=31.8 dB at lag −3.

Control: SILK-only encodes of the same sources (16 kHz mono 24 kbps, 8 kHz mono 16 kbps) decode bit-exactly (infinite SNR at lag −3/−6), so the harness and alignment are sound — the gap is specific to the CELT layer inside hybrid frames.

Suspected root cause

SILK-only output is bit-exact, so the divergence is most likely in the CELT decoder path used for the high bands of hybrid frames (src/celt.rs, src/bands.rs, src/pvq.rs in 0.1.23) — e.g. PVQ/band-energy quantization or the final anti-collapse/comb-filter steps differing from libopus's float build. No single-line candidate identified; flagging the mode-specific conformance gap with a reproducer.

Impact

Any consumer decoding real-world Ogg/Opus (voice notes, music, WebRTC recordings at ≥ ~24 kbps) gets hybrid/CELT content; non-conformance-grade output means degraded (if only slightly) and unverifiable audio, and it blocks using opus-rs where bit-exact behavior vs libopus is required (transcoding pipelines, conformance testing, archival).

Related: #2/#3 ("Hybrid mode produces broken output", fixed) and #4 ("degraded audio quality", fixed) — as of 0.1.23 hybrid output is no longer broken, this issue tracks the remaining gap to bit-exactness.

Reproduction assets

Self-contained: the ffmpeg command above generates the input file; packets.bin and ref.f32 are derived from it with any Ogg demuxer. The exact extractor used for this report (~15 lines, Python):

import struct, sys
data = open(sys.argv[1], 'rb').read()          # hybrid.ogg
out, pos, partial, n = open(sys.argv[2], 'wb'), 0, b'', 0
while True:
    i = data.find(b'OggS', pos)
    if i < 0 or i + 27 > len(data): break
    nseg = data[i + 26]
    segs = data[i + 27:i + 27 + nseg]
    p = i + 27 + nseg
    for s in segs:
        partial += data[p:p + s]; p += s
        if s < 255:
            if n >= 2:                          # skip OpusHead / OpusTags
                out.write(struct.pack('<H', len(partial))); out.write(partial)
            n += 1; partial = b''
    pos = p

(Pre-generated binary inputs — the .ogg, packets.bin, ref.f32 used for the numbers above — are kept as repro-01.zip; happy to attach or link them on request.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions