Scale Opus bitrate to audio frame duration

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-09 14:36:00 -04:00
committed by Brandon McGinty
parent 2bdf9a8193
commit aa38c99aab
3 changed files with 32 additions and 9 deletions
+1 -1
View File
@@ -176,7 +176,7 @@ Priority 2: protocol and data correctness
terminators, positional data, and volume adjustment. Test real UDP
fallback behavior too.
22. Opus bitrate calculation assumes a 10 ms interval
[x] 22. Opus bitrate calculation assumes a 10 ms interval
File: gumble/opus/opus.go
bitrate is maxDataBytes * 8 * 100. For permitted 20/40/60 ms intervals it
is 2x/4x/6x too high. Calculate bits per frame divided by the actual
+15 -8
View File
@@ -30,7 +30,8 @@ func (*generator) NewEncoder() gumble.AudioEncoder {
// Force mono for voice transmission
e, _ := opus.NewEncoder(gumble.AudioSampleRate, VoiceChannels, opus.AppVoIP)
return &Encoder{
Encoder: e,
Encoder: e,
channels: VoiceChannels,
}
}
@@ -39,7 +40,8 @@ func NewStereoEncoder() gumble.AudioEncoder {
// Create stereo encoder for file playback
e, _ := opus.NewEncoder(gumble.AudioSampleRate, gumble.AudioChannels, opus.AppAudio)
return &Encoder{
Encoder: e,
Encoder: e,
channels: gumble.AudioChannels,
}
}
@@ -56,18 +58,15 @@ func (*generator) NewDecoder() gumble.AudioDecoder {
// encoder
type Encoder struct {
*opus.Encoder
channels int
}
func (*Encoder) ID() int {
return ID
}
func (e *Encoder) Encode(pcm []int16, _, maxDataBytes int) ([]byte, error) {
// Set the encoder bitrate to match the available bandwidth per frame.
// The bitrate is: maxDataBytes * 8 (bits per byte) * 100 (10ms frames per second).
// Opus encodes best when the bitrate target is set properly rather than
// relying on truncation, which can produce incomplete or corrupt frames.
bitrate := maxDataBytes * 8 * 100
func (e *Encoder) Encode(pcm []int16, frameSamples, maxDataBytes int) ([]byte, error) {
bitrate := encoderBitrate(maxDataBytes, frameSamples, e.channels)
if bitrate < 8000 {
bitrate = 8000 // Opus minimum viable bitrate for voice
}
@@ -81,6 +80,14 @@ func (e *Encoder) Encode(pcm []int16, _, maxDataBytes int) ([]byte, error) {
return buf[:n], nil
}
// encoderBitrate converts a per-frame packet budget to bits per second.
func encoderBitrate(maxDataBytes, frameSamples, channels int) int {
if frameSamples <= 0 || channels <= 0 {
return 8000
}
return maxDataBytes * 8 * gumble.AudioSampleRate * channels / frameSamples
}
func (e *Encoder) Reset() {
_ = e.Encoder.Reset()
}
+16
View File
@@ -0,0 +1,16 @@
package opus
import "testing"
// Regression: the encoder always assumed ten millisecond frames, causing the
// bitrate for 20/40/60 ms packets to be 2/4/6 times their actual budget.
func TestEncoderBitrateUsesFrameDuration(t *testing.T) {
const budget = 100
for _, frameSamples := range []int{480, 960, 1920, 2880} {
got := encoderBitrate(budget, frameSamples, 1)
want := budget * 8 * 48000 / frameSamples
if got != want {
t.Fatalf("%d samples: got %d, want %d", frameSamples, got, want)
}
}
}