Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,30 +623,64 @@ func TestConstrainedVBRPacketRoundTrip(t *testing.T) {
}

func TestVBRProducesVaryingPacketSizes(t *testing.T) {
enc, err := NewEncoder(WithVBR(true), WithConstrainedVBR(false))
enc, err := NewEncoder(WithChannels(2), WithBitrate(96000), WithVBR(true), WithConstrainedVBR(false))
require.NoError(t, err)

sizes := make(map[int]bool)
for i := range 20 {
pcm := make([]float32, encoderTestFrameSampleCount)
if i%2 == 0 {
for j := range pcm {
pcm[j] = float32(j%100) / 100
}
} else {
for j := range pcm {
pcm[j] = 0.0001
phase := 0.0
for i := range 6 {
pcm := make([]float32, encoderTestFrameSampleCount*2)
// The first frame is silence, which needs far fewer bits than the tone
// that follows it.
if i > 0 {
for j := range encoderTestFrameSampleCount {
v := float32(math.Sin(phase)) * 0.3
pcm[2*j] = v
pcm[2*j+1] = -v
phase += 2 * math.Pi * 440 / 48000
}
}
packet := make([]byte, 256)
n, err := enc.EncodeFloat32(pcm, packet)
require.NoError(t, err)
packet := make([]byte, 1500)
n, encErr := enc.EncodeFloat32(pcm, packet)
require.NoError(t, encErr)
sizes[n] = true
}

assert.Greater(t, len(sizes), 1, "VBR should produce varying packet sizes")
}

func TestVBRTracksTargetBitrate(t *testing.T) {
// The VBR target is clamped by a floor derived from the coded bin count.
// Deriving it from the byte budget instead pinned every frame near a
// quarter of the target, so VBR delivered about a third of the rate asked
// for. Guard the rate, not just the variation.
const bitrate = 96000
enc, err := NewEncoder(WithChannels(2), WithBitrate(bitrate), WithVBR(true), WithConstrainedVBR(false))
require.NoError(t, err)

frameBudget := bitrate / 50 / 8
total, frames := 0, 40
phase := 0.0
for range frames {
pcm := make([]float32, encoderTestFrameSampleCount*2)
for j := range encoderTestFrameSampleCount {
v := float32(math.Sin(phase)+0.5*math.Sin(phase*7.3)) * 0.3
pcm[2*j] = v
pcm[2*j+1] = -v
phase += 2 * math.Pi * 440 / 48000
}
packet := make([]byte, 1500)
n, encErr := enc.EncodeFloat32(pcm, packet)
require.NoError(t, encErr)
total += n
}

// Only the lower bound belongs here: what the floor bug broke was the rate
// being delivered, and the ceiling is the frame budget's own business.
avg := float64(total) / float64(frames)
assert.Greater(t, avg, 0.8*float64(frameBudget), "VBR undershoots the requested rate")
}

func TestVBRPacketRoundTripMultiFrame(t *testing.T) {
enc, err := NewEncoder(WithVBR(true), WithConstrainedVBR(false))
require.NoError(t, err)
Expand Down
43 changes: 24 additions & 19 deletions internal/celt/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,27 +523,32 @@ func (e *Encoder) computeIntensityAndDualStereo(
// Simplified version of libopus compute_vbr() (celt_encoder.c, ~line 1605).
// Not ported: tonality/activity boost, stereo saving, surround masking,
// temporal VBR — these need the full analysis pipeline pion doesn't have yet.
// vbrTFCalibration is the average tf_estimate the target is calibrated for,
// so a typical frame gets no boost (celt_encoder.c compute_vbr).
const vbrTFCalibration = 0.044

func computeVBR(
baseTarget int, // 1/8-bit units
maxDepth float32,
totBoostBits int,
transient, constrainedVBR bool,
constrainedVBR bool,
channelCount int,
effectiveBytes int,
lm int,
tfEstimate float32,
) int {
target := baseTarget + totBoostBits - (19 << lm) // dynalloc calibration
if transient {
target += target >> 3
}

floorDepth := float32(channelCount*effectiveBytes*8) * maxDepth / 65536
if floorDepth < float32(target>>2) {
floorDepth = float32(target >> 2)
}
if float32(target) > floorDepth {
target = int(floorDepth)
}
// Transient boost, compensated for the average frame. The reference scales
// the target by tf_estimate rather than switching on the transient flag.
target += int((tfEstimate - vbrTFCalibration) * float32(target))

// The floor is the depth the spectrum can actually use: the coded bin count
// times the per-bin depth. The Q shift around it in celt_encoder.c is a
// no-op in the float build, so maxDepth goes in unscaled.
bins := int(bandEdges[maxBands-2]) << lm
floorDepth := int(float32(channelCount*bins<<bitResolution) * maxDepth)
floorDepth = max(floorDepth, target>>2)
target = min(target, floorDepth)

// Constrained VBR can't sustain a higher bitrate for long, so pull 1/3
// of the way back to baseTarget (libopus's fixed 0.67 factor).
Expand All @@ -559,8 +564,8 @@ func computeVBR(
// tellFrac is e.rangeEncoder.TellFrac() at the point of the call. Mirrors
// celt_encoder.c's VBR block around compute_vbr (~lines 2436-2530).
func (e *Encoder) applyVBR(
frameBytes int, transient bool, dr dynallocResult,
effectiveBytes, lm, channelCount, tellFrac int,
frameBytes int, dr dynallocResult,
effectiveBytes, lm, channelCount, tellFrac int, tfEstimate float32,
) int {
vbrRate := frameBytes << 6 // libopus vbr_rate, 1/8-bit units

Expand All @@ -580,7 +585,7 @@ func (e *Encoder) applyVBR(
// bytes. libopus uses this pre-rounding value for the drift update below
// and the rounded value for the reservoir — they're not the same number.
rawTarget := computeVBR(
baseTarget, dr.maxDepth, dr.totBoostBits, transient, e.constrainedVBR, channelCount, effectiveBytes, lm,
baseTarget, dr.maxDepth, dr.totBoostBits, e.constrainedVBR, channelCount, lm, tfEstimate,
) + tellFrac

nbAvailableBytes := max(2, (rawTarget+(1<<5))>>6)
Expand Down Expand Up @@ -678,9 +683,9 @@ func (e *Encoder) EncodeFrame(pcm [][]float32, dst []byte, frameBytes, startBand
return 0, err
}
if patched {
transient = true
// The reference hands tf_analysis a fixed estimate for a patched
// frame, since the time-domain metric missed the transient.
// analyzeFrame already flipped info.transient; what is left is the
// fixed estimate the reference hands tf_analysis and the VBR target
// for a frame the time-domain metric missed.
tfEstimate = 0.2
}

Expand Down Expand Up @@ -748,7 +753,7 @@ func (e *Encoder) EncodeFrame(pcm [][]float32, dst []byte, frameBytes, startBand
tellFrac := int(e.rangeEncoder.TellFrac())

if e.vbr {
effectiveBytes = e.applyVBR(frameBytes, transient, dr, effectiveBytes, info.lm, info.channelCount, tellFrac)
effectiveBytes = e.applyVBR(frameBytes, dr, effectiveBytes, info.lm, info.channelCount, tellFrac, tfEstimate)
}
info.totalBits = uint(effectiveBytes) * 8
shapeBits := (int(info.totalBits) << bitResolution) - tellFrac - 1
Expand Down
Loading