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
+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)
}
}
}