Skip to content

Match ordinary Opus decoding to libopus exactly - #247

Open
TouchDown1 wants to merge 3 commits into
mainfrom
codex/opus-plc-bitexact
Open

TouchDown1 wants to merge 3 commits into
mainfrom
codex/opus-plc-bitexact

Conversation

@TouchDown1

@TouchDown1 TouchDown1 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

This replaces the former mixed decoder/PLC change with the first half of a two-PR stack. It is rebased directly onto current main after #246 and is limited to ordinary packet decoding. PLC and recovery exactness now live in #250.

This PR aligns supported ordinary Opus decode output with pinned generic floating-point libopus by adding:

  • decoder-specific CELT arithmetic, PVQ, inverse MDCT, and synthesis ordering;
  • fixed-point SILK decode state and reconstruction;
  • Hybrid and forced-mode transition alignment;
  • libopus-compatible stateful soft clipping and float-to-int16 rounding;
  • focused reference fixtures for CELT spectrum/MDCT, SILK stages, first-frame output, no-loss sequences, and int16 conversion;
  • a blocking RFC 6716/8251 conformance workflow.

There is no public API expansion, encoder behavior change, runtime cgo dependency, or neural PLC/DRED implementation.

Stack and scope

The reference uses generic floating point with fast float approximations enabled, intrinsics/RTCD and neural PLC/DRED disabled, and OPUS_SET_PHASE_INVERSION_DISABLED(0).

Because exact pre-loss decoder history changes the input state seen by #246's approximate PLC, this PR adjusts only the intermediate PLC compatibility assertions needed for the first PR to remain independently green. The stacked #250 replaces those compatibility bounds with exact PLC/reference-state assertions. The final stack does not weaken the bit-exact claim.

Implementation notes

Decoder float32 products that feed reference-ordered additions use explicit rounding barriers. decoderRoundedProduct is deliberately the direct float32(left * right) operation requested in review. Decoder-only arithmetic is isolated from shared encoder helpers; #250 carries the corresponding PLC-only pitch implementation so encoder packet output remains unchanged.

Verification performed before publication

  • go test ./... -count=1 on Windows/amd64
  • CGO_ENABLED=1 go test -race ./... -count=1 -timeout=10m on Linux/amd64 for the final stack
  • ordinary decoder and strict PLC tests on Linux/arm64 under QEMU for the final stack
  • go vet ./...
  • go build ./...
  • go mod verify

The RFC 6716/8251 workflow checks all 120 published bitstream/output-rate/channel combinations against opus_compare and now fails the PR on a mismatch.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Encoder Quality Report

Status: pass

Tier 1 — SNR regression (96 kbps, pion encode → pion decode)

Delta = baseline − current SNR; positive = regression. Fail threshold: 1.5 dB.

Signal SNR (dB) Baseline (dB) Delta Status
chirp 33.8 13.9 -19.9 OK
harmonics 33.2 24.3 -8.9 OK
burst 4.2 4.1 -0.1 OK
shaped_noise 0.7 0.7 -0.0 OK
onset 4.3 4.1 -0.2 OK

Tier 2 — opus_compare vs libopus (96 kbps CBR)

Weighted error: lower is better. The gap reflects pion lacking constrained VBR; libopus ships with it enabled by default.

Signal pion weighted error ↓ libopus weighted error ↓
chirp 0.484478 0.109468
harmonics 0.020853 0.012980
burst 0.715058 2.192852
shaped_noise 0.430925 0.234622
onset 0.520838 0.524139
Run output
=== RUN   TestEncoderQuality
    encoder_quality_test.go:137: loaded baseline: version=1 bitrate=96000 signals=5
