Files
ai-operator/internal/call/audio_pump_test.go
T

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)
}
}