Skip to content

Implement periodic CELT packet loss concealment and recovery - #246

Merged
thomas-vilte merged 10 commits into
mainfrom
codex/celt-periodic-plc
Sep 9, 2026
Merged

thomas-vilte merged 10 commits into
mainfrom
codex/celt-periodic-plc

Conversation

@TouchDown1

@TouchDown1 TouchDown1 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Implement standard, non-neural CELT periodic PLC and its recovery state machine:

  • bounded 2048-sample pre-deemphasis history, shared pitch-search scratch and 24th-order LPC excitation/synthesis;
  • repeated-loss attenuation, synthesis-energy/NaN rejection, and noise fallback after 100 ms or when periodic PLC is unavailable;
  • background energy tracking, two-received-frame readiness after noise, and prefilter/TDAC overlap repair;
  • duration-aware inter-energy recovery prediction, applied with the matching PLC history instead of as an isolated attenuation patch.

The existing noise fallback is aligned with libopus too: it synthesizes the decoder output-channel count, floors decay at tracked background energy, does not mirror mono log-energy into an inactive channel, and advances the reference LCG directly from zero. TestPLCCorpus, the long-loss/noise cases, and the same-build Linux/macOS comparison exercise this path.

No public API or runtime dependency changes; no cgo. SILK PLC/encoder algorithms are unchanged.

Stacked on #245 (c0d7ee63cecdc35aa81b83cbde40c70148da9e74): this PR targets codex/short-celt-plc so its diff contains only the periodic PLC follow-up. Retarget to main after #245 lands; neither PR should be merged automatically.

Evidence

Reference: Xiph libopus 22244de5a79bd1d6d623c32e72bf1954b56235be (1.6.1), float, intrinsics/neural PLC/DRED disabled. Observation-only and unchanged reference builds emit byte-identical fixtures, verified before recording internal states.

502 deterministic streams / 11,044 steps cover periodic/variable-frequency/decaying signals, noise, impulses, silence, all five output rates, mono/stereo conversion and switching, short and mixed recovery frames, long loss bursts, one/two received frames between losses, and mode transitions/Hybrid.

The corpus explicitly enables signaled intensity-stereo phase inversion in libopus (OPUS_SET_PHASE_INVERSION_DISABLED(0)) to match Pion's existing policy. libopus 1.6.1 otherwise disables it by default for mono output, causing different pre-loss PCM and pitch-search input. No PLC mode is forced and Pion's no-loss behavior is unchanged.

Weighted RMSE vs reference, int16 units:

Profile Loss: baseline -> current Recovery: baseline -> current
Periodic / frequency change / decay 1562.55 -> 3.56 1020.25 -> 0.74
Noise / impulse / silence 672.43 -> 32.81 544.18 -> 11.07
SILK -> CELT 1868.41 -> 515.58 675.84 -> 0.05
Hybrid 1150.47 -> 1150.47 887.85 -> 887.85
CELT -> SILK 1601.58 -> 1601.58 973.33 -> 973.33

Every step must remain within baseline RMSE + one int16 unit; every periodic-signal scenario must at least halve aggregate loss/recovery RMSE. Pre-loss/no-loss PCM hashes must stay unchanged. Mode/duration/readiness are checked independently from PCM against reference traces; energy tolerance is 0.001 log2 units, pitch interpolation tolerance one sample.

The PLC quality CI additionally builds the unchanged base and current code on the same Linux/macOS runner with the same compiler and race mode, then enforces the exact no-loss hashes and the same loss/recovery gates. ARM64 ordinary/race snapshots are recorded from the old commit, not from new-code output; this avoids confusing existing compiler-dependent rounding with a regression. Independent pitch/downsampling/windowed-autocorrelation/LPC reference fixtures and decoder reset/isolation checks are included.

This is not bit-exact or perceptual parity: peak differences remain (periodic loss 440, noise/impulse loss 2459), and SILK-based loss remains substantially different. The original #245 short fixture now differs by at most 3 int16 units during loss and 1 during recovery.

Independent energy and recovery-boundary measurements on the same corpus:

