Fix UDP audio frame timestamp handling
This commit is contained in:
committed by
Brandon McGinty
parent
9c487c2586
commit
5b0879ddaa
@@ -86,8 +86,8 @@ type AudioPacket struct {
|
|||||||
Sender *User
|
Sender *User
|
||||||
Target *VoiceTarget
|
Target *VoiceTarget
|
||||||
|
|
||||||
// Sequence is the UDP audio packet sequence number, used by the
|
// Sequence is the UDP audio frame timestamp, used by the jitter buffer to
|
||||||
// jitter buffer to reorder packets.
|
// reorder packets.
|
||||||
Sequence int64
|
Sequence int64
|
||||||
|
|
||||||
AudioBuffer
|
AudioBuffer
|
||||||
|
|||||||
@@ -232,11 +232,15 @@ func (c *Client) AudioOutgoing() chan<- AudioBuffer {
|
|||||||
ch := make(chan AudioBuffer)
|
ch := make(chan AudioBuffer)
|
||||||
go func() {
|
go func() {
|
||||||
var seq int64
|
var seq int64
|
||||||
|
frameStep := int64(c.Config.AudioFrameSize() / AudioDefaultFrameSize)
|
||||||
|
if frameStep < 1 {
|
||||||
|
frameStep = 1
|
||||||
|
}
|
||||||
previous := <-ch
|
previous := <-ch
|
||||||
for p := range ch {
|
for p := range ch {
|
||||||
previous.writeAudio(c, seq, false)
|
previous.writeAudio(c, seq, false)
|
||||||
previous = p
|
previous = p
|
||||||
seq = (seq + 1) % math.MaxInt32
|
seq = (seq + frameStep) % math.MaxInt32
|
||||||
}
|
}
|
||||||
if previous != nil {
|
if previous != nil {
|
||||||
previous.writeAudio(c, seq, true)
|
previous.writeAudio(c, seq, true)
|
||||||
|
|||||||
@@ -106,10 +106,11 @@ func testLocalMumbleAudioRoundTrip(t *testing.T, disableUDP bool, interval time.
|
|||||||
// Discard the first few decoder warm-up frames before measuring pitch.
|
// Discard the first few decoder warm-up frames before measuring pitch.
|
||||||
var packet *gumble.AudioPacket
|
var packet *gumble.AudioPacket
|
||||||
var previousSequence int64
|
var previousSequence int64
|
||||||
|
frameStep := int64(sendConfig.AudioFrameSize() / gumble.AudioDefaultFrameSize)
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
select {
|
select {
|
||||||
case packet = <-listener.packets:
|
case packet = <-listener.packets:
|
||||||
if i > 0 && packet.Sequence != previousSequence+1 {
|
if i > 0 && packet.Sequence != previousSequence+frameStep {
|
||||||
t.Fatalf("choppy relay: sequence gap %d -> %d", previousSequence, packet.Sequence)
|
t.Fatalf("choppy relay: sequence gap %d -> %d", previousSequence, packet.Sequence)
|
||||||
}
|
}
|
||||||
previousSequence = packet.Sequence
|
previousSequence = packet.Sequence
|
||||||
@@ -119,7 +120,7 @@ func testLocalMumbleAudioRoundTrip(t *testing.T, disableUDP bool, interval time.
|
|||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case packet = <-listener.packets:
|
case packet = <-listener.packets:
|
||||||
if packet.Sequence != previousSequence+1 {
|
if packet.Sequence != previousSequence+frameStep {
|
||||||
t.Fatalf("choppy relay: sequence gap %d -> %d", previousSequence, packet.Sequence)
|
t.Fatalf("choppy relay: sequence gap %d -> %d", previousSequence, packet.Sequence)
|
||||||
}
|
}
|
||||||
frequency, purity, peak := audioQuality(packet.AudioBuffer)
|
frequency, purity, peak := audioQuality(packet.AudioBuffer)
|
||||||
|
|||||||
+42
-12
@@ -733,6 +733,8 @@ func (c *Client) dispatchOpus15(pktNum uint64, session uint32, frameNum int64, o
|
|||||||
|
|
||||||
if terminator && len(opusData) == 0 {
|
if terminator && len(opusData) == 0 {
|
||||||
decoder.Reset()
|
decoder.Reset()
|
||||||
|
user.audioSequenceValid = false
|
||||||
|
user.audioFrameStep = 0
|
||||||
log.Info("UDP15 #%d: terminator for %s, decoder reset", pktNum, user.Name)
|
log.Info("UDP15 #%d: terminator for %s, decoder reset", pktNum, user.Name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -791,14 +793,23 @@ func (c *Client) handleLegacyUDPVoice(pktNum uint64, data []byte) {
|
|||||||
|
|
||||||
// decodeAndDispatch decodes an Opus frame and dispatches PCM to audio listeners.
|
// decodeAndDispatch decodes an Opus frame and dispatches PCM to audio listeners.
|
||||||
func (c *Client) decodeAndDispatch(pktNum uint64, user *User, decoder AudioDecoder, frameNum int64, opusData []byte, context uint32, position *[3]float32, volumeAdjustment float32) {
|
func (c *Client) decodeAndDispatch(pktNum uint64, user *User, decoder AudioDecoder, frameNum int64, opusData []byte, context uint32, position *[3]float32, volumeAdjustment float32) {
|
||||||
// Detect sequence gaps.
|
// Frame numbers are timestamps in 10 ms units, not packet counters. For
|
||||||
|
// example, a standard 20 ms Opus packet advances its frame number by two.
|
||||||
|
// Only generate PLC for complete missing packets; treating every timestamp
|
||||||
|
// unit as a packet doubles playout and eventually exhausts OpenAL buffers.
|
||||||
if user.audioSequenceValid {
|
if user.audioSequenceValid {
|
||||||
gap := frameNum - user.audioSequence
|
gap := frameNum - user.audioSequence
|
||||||
if gap > 1 && gap < 100 {
|
frameStep := user.audioFrameStep
|
||||||
log.Info("UDP15 #%d: seq gap for %s: %d -> %d (loss=%d), generating PLC",
|
if frameStep < 1 {
|
||||||
pktNum, user.Name, user.audioSequence, frameNum, gap-1)
|
frameStep = 1
|
||||||
for i := int64(1); i < gap; i++ {
|
}
|
||||||
c.dispatchPLC15(user, decoder)
|
if gap > frameStep && gap < 100 {
|
||||||
|
if missing := missingAudioPackets(gap, frameStep); missing > 0 {
|
||||||
|
log.Info("UDP15 #%d: audio gap for %s: %d -> %d (loss=%d), generating PLC",
|
||||||
|
pktNum, user.Name, user.audioSequence, frameNum, missing)
|
||||||
|
for i := int64(1); i <= missing; i++ {
|
||||||
|
c.dispatchPLC15(user, decoder, user.audioSequence+i*frameStep)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if gap < 0 && gap > -100 {
|
} else if gap < 0 && gap > -100 {
|
||||||
log.Info("UDP15 #%d: seq reorder for %s: %d -> %d, resetting decoder",
|
log.Info("UDP15 #%d: seq reorder for %s: %d -> %d, resetting decoder",
|
||||||
@@ -809,8 +820,6 @@ func (c *Client) decodeAndDispatch(pktNum uint64, user *User, decoder AudioDecod
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user.audioSequence = frameNum
|
|
||||||
user.audioSequenceValid = true
|
|
||||||
|
|
||||||
pcm, err := decoder.Decode(opusData, AudioMaximumFrameSize)
|
pcm, err := decoder.Decode(opusData, AudioMaximumFrameSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -820,6 +829,9 @@ func (c *Client) decodeAndDispatch(pktNum uint64, user *User, decoder AudioDecod
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("UDP15 #%d: Opus OK for %s, pcm_samples=%d", pktNum, user.Name, len(pcm))
|
log.Debug("UDP15 #%d: Opus OK for %s, pcm_samples=%d", pktNum, user.Name, len(pcm))
|
||||||
|
user.audioSequence = frameNum
|
||||||
|
user.audioSequenceValid = true
|
||||||
|
user.audioFrameStep = audioFrameStep(len(pcm))
|
||||||
|
|
||||||
event := AudioPacket{
|
event := AudioPacket{
|
||||||
Client: c,
|
Client: c,
|
||||||
@@ -836,20 +848,38 @@ func (c *Client) decodeAndDispatch(pktNum uint64, user *User, decoder AudioDecod
|
|||||||
c.dispatchAudio(user, &event)
|
c.dispatchAudio(user, &event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// missingAudioPackets returns the number of whole packets absent from a
|
||||||
|
// timestamp gap. A non-integral gap cannot reliably identify a missing packet.
|
||||||
|
func missingAudioPackets(gap, frameStep int64) int64 {
|
||||||
|
if frameStep < 1 || gap <= frameStep || gap%frameStep != 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return gap/frameStep - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// audioFrameStep converts interleaved stereo PCM length to Mumble's 10 ms
|
||||||
|
// frame-number units.
|
||||||
|
func audioFrameStep(samples int) int64 {
|
||||||
|
frames := samples / AudioChannels
|
||||||
|
step := int64(frames / AudioDefaultFrameSize)
|
||||||
|
if step < 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return step
|
||||||
|
}
|
||||||
|
|
||||||
// dispatchPLC15 generates a Packet Loss Concealment frame for 1.5 UDP.
|
// dispatchPLC15 generates a Packet Loss Concealment frame for 1.5 UDP.
|
||||||
func (c *Client) dispatchPLC15(user *User, decoder AudioDecoder) {
|
func (c *Client) dispatchPLC15(user *User, decoder AudioDecoder, sequence int64) {
|
||||||
pcm, err := decoder.Decode(nil, AudioMaximumFrameSize)
|
pcm, err := decoder.Decode(nil, AudioMaximumFrameSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
decoder.Reset()
|
decoder.Reset()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
seq := user.audioSequence + 1
|
|
||||||
user.audioSequence = seq
|
|
||||||
event := AudioPacket{
|
event := AudioPacket{
|
||||||
Client: c,
|
Client: c,
|
||||||
Sender: user,
|
Sender: user,
|
||||||
Target: &VoiceTarget{ID: 0},
|
Target: &VoiceTarget{ID: 0},
|
||||||
Sequence: seq,
|
Sequence: sequence,
|
||||||
AudioBuffer: AudioBuffer(pcm),
|
AudioBuffer: AudioBuffer(pcm),
|
||||||
}
|
}
|
||||||
c.dispatchAudio(user, &event)
|
c.dispatchAudio(user, &event)
|
||||||
|
|||||||
@@ -223,6 +223,31 @@ func TestUDPAudioProtobufReferenceVector(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAudioFrameTimestampGaps(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
gap, step int64
|
||||||
|
want int64
|
||||||
|
}{
|
||||||
|
{"consecutive 10 ms packets", 1, 1, 0},
|
||||||
|
{"consecutive 20 ms packets", 2, 2, 0},
|
||||||
|
{"one missing 20 ms packet", 4, 2, 1},
|
||||||
|
{"two missing 20 ms packets", 6, 2, 2},
|
||||||
|
{"non-integral timestamp gap", 3, 2, 0},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := missingAudioPackets(tt.gap, tt.step); got != tt.want {
|
||||||
|
t.Fatalf("missingAudioPackets(%d, %d) = %d, want %d", tt.gap, tt.step, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := audioFrameStep(1920); got != 2 {
|
||||||
|
t.Fatalf("audioFrameStep(1920) = %d, want 2", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUDPAudioProtobufIncomingFields(t *testing.T) {
|
func TestUDPAudioProtobufIncomingFields(t *testing.T) {
|
||||||
var packet bytes.Buffer
|
var packet bytes.Buffer
|
||||||
writeVarint := func(v uint64) {
|
writeVarint := func(v uint64) {
|
||||||
|
|||||||
@@ -52,10 +52,11 @@ type User struct {
|
|||||||
client *Client
|
client *Client
|
||||||
decoder AudioDecoder
|
decoder AudioDecoder
|
||||||
|
|
||||||
// audioSequence tracks the last UDP audio packet sequence number for
|
// audioSequence tracks the last UDP audio frame timestamp for this user,
|
||||||
// this user, used to detect packet loss and reset the Opus decoder.
|
// used to detect packet loss and reset the Opus decoder.
|
||||||
audioSequence int64
|
audioSequence int64
|
||||||
audioSequenceValid bool
|
audioSequenceValid bool
|
||||||
|
audioFrameStep int64
|
||||||
|
|
||||||
// audioMu protects audio-related fields accessed from both the
|
// audioMu protects audio-related fields accessed from both the
|
||||||
// audio processing goroutine (OnAudioStream) and the UI goroutine.
|
// audio processing goroutine (OnAudioStream) and the UI goroutine.
|
||||||
|
|||||||
Reference in New Issue
Block a user