Scale Opus bitrate to audio frame duration
This commit is contained in:
committed by
Brandon McGinty
parent
2bdf9a8193
commit
aa38c99aab
@@ -176,7 +176,7 @@ Priority 2: protocol and data correctness
|
|||||||
terminators, positional data, and volume adjustment. Test real UDP
|
terminators, positional data, and volume adjustment. Test real UDP
|
||||||
fallback behavior too.
|
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
|
File: gumble/opus/opus.go
|
||||||
bitrate is maxDataBytes * 8 * 100. For permitted 20/40/60 ms intervals it
|
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
|
is 2x/4x/6x too high. Calculate bits per frame divided by the actual
|
||||||
|
|||||||
+15
-8
@@ -30,7 +30,8 @@ func (*generator) NewEncoder() gumble.AudioEncoder {
|
|||||||
// Force mono for voice transmission
|
// Force mono for voice transmission
|
||||||
e, _ := opus.NewEncoder(gumble.AudioSampleRate, VoiceChannels, opus.AppVoIP)
|
e, _ := opus.NewEncoder(gumble.AudioSampleRate, VoiceChannels, opus.AppVoIP)
|
||||||
return &Encoder{
|
return &Encoder{
|
||||||
Encoder: e,
|
Encoder: e,
|
||||||
|
channels: VoiceChannels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +40,8 @@ func NewStereoEncoder() gumble.AudioEncoder {
|
|||||||
// Create stereo encoder for file playback
|
// Create stereo encoder for file playback
|
||||||
e, _ := opus.NewEncoder(gumble.AudioSampleRate, gumble.AudioChannels, opus.AppAudio)
|
e, _ := opus.NewEncoder(gumble.AudioSampleRate, gumble.AudioChannels, opus.AppAudio)
|
||||||
return &Encoder{
|
return &Encoder{
|
||||||
Encoder: e,
|
Encoder: e,
|
||||||
|
channels: gumble.AudioChannels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,18 +58,15 @@ func (*generator) NewDecoder() gumble.AudioDecoder {
|
|||||||
// encoder
|
// encoder
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
*opus.Encoder
|
*opus.Encoder
|
||||||
|
channels int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Encoder) ID() int {
|
func (*Encoder) ID() int {
|
||||||
return ID
|
return ID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) Encode(pcm []int16, _, maxDataBytes int) ([]byte, error) {
|
func (e *Encoder) Encode(pcm []int16, frameSamples, maxDataBytes int) ([]byte, error) {
|
||||||
// Set the encoder bitrate to match the available bandwidth per frame.
|
bitrate := encoderBitrate(maxDataBytes, frameSamples, e.channels)
|
||||||
// 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
|
|
||||||
if bitrate < 8000 {
|
if bitrate < 8000 {
|
||||||
bitrate = 8000 // Opus minimum viable bitrate for voice
|
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
|
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() {
|
func (e *Encoder) Reset() {
|
||||||
_ = e.Encoder.Reset()
|
_ = e.Encoder.Reset()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user