mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
181 lines
5.1 KiB
Go
181 lines
5.1 KiB
Go
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))
|
|
}
|
|
}
|