Profile Aggregate relative energy error: before -> after Recovery-boundary error RMSE (int16): before -> after
Periodic/changing frequency/decay 109.134% -> 0.00513% 477.82 -> 3.33
Noise/impulse/silence 69.583% -> 0.80401% 772.22 -> 74.92
SILK-to-CELT 84.557% -> 5.81858% 343.09 -> 0.00
Hybrid 32.191% -> 32.191% 281.67 -> 281.67
CELT-to-SILK 35.165% -> 35.165% 418.14 -> 418.14

Energy error is sum(abs(E_step - E_reference_step)) / sum(E_reference_step) over loss/recovery steps, with E = sum(PCM^2). Boundary error compares the first-channel last-lost-sample -> first-received-sample jump against the same reference jump (not against zero). These are separate measurements, not additional relaxed acceptance thresholds or a listening-test score.

Validation and cost

Local Ubuntu WSL, Go 1.26.1:

  • go test ./..., go vet ./..., go build ./..., go mod verify: PASS.
  • golangci-lint 2.10.1: 0 issues.
  • go test -race ./...: PASS.
  • PLC-sequence fuzz 30 s (31,678 executions), decoder fuzz 30 s (320,649 executions): PASS.
  • RFC 6716 / 8251 decoder matrix and conformance encoder tests: PASS (actual test exit 0, not just the informational workflow status).
  • All measured paths after warm-up: 0 B/op, 0 allocs/op.
  • Resident amd64 sizes: Decoder 744 -> 18,472 B, decoderScratch 117,696 -> 140,936 B, �ncoderScratch 8,488 -> 9,640 B; a materialized decoder retains about 41 KiB more bounded PLC state/scratch.

Median of 3 x 200 iterations, 48 kHz / 20 ms on Ryzen 9 9950X3D:

Operation Mono before -> after (us) Stereo before -> after (us)
Normal decode 25.43 -> 25.20 45.00 -> 43.53
First loss 12.23 -> 60.69 22.90 -> 103.12
Repeated loss 12.76 -> 23.15 22.67 -> 51.24
Noise fallback 12.33 -> 12.04 23.30 -> 22.71

First-loss CPU increases because pitch/LPC analysis replaces unconditional noise. Small normal-decode differences are measurement noise, not a speedup claim. Exact regeneration, baseline provenance, measurements and remaining limitations are documented in testdata/short-plc/README.md. The translated PLC retains Xiph/CSIRO/Gregory Maxwell BSD notices.

@github-actions

github-actions Bot commented Sep 8, 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.484629 0.109468
harmonics 0.020853 0.012980
burst 0.715084 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/burst
=== 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
=== CONT  TestEncoderQuality/onset
=== 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
=== 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/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
--- PASS: TestEncoderQuality (0.00s)
    --- PASS: TestEncoderQuality/burst (0.13s)
    --- PASS: TestEncoderQuality/chirp (0.14s)
    --- PASS: TestEncoderQuality/harmonics (0.15s)
    --- PASS: TestEncoderQuality/shaped_noise (0.15s)
    --- PASS: TestEncoderQuality/onset (0.08s)
PASS
ok  	github.com/pion/opus	0.216s
=== 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/onset
=== NAME  TestEncoderQualityVsReference/burst
    encoder_conformance_test.go:448: pion quality= weighted_error=0.715084
=== NAME  TestEncoderQualityVsReference/onset
    encoder_conformance_test.go:448: pion quality= weighted_error=0.520838
=== NAME  TestEncoderQualityVsReference/shaped_noise
    encoder_conformance_test.go:448: pion quality= weighted_error=0.430925
=== NAME  TestEncoderQualityVsReference/chirp
    encoder_conformance_test.go:448: pion quality= weighted_error=0.484629
=== NAME  TestEncoderQualityVsReference/burst
    encoder_conformance_test.go:473: libopus quality= weighted_error=2.192852
=== CONT  TestEncoderQualityVsReference/harmonics
=== NAME  TestEncoderQualityVsReference/onset
    encoder_conformance_test.go:473: libopus quality= weighted_error=0.524139