=== RUN   TestEncoderQuality/chirp
=== PAUSE TestEncoderQuality/chirp
=== RUN   TestEncoderQuality/harmonics
=== PAUSE TestEncoderQuality/harmonics
=== RUN   TestEncoderQuality/burst
=== PAUSE TestEncoderQuality/burst
=== RUN   TestEncoderQuality/shaped_noise
=== PAUSE TestEncoderQuality/shaped_noise
=== RUN   TestEncoderQuality/onset
=== PAUSE TestEncoderQuality/onset
=== CONT  TestEncoderQuality/chirp
=== CONT  TestEncoderQuality/shaped_noise
=== CONT  TestEncoderQuality/onset
=== CONT  TestEncoderQuality/burst
=== NAME  TestEncoderQuality/onset
    encoder_quality_test.go:159: signal=onset SNR=4.3 dB
    encoder_quality_test.go:166: baseline=4.1 dB delta=-0.2 dB threshold=1.5 dB
=== CONT  TestEncoderQuality/harmonics
=== NAME  TestEncoderQuality/burst
    encoder_quality_test.go:159: signal=burst SNR=4.2 dB
    encoder_quality_test.go:166: baseline=4.1 dB delta=-0.1 dB threshold=1.5 dB
=== NAME  TestEncoderQuality/shaped_noise
    encoder_quality_test.go:159: signal=shaped_noise SNR=0.7 dB
    encoder_quality_test.go:166: baseline=0.7 dB delta=-0.0 dB threshold=1.5 dB
=== NAME  TestEncoderQuality/chirp
    encoder_quality_test.go:159: signal=chirp SNR=33.8 dB
    encoder_quality_test.go:166: baseline=13.9 dB delta=-19.9 dB threshold=1.5 dB
=== NAME  TestEncoderQuality/harmonics
    encoder_quality_test.go:159: signal=harmonics SNR=33.2 dB
    encoder_quality_test.go:166: baseline=24.3 dB delta=-8.9 dB threshold=1.5 dB
--- PASS: TestEncoderQuality (0.00s)
    --- PASS: TestEncoderQuality/onset (0.09s)
    --- PASS: TestEncoderQuality/burst (0.09s)
    --- PASS: TestEncoderQuality/shaped_noise (0.09s)
    --- PASS: TestEncoderQuality/chirp (0.10s)
    --- PASS: TestEncoderQuality/harmonics (0.07s)
PASS
ok  	github.com/pion/opus	0.161s
=== RUN   TestEncoderQualityVsReference
    encoder_conformance_test.go:410: loaded baseline: version=1 bitrate=96000 signals=5
=== RUN   TestEncoderQualityVsReference/chirp
=== PAUSE TestEncoderQualityVsReference/chirp
=== RUN   TestEncoderQualityVsReference/harmonics
=== PAUSE TestEncoderQualityVsReference/harmonics
=== RUN   TestEncoderQualityVsReference/burst
=== PAUSE TestEncoderQualityVsReference/burst
=== RUN   TestEncoderQualityVsReference/shaped_noise
=== PAUSE TestEncoderQualityVsReference/shaped_noise
=== RUN   TestEncoderQualityVsReference/onset
=== PAUSE TestEncoderQualityVsReference/onset
=== CONT  TestEncoderQualityVsReference/chirp
=== CONT  TestEncoderQualityVsReference/shaped_noise
=== CONT  TestEncoderQualityVsReference/burst
=== CONT  TestEncoderQualityVsReference/harmonics
=== NAME  TestEncoderQualityVsReference/shaped_noise
    encoder_conformance_test.go:448: pion quality= weighted_error=0.430925
=== NAME  TestEncoderQualityVsReference/burst
    encoder_conformance_test.go:448: pion quality= weighted_error=0.715058
=== NAME  TestEncoderQualityVsReference/harmonics
    encoder_conformance_test.go:448: pion quality=91.6 weighted_error=0.020853
=== NAME  TestEncoderQualityVsReference/chirp
    encoder_conformance_test.go:448: pion quality= weighted_error=0.484478
=== NAME  TestEncoderQualityVsReference/harmonics
    encoder_conformance_test.go:473: libopus quality=94.7 weighted_error=0.012980
