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 }