Summary
OpusDecoder.Decode() returns the full requested sample count (no exception, no error) but the output PCM is completely silent — exactly zero amplitude — for any Opus packet encoded in Hybrid mode (TOC config 12–15: SILK+CELT combined to reconstruct SWB/Fullband audio) when the decoder is configured to output at a sample rate below the native 48kHz (e.g. 16kHz).
This reproduces with essentially any real-world WebRTC encoder (Chrome/Firefox encode Hybrid/Fullband by default), so any application that decodes Opus at a reduced output rate — a very common case for telephony/VoIP use — hits it consistently.
Root cause
In OpusDecoder.cs, inside opus_decode_frame:
celt_accum = ((mode != OpusMode.MODE_CELT_ONLY) && (frame_size >= F10)) ? 1 : 0;
This is the formula from the #ifdef FIXED_POINT branch of the reference C decoder (opus_decoder.c):
/* In fixed-point, we can tell CELT to do the accumulation on top of the
SILK PCM buffer. This saves some stack space. */
#ifdef FIXED_POINT
celt_accum = (mode != MODE_CELT_ONLY) && (frame_size >= F10);
#else
celt_accum = 0;
#endif
celt_accum=1 is a FIXED_POINT-only optimization (CELT writes its output directly on top of the buffer SILK already wrote, instead of a separate buffer that gets added afterward) intended to save stack space on embedded targets. Every floating-point build of libopus — i.e. virtually every real desktop/server deployment — always takes the #else branch, so the accum=1 path is rarely exercised or tested upstream in practice.
Concentus, being a fixed-point-style port, always computes the FIXED_POINT formula with no equivalent #else — unconditionally taking the accum=1 path on every Hybrid/SILK+CELT frame with frame_size >= 10ms. That path has a bug (not further isolated beyond this point) when combined with output-rate downsampling (downsample > 1, i.e. any decode Fs below the native 48kHz).
How this was isolated
- Captured raw bytes of real Opus packets from a live WebRTC call.
- Decoded them in isolation with Concentus: at 16kHz output → peak amplitude 0; the same exact bytes at 48kHz (native) → valid audio. Confirmed the bug is in the decoder, not in any surrounding pipeline.
- Built the official reference
libopus 1.6.1 (float build) from source and decoded the same exact bytes: works correctly at both 16kHz and 48kHz — confirming this isn't an inherent Opus/packet limitation, but specific to this C# port.
- Instrumented both the reference C decoder (
opus_decoder.c/celt_decoder.c) and this port (OpusDecoder.cs/CeltDecoder.cs/CeltCommon.cs) with trace logging of every intermediate value (mode, frame_size, downsample, N, Nd, celt_accum, …) for the same packet, side by side. Every value matched exactly except celt_accum (0 in the reference, 1 in this port).
- Forcing
celt_accum = 0 in a local copy made the same packet decode correctly again (peak amplitude ~511, vs. ~515 in the reference — a normal fixed/float rounding difference, not a discrepancy).
Minimal repro
using Concentus;
// Real Hybrid-mode (TOC config 15, Fullband, 20ms), mono Opus packet captured from a live WebRTC call.
var packet = Convert.FromBase64String(
"eAQPIzMOKgxLnJI+goxXFNRlSWLRdqvLJUtfncLBYzBzkBMB3sREp2Tb9E0M8uIz+FtWp/N+078z6ZBsSLmP1XJ/IJgOwBHut34=");
var decoder = OpusCodecFactory.CreateDecoder(16000, 1); // any Fs < 48000 triggers it
var pcm = new short[320];
decoder.Decode(packet, pcm, 320, false);
Console.WriteLine(pcm.Max(Math.Abs)); // prints 0 — should be a few hundred
Suggested fix
i.e. simply mirror the #else branch of the reference decoder — this is exactly the behavior every real float-build deployment of libopus already relies on, so it carries no new/untested behavior, just removes a code path that appears to have never been correctly ported.
Environment
- Concentus commit/version: latest
master
- .NET runtime: .NET 10 (also affects other TFMs sharing this decoder code)
Summary
OpusDecoder.Decode()returns the full requested sample count (no exception, no error) but the output PCM is completely silent — exactly zero amplitude — for any Opus packet encoded in Hybrid mode (TOC config 12–15: SILK+CELT combined to reconstruct SWB/Fullband audio) when the decoder is configured to output at a sample rate below the native 48kHz (e.g. 16kHz).This reproduces with essentially any real-world WebRTC encoder (Chrome/Firefox encode Hybrid/Fullband by default), so any application that decodes Opus at a reduced output rate — a very common case for telephony/VoIP use — hits it consistently.
Root cause
In
OpusDecoder.cs, insideopus_decode_frame:This is the formula from the
#ifdef FIXED_POINTbranch of the reference C decoder (opus_decoder.c):celt_accum=1is a FIXED_POINT-only optimization (CELT writes its output directly on top of the buffer SILK already wrote, instead of a separate buffer that gets added afterward) intended to save stack space on embedded targets. Every floating-point build of libopus — i.e. virtually every real desktop/server deployment — always takes the#elsebranch, so theaccum=1path is rarely exercised or tested upstream in practice.Concentus, being a fixed-point-style port, always computes the
FIXED_POINTformula with no equivalent#else— unconditionally taking theaccum=1path on every Hybrid/SILK+CELT frame withframe_size >= 10ms. That path has a bug (not further isolated beyond this point) when combined with output-rate downsampling (downsample > 1, i.e. any decode Fs below the native 48kHz).How this was isolated
libopus1.6.1 (float build) from source and decoded the same exact bytes: works correctly at both 16kHz and 48kHz — confirming this isn't an inherent Opus/packet limitation, but specific to this C# port.opus_decoder.c/celt_decoder.c) and this port (OpusDecoder.cs/CeltDecoder.cs/CeltCommon.cs) with trace logging of every intermediate value (mode,frame_size,downsample,N,Nd,celt_accum, …) for the same packet, side by side. Every value matched exactly exceptcelt_accum(0 in the reference, 1 in this port).celt_accum = 0in a local copy made the same packet decode correctly again (peak amplitude ~511, vs. ~515 in the reference — a normal fixed/float rounding difference, not a discrepancy).Minimal repro
Suggested fix
i.e. simply mirror the
#elsebranch of the reference decoder — this is exactly the behavior every real float-build deployment of libopus already relies on, so it carries no new/untested behavior, just removes a code path that appears to have never been correctly ported.Environment
master