=== CONT  TestEncoderQualityVsReference/onset
=== NAME  TestEncoderQualityVsReference/shaped_noise
    encoder_conformance_test.go:473: libopus quality=13.8 weighted_error=0.234622
=== NAME  TestEncoderQualityVsReference/chirp
    encoder_conformance_test.go:473: libopus quality=57.5 weighted_error=0.109468
=== NAME  TestEncoderQualityVsReference/burst
    encoder_conformance_test.go:473: libopus quality= weighted_error=2.192852
=== NAME  TestEncoderQualityVsReference/onset
    encoder_conformance_test.go:448: pion quality= weighted_error=0.520838
    encoder_conformance_test.go:473: libopus quality= weighted_error=0.524139
--- PASS: TestEncoderQualityVsReference (13.11s)
    --- PASS: TestEncoderQualityVsReference/harmonics (1.69s)
    --- PASS: TestEncoderQualityVsReference/shaped_noise (1.72s)
    --- PASS: TestEncoderQualityVsReference/chirp (1.73s)
    --- PASS: TestEncoderQualityVsReference/burst (1.74s)
    --- PASS: TestEncoderQualityVsReference/onset (1.04s)
PASS
ok  	github.com/pion/opus	15.850s

Baseline: testdata/encoder-quality-baseline.json

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

RFC 6716 / 8251 conformation

Status: pass

The action extracts the RFC 6716 reference implementation, applies the RFC 8251 decoder update patch, and then builds the patched reference tools.

Legend: numeric cells are opus_compare quality percentages; FAIL means the vector did not pass.

Inputs use the shared RFC 6716 / RFC 8251 bitstream corpus; accepted references follow RFC 8251 Section 11.

