Files
ts-mobile-go/go/teamspeak/receive_audio_test.go
T
2026-07-20 19:01:03 +08:00

344 lines
10 KiB
Go

package teamspeak
import (
"encoding/binary"
"fmt"
"slices"
"sync"
"testing"
"time"
)
type fakeDecoder struct {
frames [][]int16
lost int
released bool
}
func (d *fakeDecoder) Decode([]byte) ([]int16, error) {
f := d.frames[0]
d.frames = d.frames[1:]
return f, nil
}
func (d *fakeDecoder) DecodeLost() ([]int16, error) {
d.lost++
return make([]int16, receiveFrameInterleavedSamples), nil
}
func (d *fakeDecoder) Release() { d.released = true }
type fakeFactory struct{ decoder *fakeDecoder }
func (f *fakeFactory) NewvoiceDecoder(int, int) (voiceDecoder, error) { return f.decoder, nil }
func frame(value int16) []int16 {
return stereoFrame(value, value)
}
func stereoFrame(left, right int16) []int16 {
f := make([]int16, receiveFrameInterleavedSamples)
for i := 0; i < len(f); i += receiveChannels {
f[i], f[i+1] = left, right
}
return f
}
func TestVoiceCodecChannelsAndMonoNormalization(t *testing.T) {
if got := voiceCodecChannels(4); got != 1 {
t.Fatalf("codec 4 channels=%d", got)
}
if got := voiceCodecChannels(5); got != receiveChannels {
t.Fatalf("codec 5 channels=%d", got)
}
got := monoToStereo([]int16{10, -20, 30})
want := []int16{10, 10, -20, -20, 30, 30}
if !slices.Equal(got, want) {
t.Fatalf("mono normalization=%v, want %v", got, want)
}
}
func TestStereoNormalizationPreservesChannels(t *testing.T) {
pcm := []int16{10, -10, 20, -20}
got := normalizeDecodedPCM(pcm, receiveChannels)
if !slices.Equal(got, pcm) {
t.Fatalf("stereo normalization=%v, want %v", got, pcm)
}
}
func TestPLCUsesStereoFrameContract(t *testing.T) {
d := &fakeDecoder{}
pcm, err := d.DecodeLost()
if err != nil {
t.Fatal(err)
}
if len(pcm) != receiveFrameInterleavedSamples {
t.Fatalf("PLC samples=%d", len(pcm))
}
}
func TestTimelinePrebufferPLCAndSequenceWrap(t *testing.T) {
d := &fakeDecoder{frames: [][]int16{frame(1), frame(2), frame(3), frame(4)}}
tl := newReceiveTimeline(1, d)
at := time.Now()
for _, seq := range []uint16{65534, 65535, 0} {
tl.push(voicePacket{sequence: seq, data: []byte{1}, at: at})
}
for _, want := range []int16{1, 2, 3} {
got, active := tl.next()
if !active || got[0] != want {
t.Fatalf("got active=%v sample=%d, want %d", active, got[0], want)
}
}
tl.push(voicePacket{sequence: 2, data: []byte{1}, at: at})
if _, active := tl.next(); active || d.lost != 0 {
t.Fatalf("expected jitter wait before PLC")
}
if _, active := tl.next(); active || d.lost != 0 {
t.Fatalf("expected second jitter wait before PLC")
}
if _, active := tl.next(); !active || d.lost != 1 {
t.Fatalf("expected PLC after confirmed gap")
}
tl.packets[2] = voicePacket{sequence: 2, data: []byte{1}, at: at}
got, active := tl.next()
if !active || got[0] != 4 {
t.Fatalf("got active=%v sample=%d after PLC", active, got[0])
}
}
func TestTimelineSplitsSupportedOpusDurationsIntoTwentyMillisecondFrames(t *testing.T) {
for _, durationMS := range []int{10, 20, 40, 60} {
t.Run(fmt.Sprintf("%dms", durationMS), func(t *testing.T) {
samples := receiveSampleRate * durationMS / 1000 * receiveChannels
pcm := make([]int16, samples)
for i := range pcm {
pcm[i] = int16(durationMS)
}
tl := newReceiveTimeline(1, &fakeDecoder{})
frames := 0
if frame, ok := tl.takeFrame(pcm); ok {
frames++
if len(frame) != receiveFrameInterleavedSamples {
t.Fatalf("first frame has %d samples", len(frame))
}
}
for {
frame, ok := tl.popPending()
if !ok {
break
}
frames++
if len(frame) != receiveFrameInterleavedSamples {
t.Fatalf("pending frame has %d samples", len(frame))
}
}
wantFrames := durationMS / 20
if frames != wantFrames {
t.Fatalf("got %d complete frames, want %d (pending=%d)", frames, wantFrames, len(tl.pending))
}
wantPending := 0
if durationMS == 10 {
wantPending = receiveFrameInterleavedSamples / 2
}
if len(tl.pending) != wantPending {
t.Fatalf("pending=%d, want %d", len(tl.pending), wantPending)
}
})
}
}
func TestTimelineAccumulatesTwoTenMillisecondFrames(t *testing.T) {
half := make([]int16, receiveFrameInterleavedSamples/2)
for i := range half {
half[i] = 7
}
d := &fakeDecoder{frames: [][]int16{half, half, frame(9)}}
tl := newReceiveTimeline(1, d)
at := time.Now()
for sequence := uint16(1); sequence <= 3; sequence++ {
tl.push(voicePacket{sequence: sequence, data: []byte{1}, at: at})
}
if _, active := tl.next(); active {
t.Fatal("10 ms frame should remain pending")
}
got, active := tl.next()
if !active || len(got) != receiveFrameInterleavedSamples || got[0] != 7 || got[len(got)-1] != 7 {
t.Fatalf("10 ms frames were not combined: active=%v len=%d", active, len(got))
}
got, active = tl.next()
if !active || got[0] != 9 {
t.Fatalf("next decoded frame was not preserved: active=%v", active)
}
}
func TestTimelinePrebufferSurvivesTicksBetweenPackets(t *testing.T) {
d := &fakeDecoder{frames: [][]int16{frame(1)}}
tl := newReceiveTimeline(1, d)
at := time.Now()
for sequence := uint16(1); sequence <= 3; sequence++ {
tl.push(voicePacket{sequence: sequence, data: []byte{1}, at: at.Add(time.Duration(sequence) * 20 * time.Millisecond)})
if sequence < 3 {
if _, active := tl.next(); active {
t.Fatal("timeline played before prebuffer was ready")
}
}
}
if !tl.ready || len(tl.packets) != receivePrebuffer {
t.Fatalf("interleaved ticks reset prebuffer: ready=%v packets=%d expected=%d", tl.ready, len(tl.packets), tl.expected)
}
if pcm, active := tl.next(); !active || pcm[0] != 1 {
t.Fatal("timeline did not play after interleaved prebuffer")
}
}
func TestTimelineRebasesAfterTalkSpurtGap(t *testing.T) {
d := &fakeDecoder{frames: [][]int16{frame(1), frame(2), frame(3), frame(4)}}
tl := newReceiveTimeline(1, d)
start := time.Now()
for sequence := uint16(100); sequence < 103; sequence++ {
tl.push(voicePacket{sequence: sequence, data: []byte{1}, at: start})
}
for range 3 {
tl.next()
}
resume := start.Add(receiveTalkSpurtGap + time.Millisecond)
for sequence := uint16(200); sequence < 203; sequence++ {
tl.push(voicePacket{sequence: sequence, data: []byte{1}, at: resume})
}
if !tl.ready || tl.expected != 200 {
t.Fatalf("talk spurt did not rebase: ready=%v expected=%d", tl.ready, tl.expected)
}
}
func TestTimelineSilenceDoesNotAdvanceExpected(t *testing.T) {
tl := newReceiveTimeline(1, &fakeDecoder{frames: [][]int16{frame(1), frame(2), frame(3)}})
at := time.Now()
for sequence := uint16(10); sequence < 13; sequence++ {
tl.push(voicePacket{sequence: sequence, data: []byte{1}, at: at})
}
for range 3 {
tl.next()
}
expected := tl.expected
for range 10 {
tl.next()
}
if tl.expected != expected {
t.Fatalf("silence advanced expected from %d to %d", expected, tl.expected)
}
}
func TestWorkerEmitsSilenceForActiveTimeline(t *testing.T) {
d := &fakeDecoder{frames: [][]int16{frame(1), frame(1), frame(1)}}
var frames [][]byte
r := newReceiveAudio(&fakeFactory{decoder: d}, func(p []byte) { frames = append(frames, p) }, nil)
timelines := map[int]*receiveTimeline{1: newReceiveTimeline(1, d)}
at := time.Now()
for sequence := uint16(1); sequence <= 3; sequence++ {
timelines[1].push(voicePacket{sequence: sequence, data: []byte{1}, at: at})
}
r.tick(timelines, at)
r.tick(timelines, at.Add(20*time.Millisecond))
if len(frames) != 2 || len(frames[0]) != receiveFrameBytes || len(frames[1]) != receiveFrameBytes {
t.Fatalf("expected fixed silent-capable output, got %d frames", len(frames))
}
}
func TestTimelineDuplicateOldAndBounded(t *testing.T) {
tl := newReceiveTimeline(1, &fakeDecoder{})
at := time.Now()
tl.push(voicePacket{sequence: 10, data: []byte{1}, at: at})
tl.push(voicePacket{sequence: 10, data: []byte{2}, at: at})
tl.push(voicePacket{sequence: 9, data: []byte{3}, at: at})
if len(tl.packets) != 1 {
t.Fatalf("accepted duplicate or old packets: %d", len(tl.packets))
}
for i := uint16(11); i < 40; i++ {
tl.push(voicePacket{sequence: i, data: []byte{1}, at: at})
}
if len(tl.packets) > receiveMaxPackets {
t.Fatalf("packet map exceeds cap: %d", len(tl.packets))
}
}
func TestMixerClampsAndPCMFormat(t *testing.T) {
acc := make([]int32, receiveFrameInterleavedSamples)
acc[0], acc[1] = 40000, -40000
out := mixPCM16(acc)
if len(out) != receiveFrameBytes {
t.Fatalf("got %d bytes", len(out))
}
if got := int16(binary.LittleEndian.Uint16(out)); got != 32767 {
t.Fatalf("positive clamp=%d", got)
}
if got := int16(binary.LittleEndian.Uint16(out[2:])); got != -32768 {
t.Fatalf("negative clamp=%d", got)
}
}
func TestMixerPreservesIndependentStereoChannels(t *testing.T) {
acc := make([]int32, receiveFrameInterleavedSamples)
for i := 0; i < len(acc); i += receiveChannels {
acc[i], acc[i+1] = 100, -200
}
out := mixPCM16(acc)
if got := int16(binary.LittleEndian.Uint16(out)); got != 100 {
t.Fatalf("left channel=%d", got)
}
if got := int16(binary.LittleEndian.Uint16(out[2:])); got != -200 {
t.Fatalf("right channel=%d", got)
}
}
func TestNewClientInstallsPlatformFactoryWhenAvailable(t *testing.T) {
c := NewClient()
if newPlatformVoiceDecoderFactory() != nil && c.receiveFactory == nil {
t.Fatal("NewClient did not install the platform voice decoder factory")
}
}
func TestReceiveWorkerRestartsAfterStop(t *testing.T) {
c := &TSClient{receiveFactory: &fakeFactory{decoder: &fakeDecoder{}}}
c.StartReceiveAudio()
first := c.receive
if first == nil {
t.Fatal("StartReceiveAudio did not create a worker")
}
c.StopReceiveAudio()
c.StartReceiveAudio()
if c.receive == nil || c.receive == first {
t.Fatal("StartReceiveAudio did not create a fresh worker after stop")
}
c.StopReceiveAudio()
}
func TestWorkerMuteDoesNotStopDecodeOrSpeaking(t *testing.T) {
d := &fakeDecoder{frames: [][]int16{frame(100), frame(100), frame(100)}}
var mu sync.Mutex
var pcm [][]byte
var speaking []bool
r := newReceiveAudio(&fakeFactory{decoder: d}, func(p []byte) { mu.Lock(); pcm = append(pcm, p); mu.Unlock() }, func(_ int, v bool) { mu.Lock(); speaking = append(speaking, v); mu.Unlock() })
r.start()
for i := 1; i <= 3; i++ {
r.enqueue(1, 4, uint16(i), []byte{1})
}
time.Sleep(30 * time.Millisecond)
r.command(audioCommand{kind: "mute", clientID: 1, muted: true})
time.Sleep(70 * time.Millisecond)
r.stopAndWait()
mu.Lock()
defer mu.Unlock()
if len(speaking) == 0 || !speaking[0] {
t.Fatal("mute prevented speaking")
}
if len(d.frames) >= 3 {
t.Fatal("mute prevented decoding")
}
if !d.released {
t.Fatal("worker did not release decoder")
}
// At least the initial unmuted tick may mix; muted ticks must not require PCM.
_ = pcm
}