134 lines
5.1 KiB
Go
134 lines
5.1 KiB
Go
package call
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"ai-operator/internal/ai"
|
|
"ai-operator/internal/media"
|
|
)
|
|
|
|
type pumpMedia struct {
|
|
audio chan media.AudioChunk
|
|
sent [][]byte
|
|
flushed int
|
|
}
|
|
|
|
func newPumpMedia() *pumpMedia {
|
|
return &pumpMedia{audio: make(chan media.AudioChunk, 4)}
|
|
}
|
|
func (m *pumpMedia) Audio() <-chan media.AudioChunk { return m.audio }
|
|
func (m *pumpMedia) SendAudio(ctx context.Context, data []byte) error {
|
|
m.sent = append(m.sent, append([]byte(nil), data...))
|
|
return nil
|
|
}
|
|
func (m *pumpMedia) FlushMedia(ctx context.Context) error { m.flushed++; return nil }
|
|
func (m *pumpMedia) Close(ctx context.Context) error { close(m.audio); return nil }
|
|
|
|
type pumpProvider struct {
|
|
sent []media.AudioChunk
|
|
}
|
|
|
|
func (p *pumpProvider) StartSession(ctx context.Context, config ai.VoiceSessionConfig) error {
|
|
return nil
|
|
}
|
|
func (p *pumpProvider) SendAudio(ctx context.Context, chunk media.AudioChunk) error {
|
|
p.sent = append(p.sent, media.AudioChunk{CallID: chunk.CallID, Data: append([]byte(nil), chunk.Data...), Codec: chunk.Codec, Timestamp: chunk.Timestamp})
|
|
return nil
|
|
}
|
|
func (p *pumpProvider) SendToolResult(ctx context.Context, result ai.ToolResult) error { return nil }
|
|
func (p *pumpProvider) Close(ctx context.Context) error { return nil }
|
|
func (p *pumpProvider) Events() <-chan ai.VoiceEvent { return nil }
|
|
func (p *pumpProvider) Stats() ai.VoiceProviderStats { return ai.VoiceProviderStats{} }
|
|
|
|
func TestAudioPumpForwardsAsteriskAudioToProvider(t *testing.T) {
|
|
pm := newPumpMedia()
|
|
pp := &pumpProvider{}
|
|
pump := &AudioPump{CallID: "c1", Media: pm, Provider: pp, AsteriskSampleRate: 16000, ProviderInputSampleRate: 24000, ProviderOutputSampleRate: 24000}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
if err := pump.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pm.audio <- media.AudioChunk{Data: []byte{0, 0, 1, 0, 2, 0, 3, 0}, Codec: media.CodecSLIN16, Timestamp: time.Now()}
|
|
deadline := time.Now().Add(time.Second)
|
|
for len(pp.sent) == 0 && time.Now().Before(deadline) {
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if len(pp.sent) != 1 {
|
|
t.Fatalf("provider chunks=%d", len(pp.sent))
|
|
}
|
|
if len(pp.sent[0].Data) <= 8 {
|
|
t.Fatalf("expected 16k->24k resample to increase bytes, got %d", len(pp.sent[0].Data))
|
|
}
|
|
}
|
|
|
|
func TestAudioPumpForwardsProviderAudioToAsterisk(t *testing.T) {
|
|
pm := newPumpMedia()
|
|
pp := &pumpProvider{}
|
|
pump := &AudioPump{CallID: "c1", Media: pm, Provider: pp, AsteriskSampleRate: 16000, ProviderInputSampleRate: 24000, ProviderOutputSampleRate: 24000}
|
|
in := []byte{0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0}
|
|
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventAssistantAudioDelta, Audio: in}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(pm.sent) != 1 {
|
|
t.Fatalf("media sent chunks=%d", len(pm.sent))
|
|
}
|
|
if len(pm.sent[0]) >= len(in) {
|
|
t.Fatalf("expected 24k->16k resample to reduce bytes, got %d from %d", len(pm.sent[0]), len(in))
|
|
}
|
|
pump.mu.Lock()
|
|
pump.suppressInputUntil = time.Now().Add(-time.Second)
|
|
pump.mu.Unlock()
|
|
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventInterruption}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pm.flushed != 1 {
|
|
t.Fatalf("flush count=%d", pm.flushed)
|
|
}
|
|
}
|
|
|
|
func TestAudioPumpSuppressesInputDuringAssistantPlayback(t *testing.T) {
|
|
pm := newPumpMedia()
|
|
pp := &pumpProvider{}
|
|
pump := &AudioPump{CallID: "c1", Media: pm, Provider: pp, AsteriskSampleRate: 16000, ProviderInputSampleRate: 24000, ProviderOutputSampleRate: 24000}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
if err := pump.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventAssistantAudioDelta, Audio: []byte{0, 0, 1, 0, 2, 0, 3, 0}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pm.audio <- media.AudioChunk{Data: []byte{0, 0, 1, 0, 2, 0, 3, 0}, Codec: media.CodecSLIN16, Timestamp: time.Now()}
|
|
time.Sleep(50 * time.Millisecond)
|
|
if len(pp.sent) != 0 {
|
|
t.Fatalf("provider received echo audio chunks=%d", len(pp.sent))
|
|
}
|
|
}
|
|
|
|
func TestAudioPumpIgnoresEchoInterruptionDuringAssistantPlayback(t *testing.T) {
|
|
pm := newPumpMedia()
|
|
pp := &pumpProvider{}
|
|
pump := &AudioPump{CallID: "c1", Media: pm, Provider: pp, AsteriskSampleRate: 16000, ProviderInputSampleRate: 24000, ProviderOutputSampleRate: 24000}
|
|
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventAssistantAudioDelta, Audio: []byte{0, 0, 1, 0, 2, 0, 3, 0}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventInterruption}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pm.flushed != 0 {
|
|
t.Fatalf("unexpected flush during assistant echo suppression: %d", pm.flushed)
|
|
}
|
|
pump.mu.Lock()
|
|
pump.suppressInputUntil = time.Now().Add(-time.Second)
|
|
pump.mu.Unlock()
|
|
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventInterruption}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pm.flushed != 1 {
|
|
t.Fatalf("flush count=%d", pm.flushed)
|
|
}
|
|
}
|