=== NAME  TestEncoderQualityVsReference/chirp
    encoder_conformance_test.go:473: libopus quality=57.5 weighted_error=0.109468
=== NAME  TestEncoderQualityVsReference/shaped_noise
    encoder_conformance_test.go:473: libopus quality=13.8 weighted_error=0.234622
=== NAME  TestEncoderQualityVsReference/harmonics
    encoder_conformance_test.go:448: pion quality=91.6 weighted_error=0.020853
    encoder_conformance_test.go:473: libopus quality=94.7 weighted_error=0.012980
--- PASS: TestEncoderQualityVsReference (12.29s)
    --- PASS: TestEncoderQualityVsReference/burst (2.40s)
    --- PASS: TestEncoderQualityVsReference/onset (2.41s)
    --- PASS: TestEncoderQualityVsReference/chirp (2.41s)
    --- PASS: TestEncoderQualityVsReference/shaped_noise (2.41s)
    --- PASS: TestEncoderQualityVsReference/harmonics (1.21s)
PASS
ok  	github.com/pion/opus	15.910s

Baseline: testdata/encoder-quality-baseline.json

@TouchDown1
TouchDown1 force-pushed the codex/celt-periodic-plc branch from 78cb47d to cca94d4 Compare September 8, 2026 18:47
@github-actions

