package audio import ( "testing" "github.com/pion/rtp" ) func newTestXmppToTg(t *testing.T) (*XmppToTg, *fakeNtg) { t.Helper() n := &fakeNtg{} h, err := NewXmppToTg(XmppToTgOptions{ Ntg: n, ChatID: 12345, }) if err != nil { t.Fatalf("NewXmppToTg: %v", err) } return h, n } func TestAppendChunks10ms(t *testing.T) { // One 20ms decode -> two 10ms chunks. pcm := make([]int16, SamplesPerOpusFrame*Channels) got := appendChunks10ms(nil, pcm) if len(got) != 2 { t.Fatalf("got %d chunks for 20ms, want 2", len(got)) } for i, c := range got { if len(c) != NtgFrameBytes { t.Errorf("chunk %d len=%d, want %d", i, len(c), NtgFrameBytes) } } // Partial trailing samples are dropped (a 15ms slice yields one 10ms chunk). partial := make([]int16, (SamplesPerNtgFrame+SamplesPerNtgFrame/2)*Channels) got = appendChunks10ms(nil, partial) if len(got) != 1 { t.Errorf("partial slice: got %d chunks, want 1", len(got)) } } // encodeOpusFrame returns a real opus packet encoding a 440Hz sine wave. func encodeOpusFrame(t *testing.T, enc *Encoder) []byte { t.Helper() pcm := makeSineStereo(440) buf := make([]byte, MaxOpusPacketBytes) n, err := enc.Encode(pcm, buf) if err != nil { t.Fatalf("encode: %v", err) } return buf[:n] } // rtpWithSeq wraps payload in an RTP packet with the given seq. func rtpWithSeq(seq uint16, payload []byte) *rtp.Packet { return &rtp.Packet{ Header: rtp.Header{ SequenceNumber: seq, PayloadType: opusPayloadType, }, Payload: payload, } } func TestProcessTickSilenceWhenEmpty(t *testing.T) { h, n := newTestXmppToTg(t) st := newTickState() // Buffer is empty + not primed: each tick should ship one silence frame. for i := 0; i < 3; i++ { h.processTick(st) } if got := n.sentCount(); got != 3 { t.Errorf("expected 3 silence frames, got %d", got) } // All sent buffers should equal h.silence. n.mu.Lock() defer n.mu.Unlock() for i, p := range n.sentPCM { for j, x := range p { if x != 0 { t.Fatalf("frame %d byte %d = %x, want 0 (silence)", i, j, x) return } } } } func TestProcessTickDecodesAndSplits(t *testing.T) { h, n := newTestXmppToTg(t) // Push enough packets to prime the buffer (default 2). enc, err := NewEncoder() if err != nil { t.Fatalf("NewEncoder: %v", err) } for seq := uint16(100); seq < 102; seq++ { h.playout.Push(rtpWithSeq(seq, encodeOpusFrame(t, enc))) } st := newTickState() // First tick: pops packet 100 (20ms PCM = 2 chunks queued), ships chunk 1. h.processTick(st) // Second tick: ships chunk 2 from same decode (still pending). h.processTick(st) // Third tick: pops packet 101, ships its first chunk. h.processTick(st) // Fourth tick: ships packet 101's second chunk. h.processTick(st) if got := n.sentCount(); got != 4 { t.Errorf("expected 4 PCM frames shipped, got %d", got) } // Verify each is the right size. n.mu.Lock() defer n.mu.Unlock() for i, p := range n.sentPCM { if len(p) != NtgFrameBytes { t.Errorf("frame %d len=%d, want %d", i, len(p), NtgFrameBytes) } } } func TestProcessTickConcealsDtxEmptyPacket(t *testing.T) { h, n := newTestXmppToTg(t) enc, err := NewEncoder() if err != nil { t.Fatalf("NewEncoder: %v", err) } // Prime with a real packet, then an empty (DTX) packet the peer sends // during silence. h.playout.Push(rtpWithSeq(200, encodeOpusFrame(t, enc))) h.playout.Push(rtpWithSeq(201, nil)) st := newTickState() // real packet -> 2 chunks (ticks 1-2), DTX packet -> concealed (ticks 3+). for i := 0; i < 4; i++ { h.processTick(st) } if got := h.cDtxFrames.Load(); got != 1 { t.Errorf("dtx frames = %d, want 1 (empty packet must be concealed, not errored)", got) } // DTX must not fall through to the silence path, and every tick ships a frame. if got := h.cShipSilence.Load(); got != 0 { t.Errorf("ship_silence = %d, want 0 (DTX conceals rather than emitting silence)", got) } if got := n.sentCount(); got != 4 { t.Errorf("shipped %d frames, want 4", got) } } func TestProcessTickRunsPLCBeforeGapRecovery(t *testing.T) { h, _ := newTestXmppToTg(t) enc, err := NewEncoder() if err != nil { t.Fatalf("NewEncoder: %v", err) } // Prime with two packets, drain them to leave the buffer empty but // established (playoutHead = 502). h.playout.Push(rtpWithSeq(500, encodeOpusFrame(t, enc))) h.playout.Push(rtpWithSeq(501, encodeOpusFrame(t, enc))) st := newTickState() for i := 0; i < 4; i++ { // drain 2 packets * 2 chunks each h.processTick(st) } // Now skip 502, 503 entirely; deliver 504. h.playout.Push(rtpWithSeq(504, encodeOpusFrame(t, enc))) // Next tick should detect the gap, run 2 PLC decodes (502 + 503), // then decode 504. That's 6 chunks queued (2 PLC * 2 chunks + 1 real * 2). // processTick ships one per tick, so 6 ticks are required to drain. h.processTick(st) // ships first chunk // pending should still hold 5 more chunks if len(st.pending10ms) != 5 { t.Errorf("after first post-gap tick: pending=%d, want 5", len(st.pending10ms)) } for i := 0; i < 5; i++ { h.processTick(st) } if len(st.pending10ms) != 0 { t.Errorf("pending should be drained, got %d", len(st.pending10ms)) } }