344 lines
10 KiB
Go
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
|
||
|
|
}
|