Narrow the stereo image at low bitrate - #201
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #201 +/- ##
==========================================
+ Coverage 93.10% 93.11% +0.01%
==========================================
Files 58 58
Lines 10263 10308 +45
==========================================
+ Hits 9555 9598 +43
- Misses 500 501 +1
- Partials 208 209 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
thomas-vilte
force-pushed
the
feat/stereo-width
branch
from
August 11, 2026 23:29
d356bcb to
19dacd6
Compare
FrantaBOT
approved these changes
Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
libopus narrows the stereo image when the bitrate isn't high enough to afford full stereo (
src/opus_encoder.c, insideopus_encode_native):stereo_fade scales the side signal: at width 0, each pair of samples becomes their average, effectively turning the input into mono. The transition between the previous and new width is weighted by the MDCT window, so a bitrate change doesn't introduce a hard step.
pion didn't have any of this. This PR ports the three pieces: compute_equiv_rate (the part used by the CELT-only path), the stereo-width calculation, and stereo_fade.
This matters at lower bitrates because the side channel doesn't pay for itself. Narrowing the stereo image deliberately is better than letting the allocator spread too few bits across both channels.
How I found it
I was chasing an alloc_trim mismatch that only showed up at 24 and 32 kbps (72.6% and 95.5% agreement, compared to 99.8% from 48 kbps up). The inter-channel correlation term in the trim wasn't matching, and I found that libopus's X value changed with bitrate while pion's didn't: all 400 frames differed at 24 kbps and none did at 96 kbps.
I disabled the prefilter on both sides and ruled out the band boundaries. The raw MDCT was already different on the first frame, which put the problem before CELT itself — and that's where I found the stereo fade.
The numbers line up: with CBR and complexity 5, compute_equiv_rate gives 20900 at 24 kbps and 27867 at 32 kbps, both inside the transition range. At 48 kbps it gives 41800, already above 32000, which is exactly where the disagreement disappeared.
Measurement
Frame-by-frame agreement against opus_demo restricted-celt, 3000 music frames per bitrate:
The effect carries into transient analysis, TF analysis, and the prefilter because all of them operate on the same input signal.
Round-trip SNR against libopus at the same bitrate:
One thing worth noting: the fade changes the signal going into the codec, so SNR against the original naturally drops compared to not applying it. That's inherent to the feature, and libopus does the same. The useful comparison here is against the reference, not against the pre-change encoder.
Tests
Four tests cover the stereo-width scaling (monotonicity and both extremes), full width as an identity operation, zero width collapsing to mono, and the crossfade starting at the old width and ending at the new one.
Reference issue
Part of #34.