Cap VBR frames by the buffer, not the rate - #203
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #203 +/- ##
==========================================
- Coverage 93.09% 93.08% -0.02%
==========================================
Files 58 58
Lines 10328 10337 +9
==========================================
+ Hits 9615 9622 +7
- Misses 503 504 +1
- Partials 210 211 +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:
|
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
In VBR, the encoder was still capping each frame at the nominal bitrate, so a difficult frame could only undershoot — it could never use extra bits and make them back later. libopus instead caps the frame using the buffer size provided by the caller:
In VBR, nbCompressedBytes is max_data_bytes (capped at 1275), not vbr_rate. The reservoir keeps the long-term average in check rather than putting a hard ceiling on every frame.
I also ported min_allowed, which was missing:
pion was using a fixed floor of 2 bytes, without accounting for what had already been written or the dynalloc boosts.
I checked what budget each stage sees in the reference before the VBR block to make sure this change didn't affect anything else. Instrumenting celt_encoder.c at 96 kbps VBR, effectiveBytes is 240 (the nominal rate) while nbCompressedBytes is 1275. The buffer only comes into play in the final clamp; dynalloc and the TF gate still see the nominal rate. This PR only changes that clamp.
The prefilter does see 1275 in the reference versus 239 in pion, but its thresholds (<25, <35, >12*C) don't change within that range. It would only matter below roughly 14 kbps, so I'm leaving that for now.
Measurement
Average packet size over 200 music frames:
VBR can now actually use the available headroom instead of being capped at the nominal frame size.
Round-trip SNR against libopus at the same bitrate:
Constrained VBR ends up at 97.6–98.5% of the target, with less variation than unconstrained VBR, as expected.
What doesn't match yet
At 96 kbps we're still about 0.29 dB behind libopus with roughly the same bit usage (242.6 vs 244.0 bytes), so the remaining difference is more about where the bits go than how many there are.
Looking at the distributions on the same clip, pion has more spread around the average (stddev 49.1 vs 37.6) but a smaller range (223/453 vs 206/477). I haven't isolated the cause yet.
The likely candidates are the remaining compute_vbr terms: stereo_saving and temporal_vbr are straightforward to port, while the tonality boost depends on analysis.c.
Tests
TestVBRUsesBufferHeadroom checks that a demanding frame can go above the nominal bitrate.
I deliberately didn't put an upper bound on the average in that test. On a sustained difficult signal there may not be easy frames to recover the bits, and the reference behaves the same way — with the same PCM, libopus averages 420.7 bytes against pion's 402.6 for a 240-byte target.
The long-term target is covered separately by TestVBRTracksTargetBitrate using material where the reservoir can actually recover.
Reference issue
Part of #34.