github-actions Bot commented Sep 8, 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.1 75.0 67.8 76.0 70.0 75.5 85.9 91.0 43.4
8000 2 93.3 57.6 66.1 75.3 75.2 67.9 76.0 70.4 76.2 86.0 93.0 43.7
12000 1 95.6 83.4 71.8 79.1 77.0 69.0 85.1 81.6 84.8 88.1 94.9 66.0
12000 2 96.0 83.3 71.3 79.2 77.3 69.1 85.1 81.8 85.2 87.0 95.8 66.1
16000 1 95.3 91.4 88.1 81.6 77.2 68.9 89.9 86.2 78.8 89.5 96.3 56.5
16000 2 94.7 90.7 88.1 80.6 77.6 69.1 89.8 87.6 78.9 87.5 96.4 56.7
24000 1 96.7 92.0 83.2 85.9 77.5 68.4 93.9 92.4 89.2 95.4 97.9 68.5
24000 2 96.8 90.6 82.8 86.1 77.8 68.8 93.9 93.5 92.1 87.7 98.1 68.6
48000 1 98.4 92.1 87.7 85.9 77.4 68.3 98.1 96.2 95.9 96.0 98.4 88.8
48000 2 99.8 90.6 87.8 86.1 77.7 68.6 99.6 93.7 94.4 87.7 99.7 88.9
Run output
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector02
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector05: Opus quality metric: 75.2 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector01
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector03: Opus quality metric: 66.1 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector12
TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector04: Opus quality metric: 75.3 %
=== 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.6 %
=== 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/testvector09: Opus quality metric: 75.5 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector06
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector10: Opus quality metric: 85.9 %
=== 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: 67.8 %
=== CONT  TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector02
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector05: Opus quality metric: 75.0 %
=== CONT  TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector05
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector04: Opus quality metric: 75.1 %
=== CONT  TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector04
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector03: Opus quality metric: 66.3 %
=== CONT  TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector03
TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector02: Opus quality metric: 59.7 %
=== CONT  TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector02
TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector03: Opus quality metric: 88.1 %
TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector05: Opus quality metric: 77.6 %
TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector04: Opus quality metric: 80.6 %
TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector02: Opus quality metric: 90.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.1  | 75.0  | 67.8  | 76.0  | 70.0  | 75.5  | 85.9  | 91.0  | 43.4  |
| 8000     | 2  | 93.3  | 57.6  | 66.1  | 75.3  | 75.2  | 67.9  | 76.0  | 70.4  | 76.2  | 86.0  | 93.0  | 43.7  |
| 12000    | 1  | 95.6  | 83.4  | 71.8  | 79.1  | 77.0  | 69.0  | 85.1  | 81.6  | 84.8  | 88.1  | 94.9  | 66.0  |
| 12000    | 2  | 96.0  | 83.3  | 71.3  | 79.2  | 77.3  | 69.1  | 85.1  | 81.8  | 85.2  | 87.0  | 95.8  | 66.1  |
| 16000    | 1  | 95.3  | 91.4  | 88.1  | 81.6  | 77.2  | 68.9  | 89.9  | 86.2  | 78.8  | 89.5  | 96.3  | 56.5  |
| 16000    | 2  | 94.7  | 90.7  | 88.1  | 80.6  | 77.6  | 69.1  | 89.8  | 87.6  | 78.9  | 87.5  | 96.4  | 56.7  |
| 24000    | 1  | 96.7  | 92.0  | 83.2  | 85.9  | 77.5  | 68.4  | 93.9  | 92.4  | 89.2  | 95.4  | 97.9  | 68.5  |
| 24000    | 2  | 96.8  | 90.6  | 82.8  | 86.1  | 77.8  | 68.8  | 93.9  | 93.5  | 92.1  | 87.7  | 98.1  | 68.6  |
| 48000    | 1  | 98.4  | 92.1  | 87.7  | 85.9  | 77.4  | 68.3  | 98.1  | 96.2  | 95.9  | 96.0  | 98.4  | 88.8  |
| 48000    | 2  | 99.8  | 90.6  | 87.8  | 86.1  | 77.7  | 68.6  | 99.6  | 93.7  | 94.4  | 87.7  | 99.7  | 88.9  |
+----------+----+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
--- PASS: TestRFC6716Conformance (110.77s)
    --- PASS: TestRFC6716Conformance/vectors (0.00s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector07 (1.58s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector01 (2.03s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector01 (4.39s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector10 (4.56s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector09 (3.87s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector08 (3.67s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector07 (3.11s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector06 (3.34s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector05 (3.65s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector04 (3.46s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector03 (2.75s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector12 (1.78s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector02 (3.17s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector11 (2.06s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector09 (1.96s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector01 (4.03s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector10 (2.28s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector08 (1.86s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector12 (6.46s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector11 (7.29s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector09 (6.79s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector10 (7.87s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector07 (5.49s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector08 (6.66s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector06 (6.15s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector05 (6.69s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector03 (5.11s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector04 (6.45s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector02 (5.99s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector12 (3.23s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_2/testvector01 (7.33s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector11 (3.70s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector10 (4.06s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector09 (3.47s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector08 (3.35s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector07 (2.81s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector06 (3.12s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector05 (3.38s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector03 (2.57s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector04 (3.23s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector02 (3.02s)
        --- PASS: TestRFC6716Conformance/vectors/rate_48000/channels_1/testvector01 (3.77s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector12 (4.17s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector11 (4.86s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector10 (5.25s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector09 (4.50s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector08 (4.39s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector07 (3.66s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector06 (4.07s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector03 (3.31s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector05 (4.39s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector04 (4.19s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector12 (2.11s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector02 (3.94s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector11 (2.48s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_2/testvector01 (4.85s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector10 (2.68s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector09 (2.36s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector08 (2.25s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector07 (1.87s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector06 (2.12s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector04 (2.24s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector05 (2.26s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector03 (1.70s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector02 (1.98s)
        --- PASS: TestRFC6716Conformance/vectors/rate_24000/channels_1/testvector01 (2.51s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector12 (3.64s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector11 (4.26s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector10 (4.57s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector09 (3.95s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector08 (3.83s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector07 (3.22s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector12 (1.84s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector11 (2.18s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector06 (3.55s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector09 (2.05s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector10 (2.41s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector07 (1.66s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector08 (1.95s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector06 (1.85s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector05 (2.02s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector03 (1.45s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector04 (1.89s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector02 (1.80s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_1/testvector01 (2.25s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector06 (1.75s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector12 (3.44s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector05 (1.86s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector04 (1.75s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_2/testvector11 (4.18s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector03 (1.39s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector02 (1.63s)
        --- PASS: TestRFC6716Conformance/vectors/rate_12000/channels_1/testvector01 (2.24s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector12 (3.30s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector11 (3.88s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector10 (4.26s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector09 (3.71s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector08 (3.48s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector07 (2.92s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector06 (3.23s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector05 (3.48s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector03 (2.62s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector04 (3.35s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector12 (1.68s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector02 (3.08s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector11 (2.00s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_2/testvector01 (3.85s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector09 (1.88s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector10 (2.19s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector08 (1.79s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector07 (1.51s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector06 (1.66s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector05 (1.80s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector04 (1.67s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector03 (1.33s)
        --- PASS: TestRFC6716Conformance/vectors/rate_8000/channels_1/testvector02 (1.57s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector03 (2.89s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector05 (3.73s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector04 (3.61s)
        --- PASS: TestRFC6716Conformance/vectors/rate_16000/channels_2/testvector02 (3.14s)
=== RUN   TestRFC6716ConformanceEncoder
=== RUN   TestRFC6716ConformanceEncoder/mono_sine
TestRFC6716ConformanceEncoder/mono_sine: Opus quality metric: 99.9 %
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.9 %
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.9 %
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.39s)
    --- PASS: TestRFC6716ConformanceEncoder/mono_sine (1.37s)
    --- PASS: TestRFC6716ConformanceEncoder/stereo_tones (1.39s)
    --- PASS: TestRFC6716ConformanceEncoder/stereo_wide (1.39s)
    --- PASS: TestRFC6716ConformanceEncoder/stereo_broadband_low_bitrate (1.37s)
PASS
ok  	github.com/pion/opus	128.171s

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.11%. Comparing base (7aa7a85) to head (a403293).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #246      +/-   ##
==========================================
+ Coverage   93.51%   94.11%   +0.60%     
==========================================
  Files          61       63       +2     
  Lines       10973    11119     +146     
==========================================
+ Hits        10261    10465     +204     
+ Misses        503      453      -50     
+ Partials      209      201       -8     
Flag Coverage Δ
go 94.11% <100.00%> (+0.60%) ⬆️

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: all 19 current checks are green at 9e3044b, including same-compiler/same-race before-after comparisons on Linux and macOS. This is the standard non-neural periodic/noise CELT PLC follow-up, stacked on pending #245; it does not change the public API or its accepted durations. On the pinned 502-stream corpus, periodic loss RMSE improves 1562.55 -> 3.56 and recovery 1020.25 -> 0.74, with no per-step regressions and unchanged no-loss PCM. The description includes energy/recovery-boundary measurements, exact pins and CPU costs (first loss is more expensive; warm paths remain allocation-free). SILK-based/Hybrid PLC residuals and non-bit-exact limitations are stated explicitly. Please review the history/postfilter-overlap handling and recovery state transitions in particular.

@TouchDown1

Copy link
Copy Markdown
Contributor Author

Downstream integration validation at 9e3044b (2026-09-09).

We tested this exact revision in our experimental Go receive pipeline and call executor, using Go 1.26.1 on Linux/amd64 with CGO_ENABLED=0.

Module consumption and build: PASS

  • Downloaded the immutable commit through github.com/pion/opus with initially empty GOMODCACHE and GOCACHE, GOWORK=off, GOTOOLCHAIN=local, GOPROXY=https://proxy.golang.org,direct, and GOSUMDB=sum.golang.org. No local Pion replacement or pre-populated cache was used.
  • Resolved version: v0.1.1-0.20260908190641-9e3044b1a141.
  • Module checksum: h1:Wcz2+z/CSdx/9re8k6smyKPY2nQFlD+on4hSCKd4G8I=.
  • Consumer and executor tests, vet, builds, and module verification passed. The rebuilt executor's go version -m confirms this exact dependency.

Short PLC through the actual receive backend: PASS

After priming with a synthetic 20 ms CELT packet, the 16 kHz mono backend accepts DecodePLC output lengths of 40, 80, 160, and 320 int16 samples (2.5, 5, 10, and 20 ms). Two separate decoder instances produce matching PLC and recovery output. Invalid lengths 0, 79, and 81 return an error, preserve the caller's output, and leave the next decoded packet equal to a control that did not receive the failed PLC request. The duration/API contract comes from stacked #245; #246 improves the concealment and recovery implementation without changing that contract.

Application-level compatibility is still open

In a separate offline codec-transition experiment, a 5 ms PLC call produces the required 80 samples. Comparing its first 40 samples with a saved original transition prefix gives 27 unequal samples, maximum absolute error 2 in int16 units. The priming packet itself differs from the saved original at 13/320 samples, maximum error 1. These are preliminary consumer observations: the original harness and transition-state contract are still being qualified, so this is not a bit-exact claim or an attribution of those residuals to this PR.

The complete MLow -> Opus -> MLow PCM comparison remains failing (913/960, 89/320, and 956/960 unequal samples respectively). MLow PLC and the original reset/overlap sequence remain consumer-side work; the new PLC has not yet been wired into that transition path. The public PLC API returns quantized int16 output and provides no float PLC or state snapshot/restore API; integration must respect those boundaries. No live-call validation was performed with this rebuilt binary.

@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 9e3044b. There's one blocking issue from my side; otherwise this looks good and I'd be happy to approve once that's fixed.

On the size of the PR: GitHub shows 39 files and +1417/-32, but the production diff is really +270/-21 across 8 files, and 220 of those added lines are the two new self-contained files, plc.go and recovery_energy.go.

I don't think this needs to be split. The pitch-scratch refactor itself is small — mostly moving fields between structs and changing two signatures — and pulling it into a separate PR wouldn't really make the PLC change easier to review.

I also went through the periodic path line by line against libopus v1.6.1 (22244de5 resolves to that tag) and didn't find any behavioral differences. I checked the periodic/noise decision, the decay calculation and offsets, extrapolation_len = N + overlap, the S1 source after the buffer move, the float explosion check, the gain calculation, autocorrelation and lag windowing, FIR/IIR state layout, background-energy updates, and the recovery-energy path.

The deferred plc.fold also looks right to me. It matches libopus's prefilter_and_fold flag, and it reads the postfilter parameters before updatePostfilterState changes them, so it still uses the pre-loss values.

The one thing I'd block on is:

  1. decoder_plc_corpus_test.go:120

    float64(delta * delta) does the multiplication as int before converting to float64. On 32-bit, a delta of 65535 overflows and turns negative, which can make the RMSE NaN.

    This matters here because the repo runs test-i386 for Go 1.24 and 1.25, and TestPLCCorpus isn't gated out there. The current baseline doesn't hit it — the largest peak is 34367, below the 46341 overflow point — but a louder corpus sample could.

    Please use:

    float64(delta) * float64(delta)

    like the short PLC test already does.

A few other things I'd like to see before merging, but I wouldn't block on them:

  1. Please call out the noise-path changes in the PR description. decodeLostFrame isn't only adding periodic PLC; the existing noise fallback also changes in four places:

    • info.channelCount = info.outputChannelCount
    • the max(plc.background...) floor on energy decay
    • removal of the mono previousLogE mirror
    • removal of the seed == 0 fallback

    I checked all four against v1.6.1 and they look correct, but they're still behavior changes to existing code. It'd be good to mention them explicitly and point to the tests that exercise the noise path.

  2. I'd also document the resident-memory cost next to the warm allocation benchmarks.

    On amd64 I get:

    • Decoder: 744 → 18472 B
    • decoderScratch: 117696 → 140936 B
    • encoderScratch: 8488 → 9640 B

    The decoder increase is basically the new plcState, and once scratchBuffer() is materialized that's around 41 KiB more per decoder instance. That's perfectly compatible with 0 allocs/op when warm; I just think the memory tradeoff should be visible.

A few smaller nits:

  • plcMeasurement.Energy and .Boundary are written to the baseline but never asserted, and summarize.mjs only reads Peak. I'd drop them.
  • baseline.json is uncompressed at about 1.5 MB while the two arm64 baselines are already gzipped at around 512 KB. I'd gzip this one too.
  • It'd be worth documenting that GOARCH values outside amd64/arm64 fall back to the amd64 golden even though the test compares byte-exact PCM hashes. Everything is green today; this is mostly a maintenance note for future Go upgrades.
  • regenerate.sh doesn't verify the libopus SHA or that the checkout is clean. Checking the pinned commit would make the fixture provenance stronger.

The plc-quality.yml setup looks good to me. Rebuilding the base commit with the same compiler, architecture, and race mode is the right comparison, and I'd keep that approach.

Once the 32-bit overflow issue is fixed, I'm good with this.

Comment thread decoder_plc_corpus_test.go Outdated
Comment thread decoder_plc_corpus_test.go Outdated
Comment thread decoder_plc_corpus_test.go
Comment thread .github/workflows/plc-quality.yml
Comment thread internal/celt/decoder.go
Comment thread internal/celt/celt.go Outdated
Comment thread internal/celt/plc.go
Comment thread internal/celt/decoder.go
Comment thread testdata/short-plc/baseline.json
@TouchDown1

Copy link
Copy Markdown
Contributor Author

@thomas-vilte addressed the i386 RMSE overflow at d6c4ebd and all 19 checks are green again. I also removed the unasserted Energy/Boundary fields, documented golden selection and retained-memory cost, made the noise-path changes explicit in the PR description, added the requested scratch/reset/fold rationale, and made fixture regeneration verify a clean exact libopus pin. Ready for re-review.

Base automatically changed from codex/short-celt-plc to main September 9, 2026 17:55
@thomas-vilte

Copy link
Copy Markdown
Member

Thanks for turning this around so quickly — I went through d6c4ebd and the overflow fix, the dropped Energy/Boundary fields, the scratch-sizing and reset rationale, the golden-resolution and resident-memory notes in the README, the pin check in regenerate.sh, and the noise-path paragraph in the description all look right. I've resolved those threads.

Two things left, then I'll approve:

  1. One sentence in the new foldPLCOverlap comment is wrong — details in that thread. Small reword.
  2. baseline.json is still committed uncompressed at ~1.54 MB while the arm64 baselines are gzipped. Nit, not a merge blocker; happy to drop it if you'd rather not churn the fixture.

On the conflicts. #245 landed as a squash (7aa7a85), so this branch still carries its original commit c0d7ee6 while main has the same content under a different SHA. The three conflicting files aren't a real content conflict — I compared the blobs and decoder_plc_test.go, testdata/short-plc/README.md, testdata/short-plc/generate.c and decoder.go are byte-identical between c0d7ee6 and 7aa7a85. It's purely a history-shape artifact.

Please don't use the web editor GitHub is suggesting: that merges main into the branch, keeps c0d7ee6 in the history, and makes this PR's diff show #245's changes again. Dropping that commit instead should apply cleanly:

git fetch origin
git rebase --onto origin/main c0d7ee63cecdc35aa81b83cbde40c70148da9e74 codex/celt-periodic-plc
git push --force-with-lease

That replays only this PR's nine commits onto main. The comment reword can ride along in the same push.

Once CI is green on the new base I'll switch my review to approve. Note that #247 will need the same treatment against its base after this one merges.

@TouchDown1
TouchDown1 force-pushed the codex/celt-periodic-plc branch from d6c4ebd to a403293 Compare September 9, 2026 21:14

@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.

Rebase looks right — linear, c0d7ee6 dropped, merge_base is 7aa7a85, and the comment fix in a403293 removes exactly the wrong sentence and leaves the correct explanation intact. 23/23 checks green on the new base.

Agreed on leaving baseline.json as is; not worth the fixture churn for 1 MB.

Approving.

@thomas-vilte
thomas-vilte merged commit b8ebd65 into main Sep 9, 2026
23 checks passed
@thomas-vilte
thomas-vilte deleted the codex/celt-periodic-plc branch September 9, 2026 21:27
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