Wire CELT and hybrid Opus decoding#109
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #109 +/- ##
==========================================
- Coverage 83.45% 82.74% -0.72%
==========================================
Files 21 21
Lines 3899 4289 +390
==========================================
+ Hits 3254 3549 +295
- Misses 508 566 +58
- Partials 137 174 +37
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
RFC 6716 / 8251 conformationStatus: fail (informational) The action extracts the RFC 6716 reference implementation, applies the RFC 8251 decoder update patch, and then builds the patched reference tools. This check is informational while CELT support is incomplete; the workflow still reports success. Legend: numeric cells are rfc6716
rfc8251
Run output |
There was a problem hiding this comment.
Pull request overview
Wires CELT-only and Hybrid Opus packets into the top-level decoder path and adds RFC 6716 hybrid/SILK transition redundancy handling, including state resets and cross-fade behavior.
Changes:
- Route CELT-only and Hybrid packets through the public decode path (instead of rejecting them).
- Add hybrid redundancy parsing/decoding and transition cross-fade logic (SILK ↔ CELT/hybrid).
- Extend decoder state tracking (range decoder/final range, previous mode/redundancy) and update tests for the new decode signature.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| decoder.go | Implements CELT-only + Hybrid decode paths, redundancy parsing, and transition state/cross-fades; updates internal decode plumbing and resampling. |
| internal/silk/decoder.go | Adds DecodeWithRange to allow SILK to share an Opus range decoder with CELT in hybrid packets. |
| decoder_test.go | Updates/extends tests for CELT-only + Hybrid decoding and init/reset behavior. |
| packet_test.go | Updates tests to match the new internal decode(...) return signature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| bandwidth, isStereo, sampleCount, decodedChannelCount, err := d.decode(in, d.silkBuffer) | ||
| if err != nil { | ||
| return 0, 0, false, err | ||
| } | ||
|
|
||
| channelCount := 1 | ||
| if isStereo { | ||
| channelCount = 2 | ||
| } | ||
|
|
||
| samplesPerChannel = (sampleCount / channelCount) * d.sampleRate / bandwidth.SampleRate() | ||
| requiredSamples := samplesPerChannel * channelCount | ||
| samplesPerChannel = (sampleCount / decodedChannelCount) * d.sampleRate / bandwidth.SampleRate() | ||
| requiredSamples := samplesPerChannel * decodedChannelCount | ||
| if cap(d.resampleBuffer) < requiredSamples { | ||
| d.resampleBuffer = make([]float32, requiredSamples) | ||
| } | ||
| d.resampleBuffer = d.resampleBuffer[:requiredSamples] | ||
| if err = d.resampleSilk(d.silkBuffer[:sampleCount], d.resampleBuffer, channelCount, bandwidth); err != nil { | ||
| return 0, 0, false, err | ||
| if d.sampleRate == bandwidth.SampleRate() { | ||
| copy(d.resampleBuffer, d.silkBuffer[:sampleCount]) | ||
| } else { | ||
| if err = d.resampleSilk(d.silkBuffer[:sampleCount], d.resampleBuffer, decodedChannelCount, bandwidth); err != nil { | ||
| return 0, 0, false, err | ||
| } |
There was a problem hiding this comment.
decodeToFloat32 assumes the decoder output sample rate is bandwidth.SampleRate(), but CELT-only and Hybrid packets always decode at 48 kHz even when cfg.bandwidth() is Narrowband/Wideband/etc. This makes samplesPerChannel/requiredSamples wrong and can cause incorrect resampling (and potentially stale bytes in the caller’s output buffer) for CELT/Hybrid packets. Use the configuration’s decoded sample rate (e.g., 48 kHz for CELT/Hybrid) when computing samplesPerChannel and choosing the resampling path; one approach is to return decodedSampleRate from decode(...) or to derive it from the mode and treat CELT/Hybrid input as 48 kHz regardless of bandwidth.
| if rangeDecoder == nil { | ||
| return errOutBufferTooSmall | ||
| } |
There was a problem hiding this comment.
DecodeWithRange returns errOutBufferTooSmall when rangeDecoder is nil, which is unrelated to the actual failure and will mislead callers debugging hybrid decode issues. Return a dedicated error (e.g., errNilRangeDecoder) or at least errors.New("nil rangeDecoder") so the failure mode is accurately reported.
| d.rangeDecoder.Init(encodedFrame) | ||
|
|
||
| silkInternal := make([]float32, silkSamplesPerChannel*streamChannelCount) | ||
| if err := d.silkDecoder.DecodeWithRange( | ||
| &d.rangeDecoder, | ||
| silkInternal, | ||
| isStereo, | ||
| frameNanoseconds, | ||
| silk.Bandwidth(BandwidthWideband), | ||
| ); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| var err error | ||
| redundancy := d.decodeHybridRedundancyHeader(encodedFrame) | ||
| if redundancy.present && redundancy.celtToSilk { | ||
| if err = d.decodeHybridRedundantFrame(&redundancy, isStereo, outputChannelCount, endBand); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if d.previousMode != configurationModeHybrid && d.previousMode != 0 && !d.previousRedundancy { | ||
| d.celtDecoder.Reset() | ||
| clear(d.celtBuffer) | ||
| } | ||
| if err = d.celtDecoder.DecodeWithRange( | ||
| encodedFrame[:redundancy.celtDataLen], | ||
| out, | ||
| isStereo, | ||
| outputChannelCount, | ||
| frameSampleCount, | ||
| startBand, | ||
| endBand, | ||
| &d.rangeDecoder, | ||
| ); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| silk48 := make([]float32, frameSampleCount*streamChannelCount) | ||
| if err = d.resampleHybridSilkTo48(silkInternal, silk48, streamChannelCount); err != nil { | ||
| return err | ||
| } | ||
| d.addHybridSilk(out, silk48, streamChannelCount, outputChannelCount, frameSampleCount) |
There was a problem hiding this comment.
decodeHybridFrame allocates new silkInternal and silk48 buffers on every frame (make([]float32, ...)). Since decode is a hot path, this will add avoidable GC pressure (especially for multi-frame packets). Consider reusing decoder-owned scratch buffers (e.g., grow-and-reuse slices on Decoder) instead of allocating per frame.
| if d.sampleRate != celtSampleRate { | ||
| return | ||
| } |
There was a problem hiding this comment.
applySilkRedundancyFades currently bails out unless d.sampleRate == 48000. That means hybrid/SILK transition cross-fades and redundant CELT contributions are silently dropped for decoders configured to output 8/12/16/24 kHz (even though Init explicitly supports those rates). To keep behavior consistent across output rates, apply the fades/additions in the 48 kHz domain before resampling, or resample the redundant/CELT transition audio to the output sample rate and apply the fade there.
c647841 to
16e5ebd
Compare
Summary
Why
The prior CELT work decoded frames internally, but the top-level Opus decoder still rejected CELT-only and Hybrid packets. This change wires those modes into the public decode path and adds the transition bookkeeping needed for normative RFC 6716 behavior.
Validation
go test ./...go test -cover ./...GOCACHE=/tmp/go-build-opus GOLANGCI_LINT_CACHE=/tmp/golangci-lint-cache-opus golangci-lint run --new-from-rev origin/main ./...