rate ch 01 02 03 04 05 06 07 08 09 10 11 12
8000 1 91.4 59.7 66.3 75.0 75.4 69.0 76.0 70.0 75.5 86.0 91.0 43.4
8000 2 93.3 57.7 66.3 75.2 75.4 69.0 76.0 70.4 76.2 86.1 93.0 43.6
12000 1 95.6 83.4 71.8 78.8 79.3 74.0 85.1 81.6 84.8 88.2 94.9 65.9
12000 2 96.0 83.5 71.6 79.0 79.4 74.1 85.1 81.8 85.2 88.3 95.8 66.0
16000 1 95.2 92.8 89.7 81.5 81.7 77.2 89.9 86.2 78.8 89.5 96.3 56.5
16000 2 94.7 92.9 89.7 80.6 81.7 77.2 89.8 87.6 78.9 89.6 96.3 56.7
24000 1 96.7 94.1 83.3 95.2 95.2 94.2 94.0 92.4 89.2 97.1 98.0 68.5
24000 2 96.8 94.4 83.3 95.3 95.3 94.2 94.0 93.6 92.1 97.2 98.1 68.6
48000 1 98.4 96.2 96.7 96.5 96.2 96.5 98.1 96.5 98.1 97.7 98.4 99.9
48000 2 99.9 100.0 100.0 100.0 99.8 99.8 99.8 99.8 99.8 99.8 99.8 99.9
Run output
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector06
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector09: Opus quality metric: 76.2 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector05
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector08: Opus quality metric: 70.4 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector04
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector07: Opus quality metric: 76.0 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector03
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector06: Opus quality metric: 69.0 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector02
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector05: Opus quality metric: 75.4 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector01
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector03: Opus quality metric: 66.3 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector12
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector04: Opus quality metric: 75.2 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector11
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector12: Opus quality metric: 43.4 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector10
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector02: Opus quality metric: 57.7 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector09
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector11: Opus quality metric: 91.0 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector08
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector01: Opus quality metric: 93.3 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector07
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector10: Opus quality metric: 86.0 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector06
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector09: Opus quality metric: 75.5 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector05
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector08: Opus quality metric: 70.0 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector04
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector07: Opus quality metric: 76.0 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector03
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector06: Opus quality metric: 69.0 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector02
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector05: Opus quality metric: 75.4 %
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector03: Opus quality metric: 66.3 %
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector04: Opus quality metric: 75.0 %
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector02: Opus quality metric: 59.7 %
Opus conformance matrix
Legend: numeric cells are opus_compare quality percentages; FAIL means the vector did not pass.
Inputs use the shared RFC 6716 / RFC 8251 bitstream corpus; accepted references follow RFC 8251 Section 11.
+----------+----+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| rate     | ch | 01    | 02    | 03    | 04    | 05    | 06    | 07    | 08    | 09    | 10    | 11    | 12    |
+----------+----+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| 8000     | 1  | 91.4  | 59.7  | 66.3  | 75.0  | 75.4  | 69.0  | 76.0  | 70.0  | 75.5  | 86.0  | 91.0  | 43.4  |
| 8000     | 2  | 93.3  | 57.7  | 66.3  | 75.2  | 75.4  | 69.0  | 76.0  | 70.4  | 76.2  | 86.1  | 93.0  | 43.6  |
| 12000    | 1  | 95.6  | 83.4  | 71.8  | 78.8  | 79.3  | 74.0  | 85.1  | 81.6  | 84.8  | 88.2  | 94.9  | 65.9  |
| 12000    | 2  | 96.0  | 83.5  | 71.6  | 79.0  | 79.4  | 74.1  | 85.1  | 81.8  | 85.2  | 88.3  | 95.8  | 66.0  |
| 16000    | 1  | 95.2  | 92.8  | 89.7  | 81.5  | 81.7  | 77.2  | 89.9  | 86.2  | 78.8  | 89.5  | 96.3  | 56.5  |
| 16000    | 2  | 94.7  | 92.9  | 89.7  | 80.6  | 81.7  | 77.2  | 89.8  | 87.6  | 78.9  | 89.6  | 96.3  | 56.7  |
| 24000    | 1  | 96.7  | 94.1  | 83.3  | 95.2  | 95.2  | 94.2  | 94.0  | 92.4  | 89.2  | 97.1  | 98.0  | 68.5  |
| 24000    | 2  | 96.8  | 94.4  | 83.3  | 95.3  | 95.3  | 94.2  | 94.0  | 93.6  | 92.1  | 97.2  | 98.1  | 68.6  |
| 48000    | 1  | 98.4  | 96.2  | 96.7  | 96.5  | 96.2  | 96.5  | 98.1  | 96.5  | 98.1  | 97.7  | 98.4  | 99.9  |
| 48000    | 2  | 99.9  | 100.0 | 100.0 | 100.0 | 99.8  | 99.8  | 99.8  | 99.8  | 99.8  | 99.8  | 99.8  | 99.9  |
+----------+----+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
--- PASS: TestRFC6716Conformance (136.33s)
    --- PASS: TestRFC6716Conformance/vectors (0.00s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector01 (2.31s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector04 (2.47s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector07 (4.25s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector12 (2.16s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector01 (4.89s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector11 (2.50s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector09 (2.36s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector10 (2.71s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector08 (2.28s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector06 (4.76s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector03 (4.05s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector05 (5.12s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector04 (5.02s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector02 (4.63s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector12 (2.51s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector11 (2.86s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector01 (5.57s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector09 (2.70s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector08 (2.59s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector10 (3.13s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector07 (2.16s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector03 (1.99s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector06 (2.38s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector05 (2.62s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector02 (2.31s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector01 (2.92s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector12 (4.27s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector11 (4.91s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector10 (5.33s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector09 (4.53s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector08 (4.40s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector07 (3.64s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector06 (4.08s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector05 (4.41s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector03 (3.40s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector04 (4.28s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector02 (3.98s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector12 (7.48s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector11 (8.54s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector10 (9.20s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector09 (7.88s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector08 (7.71s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector07 (6.39s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector06 (7.14s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector05 (7.77s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector03 (5.90s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector04 (7.51s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector02 (7.03s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector12 (3.78s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector11 (4.24s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector01 (8.45s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector10 (4.60s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector09 (3.96s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector07 (3.15s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector08 (3.82s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector06 (3.57s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector05 (3.87s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector03 (2.95s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector04 (3.70s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector02 (3.47s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector01 (4.20s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector12 (4.82s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector11 (5.56s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector07 (1.86s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector10 (5.96s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector09 (5.10s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector08 (4.97s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector06 (2.05s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector05 (2.21s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector04 (2.13s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector03 (1.66s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector02 (1.98s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector01 (2.40s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector12 (3.95s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector11 (4.51s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector10 (4.86s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector09 (4.19s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector08 (4.11s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector07 (3.47s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector06 (3.80s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector05 (4.17s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector03 (3.16s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector04 (3.98s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector02 (3.75s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector12 (1.99s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector11 (2.35s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector01 (4.50s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector09 (2.20s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector10 (2.51s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector08 (2.07s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector07 (1.75s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector06 (1.92s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector05 (2.10s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector03 (1.56s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector04 (2.01s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector02 (1.85s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector01 (2.35s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector12 (3.80s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector11 (4.40s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector10 (4.73s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector09 (4.09s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector08 (3.96s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector07 (3.28s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector06 (3.67s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector05 (3.97s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector03 (3.06s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector04 (3.81s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector12 (1.94s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector02 (3.54s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector11 (2.29s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector01 (4.34s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector10 (2.43s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector09 (2.10s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector08 (2.00s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector07 (1.69s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector06 (1.88s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector05 (2.05s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector03 (1.56s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector04 (1.95s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector02 (1.41s)
=== RUN   TestRFC6716ConformanceEncoder
=== RUN   TestRFC6716ConformanceEncoder/mono_sine
TestRFC6716ConformanceEncoder/mono_sine: Opus quality metric: 99.0 %
TestRFC6716ConformanceEncoder/mono_sine: Go encoder vs original: quality 30.4 % (weighted error 0.185345, delay 120 samples)
TestRFC6716ConformanceEncoder/mono_sine: reference encoder vs original: below quality threshold, weighted error 0.300608 (delay 312 samples)
=== RUN   TestRFC6716ConformanceEncoder/stereo_tones
TestRFC6716ConformanceEncoder/stereo_tones: Opus quality metric: 99.2 %
TestRFC6716ConformanceEncoder/stereo_tones: Go encoder vs original: below quality threshold, weighted error 0.369246 (delay 120 samples)
TestRFC6716ConformanceEncoder/stereo_tones: reference encoder vs original: below quality threshold, weighted error 0.383133 (delay 312 samples)
=== RUN   TestRFC6716ConformanceEncoder/stereo_wide
TestRFC6716ConformanceEncoder/stereo_wide: Opus quality metric: 99.4 %
TestRFC6716ConformanceEncoder/stereo_wide: Go encoder vs original: below quality threshold, weighted error 0.983215 (delay 120 samples)
TestRFC6716ConformanceEncoder/stereo_wide: reference encoder vs original: below quality threshold, weighted error 0.611077 (delay 312 samples)
=== RUN   TestRFC6716ConformanceEncoder/stereo_broadband_low_bitrate
TestRFC6716ConformanceEncoder/stereo_broadband_low_bitrate: Opus quality metric: 99.9 %
TestRFC6716ConformanceEncoder/stereo_broadband_low_bitrate: Go encoder vs original: below quality threshold, weighted error 1.256102 (delay 20 samples)
TestRFC6716ConformanceEncoder/stereo_broadband_low_bitrate: reference encoder vs original: below quality threshold, weighted error 0.910593 (delay 1712 samples)
--- PASS: TestRFC6716ConformanceEncoder (17.25s)
    --- PASS: TestRFC6716ConformanceEncoder/mono_sine (1.56s)
    --- PASS: TestRFC6716ConformanceEncoder/stereo_tones (1.60s)
    --- PASS: TestRFC6716ConformanceEncoder/stereo_wide (1.59s)
    --- PASS: TestRFC6716ConformanceEncoder/stereo_broadband_low_bitrate (1.57s)
PASS
ok  	github.com/pion/opus	153.588s

@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.57831% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.63%. Comparing base (b8ebd65) to head (474fff6).

Files with missing lines Patch % Lines
decoder.go 79.45% 9 Missing and 6 partials ⚠️
internal/celt/decoder_math.go 93.63% 6 Missing and 4 partials ⚠️
internal/celt/decoder_mdct.go 96.66% 5 Missing and 2 partials ⚠️
internal/silk/decoder_fixed.go 97.24% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #247      +/-   ##
==========================================
- Coverage   94.11%   93.63%   -0.49%     
==========================================
  Files          63       66       +3     
  Lines       11119    11703     +584     
==========================================
+ Hits        10465    10958     +493     
- Misses        453      533      +80     
- Partials      201      212      +11     
Flag Coverage Δ
go 93.63% <94.57%> (-0.49%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@TouchDown1

Copy link
Copy Markdown
Contributor Author

@thomas-vilte ready for review at 198f59df6078d6a99b7be3016a1c2fbabcaf288e. All 21 checks are green, including the full sequential Linux race+coverage corpus (Go 1.24: 23m17s; Go 1.25: 28m39s). The pinned 1,300-scenario / 28,600-step / 10,620-loss corpus is exact before loss, during PLC, and after recovery. ARM64 FMA/order divergence is handled with decoder-only float32 rounding barriers; encoder helpers are unchanged. This remains stacked on the reviewed #246 head d6c4ebdaa1929c9f8080bfa92df657b48fed3ada.

@thomas-vilte thomas-vilte left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 198f59d. I'm requesting changes here, mostly because I don't think this PR is reviewable as one unit anymore.

The diff is large, but the bigger issue isn't the line count by itself. This PR now changes several independent parts of the decoder: MDCT, arithmetic helpers, fixed-point SILK decode, PLC, pitch search, and normal decode paths.

Those are separate numerical-equivalence claims, and when they're all bundled together it's very hard to tell which change caused a mismatch. I don't feel comfortable approving that as one review.

I'd split this into at least:

  • normal decode exactness
  • PLC exactness

The normal decode part can be verified against the existing RFC conformance vectors, and the PLC part against the strict corpus. That makes each PR much easier to reason about and also much easier to revert if something downstream breaks.

This is different from #246, where I argued against splitting. That PR was one algorithm with one acceptance gate. Here the changes are independent enough that splitting actually improves the review.

There are also a few concrete issues I'd want fixed either way:

  1. decoderRoundedProduct should use float32(left * right) rather than the Float32bits round-trip. The explicit conversion is what the Go spec actually guarantees as a rounding barrier, and it generates better code.

  2. TestPLCCorpus now only applies its baseline checks to the first 502 cases even though the corpus has 1300. The strict bit-exact test covers everything, so this isn't a coverage hole today, but the old checks are silently skipped for most of the corpus.

  3. I don't think the strict corpus should run under -race. Locally it's around a 63x slowdown for a deterministic single-goroutine numeric test, and the current 30-minute timeout leaves very little margin.

One other thing I'd make explicit: #246 introduced shared pitch helpers, while this PR splits the decoder copies back out. If that's required for bit-exactness, that's fine, but I think it should be an intentional design decision rather than something that happens implicitly across two PRs.

I'm happy to review the normal-decode part as soon as it's split out.

Comment thread internal/celt/decoder_math.go Outdated
Comment thread decoder_plc_corpus_test.go
Comment thread .github/.ci.conf Outdated
Comment thread internal/celt/plc.go Outdated
@TouchDown1
TouchDown1 force-pushed the codex/celt-periodic-plc branch from d6c4ebd to a403293 Compare September 9, 2026 21:14
Base automatically changed from codex/celt-periodic-plc to main September 9, 2026 21:27
@TouchDown1
TouchDown1 force-pushed the codex/opus-plc-bitexact branch from 198f59d to 7364466 Compare September 12, 2026 05:08
@TouchDown1 TouchDown1 changed the title Match libopus decoding and PLC output exactly Match ordinary Opus decoding to libopus exactly Sep 12, 2026

@thomas-vilte thomas-vilte left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed 474fff6 again. The split resolves the points from my previous review: decoderRoundedProduct is now float32(left * right), the repo-wide timeout is gone, conformance blocks, and the ordinary-decode diff stands on its own.

I found one remaining issue tied to this PR's compatibility claim: softClip is not bit-exact on arm64 (softclip.go:41, :79, :84). Go can fuse x + a*x*x and a += a*2.4e-7, while libopus v1.6.1 rounds the intermediate products separately (src/opus.c:82, :134, :139). I compared both implementations on 4,800 clipping frames: amd64 matched exactly, but arm64 produced 158 different int16 PCM frames. The strict corpus does not exercise clipping, so it would not catch this. Please wrap the three products in float32(...) and add a clipped libopus fixture.

I also left inline notes on two transition differences that predate this PR. I opened #251 for those; I only expect the compatibility claim in the description to be scoped accordingly here. The two PLC test relaxations are acceptable to me if #250 follows immediately.

One unrelated note for clarity: lshiftSat32 clamps after shifting instead of before it. That came from my #153, and I'll fix it separately on main.

Comment thread decoder.go
channelCount: d.channels,
})
}
if previousMode == configurationModeHybrid &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the order here differs from libopus for a Hybrid -> SILK packet that also carries SILK -> CELT redundancy. We reset CELT and decode the redundant frame above (1133-1145), then decode the Hybrid -> SILK fade-out. libopus decodes the silence frame first (src/opus_decoder.c:622-625), then resets CELT and handles redundancy (from :636). That means our fade-out uses a fresh CELT state rather than the state from the Hybrid packet. This predates the PR and is tracked in #251.

// Exact received-frame reconstruction changes the state consumed by
// the approximate PLC from #246. Bound individual drift while the
// aggregate gate below still requires the periodic PLC improvement.
limit := max(b.RMSE+1, b.RMSE*1.5)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am okay with this temporarily because #250 restores b.RMSE+1. On its own, though, this allows each step's RMSE to grow to 1.5x its baseline. Please make sure #250 lands immediately after this one so the relaxed check does not remain on main.

Comment thread decoder.go
@@ -1478,14 +1522,14 @@ func (d *Decoder) applySilkRedundancyFades(channelCount int) {
frameStart := fade.startSample * channelCount
if fade.celtToSilk {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that libopus only overlays the CELT -> SILK redundant frame when st->prev_mode != MODE_SILK_ONLY || st->prev_redundancy (src/opus_decoder.c:650). That avoids using redundancy when the first frame of the transition was lost. We apply every celtToSilk fade unconditionally, and the Hybrid path has the same gap. This also predates the PR and is tracked in #251.

Comment thread internal/celt/plc_test.go
}
}

func requireCompatiblePitch(t *testing.T, reference, actual int, label string) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, this test uses InDelta(ref, pitch, 1). With the arguments here, it only verifies that the lag is somewhere in [100, 720], so any valid lag passes and #247 alone stops testing pitch. #250 makes this exact, which I prefer. Please land the two PRs together, or keep the +/-1 assertion here until #250 is merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants