Files
go-mxl-player/internal/output/sdl_audio_test.go
T
2026-08-28 21:58:43 +03:00

214 lines
6.9 KiB
Go

package output
import (
"bytes"
"context"
"errors"
"testing"
"time"
"mxl-player/internal/playback"
"mxl-player/internal/sdl"
)
type fakeAudioBackend struct {
nextStream uintptr
openSpecs []sdl.AudioSpec
openDevice []uint32
resumeOK bool
putOK bool
puts [][]byte
queued []int32
queueCalls int
destroyed []uintptr
errText string
}
func (b *fakeAudioBackend) Open(device uint32, spec sdl.AudioSpec) uintptr {
b.openDevice = append(b.openDevice, device)
b.openSpecs = append(b.openSpecs, spec)
return b.nextStream
}
func (b *fakeAudioBackend) Resume(uintptr) bool { return b.resumeOK }
func (b *fakeAudioBackend) Put(_ uintptr, data []byte) bool {
b.puts = append(b.puts, append([]byte(nil), data...))
return b.putOK
}
func (b *fakeAudioBackend) Queued(uintptr) int32 {
if len(b.queued) == 0 {
return 0
}
index := b.queueCalls
if index >= len(b.queued) {
index = len(b.queued) - 1
}
b.queueCalls++
return b.queued[index]
}
func (b *fakeAudioBackend) Destroy(stream uintptr) {
b.destroyed = append(b.destroyed, stream)
}
func (b *fakeAudioBackend) Error() string { return b.errText }
func newTestSDLAudioSink(backend audioBackend) *SDLAudioSink {
return &SDLAudioSink{
deviceID: 7,
backend: backend,
maxQueuedBatches: 2,
queuePoll: time.Millisecond,
wait: func(context.Context, time.Duration) error { return nil },
}
}
func TestAudioOutputFormat(t *testing.T) {
tests := []struct {
name string
frame playback.AudioFrame
want audioFormat
wantErr bool
}{
{
name: "valid",
frame: playback.AudioFrame{Channels: 2, SampleRateNumerator: 48000, SampleRateDenominator: 1},
want: audioFormat{channels: 2, frequency: 48000},
},
{name: "zero channels", frame: playback.AudioFrame{SampleRateNumerator: 48000, SampleRateDenominator: 1}, wantErr: true},
{name: "zero numerator", frame: playback.AudioFrame{Channels: 2, SampleRateDenominator: 1}, wantErr: true},
{name: "zero denominator", frame: playback.AudioFrame{Channels: 2, SampleRateNumerator: 48000}, wantErr: true},
{name: "fractional frequency", frame: playback.AudioFrame{Channels: 2, SampleRateNumerator: 30000, SampleRateDenominator: 1001}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := audioOutputFormat(tt.frame)
if tt.wantErr {
if !errors.Is(err, ErrInvalidAudioFormat) {
t.Fatalf("audioOutputFormat() error = %v, want %v", err, ErrInvalidAudioFormat)
}
return
}
if err != nil || got != tt.want {
t.Fatalf("audioOutputFormat() = %#v, %v; want %#v, nil", got, err, tt.want)
}
})
}
}
func TestSDLAudioSinkOpensInterleavesAndReusesStream(t *testing.T) {
backend := &fakeAudioBackend{nextStream: 11, resumeOK: true, putOK: true}
sink := newTestSDLAudioSink(backend)
frame := validAudioFrame()
if err := sink.ConsumeAudio(context.Background(), frame); err != nil {
t.Fatalf("first ConsumeAudio() error = %v", err)
}
if err := sink.ConsumeAudio(context.Background(), frame); err != nil {
t.Fatalf("second ConsumeAudio() error = %v", err)
}
if len(backend.openSpecs) != 1 {
t.Fatalf("open calls = %d, want 1", len(backend.openSpecs))
}
wantSpec := sdl.AudioSpec{Format: sdl.AudioF32, Channels: 2, Freq: 48000}
if backend.openDevice[0] != 7 || backend.openSpecs[0] != wantSpec {
t.Fatalf("open = device %d spec %#v, want 7 %#v", backend.openDevice[0], backend.openSpecs[0], wantSpec)
}
wantData := []byte{1, 2, 3, 4, 9, 10, 11, 12, 5, 6, 7, 8, 13, 14, 15, 16}
if len(backend.puts) != 2 || !bytes.Equal(backend.puts[0], wantData) {
t.Fatalf("queued data = %v, want %v twice", backend.puts, wantData)
}
}
func TestSDLAudioSinkRecreatesStreamOnFormatChange(t *testing.T) {
backend := &fakeAudioBackend{nextStream: 11, resumeOK: true, putOK: true}
sink := newTestSDLAudioSink(backend)
first := validAudioFrame()
if err := sink.ConsumeAudio(context.Background(), first); err != nil {
t.Fatalf("first ConsumeAudio() error = %v", err)
}
backend.nextStream = 12
second := first
second.SampleRateNumerator = 96000
if err := sink.ConsumeAudio(context.Background(), second); err != nil {
t.Fatalf("second ConsumeAudio() error = %v", err)
}
if len(backend.openSpecs) != 2 || len(backend.destroyed) != 1 || backend.destroyed[0] != 11 {
t.Fatalf("opens=%d destroyed=%v, want 2 and [11]", len(backend.openSpecs), backend.destroyed)
}
}
func TestSDLAudioSinkReportsOpenResumeAndPutFailures(t *testing.T) {
frame := validAudioFrame()
t.Run("open", func(t *testing.T) {
backend := &fakeAudioBackend{errText: "open failed"}
err := newTestSDLAudioSink(backend).ConsumeAudio(context.Background(), frame)
if !errors.Is(err, ErrOpenAudioStream) {
t.Fatalf("ConsumeAudio() error = %v, want %v", err, ErrOpenAudioStream)
}
})
t.Run("resume", func(t *testing.T) {
backend := &fakeAudioBackend{nextStream: 11, errText: "resume failed"}
sink := newTestSDLAudioSink(backend)
err := sink.ConsumeAudio(context.Background(), frame)
if !errors.Is(err, ErrResumeAudioStream) || sink.stream != 0 {
t.Fatalf("ConsumeAudio() error=%v stream=%d, want resume error and zero stream", err, sink.stream)
}
if len(backend.destroyed) != 1 || backend.destroyed[0] != 11 {
t.Fatalf("destroyed = %v, want [11]", backend.destroyed)
}
})
t.Run("put", func(t *testing.T) {
backend := &fakeAudioBackend{nextStream: 11, resumeOK: true, errText: "put failed"}
err := newTestSDLAudioSink(backend).ConsumeAudio(context.Background(), frame)
if !errors.Is(err, ErrQueueAudioData) {
t.Fatalf("ConsumeAudio() error = %v, want %v", err, ErrQueueAudioData)
}
})
}
func TestSDLAudioSinkWaitsForQueueAndHonorsCancellation(t *testing.T) {
backend := &fakeAudioBackend{
nextStream: 11,
resumeOK: true,
putOK: true,
queued: []int32{100, 100},
}
sink := newTestSDLAudioSink(backend)
ctx, cancel := context.WithCancel(context.Background())
waits := 0
sink.wait = func(ctx context.Context, _ time.Duration) error {
waits++
cancel()
return ctx.Err()
}
err := sink.ConsumeAudio(ctx, validAudioFrame())
if !errors.Is(err, context.Canceled) {
t.Fatalf("ConsumeAudio() error = %v, want context.Canceled", err)
}
if waits != 1 || len(backend.puts) != 0 {
t.Fatalf("waits=%d puts=%d, want 1 and 0", waits, len(backend.puts))
}
}
func TestSDLAudioSinkCloseIsIdempotent(t *testing.T) {
backend := &fakeAudioBackend{nextStream: 11, resumeOK: true, putOK: true}
sink := newTestSDLAudioSink(backend)
if err := sink.ConsumeAudio(context.Background(), validAudioFrame()); err != nil {
t.Fatalf("ConsumeAudio() error = %v", err)
}
if err := sink.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
if err := sink.Close(); err != nil {
t.Fatalf("second Close() error = %v", err)
}
if len(backend.destroyed) != 1 || sink.stream != 0 || sink.format != (audioFormat{}) {
t.Fatalf("destroyed=%v stream=%d format=%#v", backend.destroyed, sink.stream, sink.format)
}
}