package output import ( "context" "errors" "fmt" "math" "time" "mxl-player/internal/playback" "mxl-player/internal/sdl" ) const ( defaultMaxQueuedBatches = int32(20) defaultAudioQueuePoll = 10 * time.Millisecond ) var ( ErrInvalidAudioFormat = errors.New("invalid audio output format") ErrOpenAudioStream = errors.New("open SDL audio stream") ErrResumeAudioStream = errors.New("resume SDL audio stream") ErrQueueAudioData = errors.New("queue SDL audio data") ) type audioBackend interface { Open(uint32, sdl.AudioSpec) uintptr Resume(uintptr) bool Put(uintptr, []byte) bool Queued(uintptr) int32 Destroy(uintptr) Error() string } type systemAudioBackend struct{} func (systemAudioBackend) Open(device uint32, spec sdl.AudioSpec) uintptr { return sdl.OpenAudioDeviceStream(device, spec) } func (systemAudioBackend) Resume(stream uintptr) bool { return sdl.ResumeAudioStreamDevice(stream) } func (systemAudioBackend) Put(stream uintptr, data []byte) bool { return sdl.PutAudioStreamData(stream, data) } func (systemAudioBackend) Queued(stream uintptr) int32 { return sdl.GetAudioStreamQueued(stream) } func (systemAudioBackend) Destroy(stream uintptr) { sdl.DestroyAudioStream(stream) } func (systemAudioBackend) Error() string { return sdl.GetError() } type audioFormat struct { channels int32 frequency int32 } type audioQueueWaitFunc func(context.Context, time.Duration) error type SDLAudioSink struct { deviceID uint32 backend audioBackend stream uintptr format audioFormat maxQueuedBatches int32 queuePoll time.Duration wait audioQueueWaitFunc } var _ playback.AudioSink = (*SDLAudioSink)(nil) func NewSDLAudioSink(deviceID uint32) *SDLAudioSink { return &SDLAudioSink{ deviceID: deviceID, backend: systemAudioBackend{}, maxQueuedBatches: defaultMaxQueuedBatches, queuePoll: defaultAudioQueuePoll, wait: waitForAudioQueue, } } func waitForAudioQueue(ctx context.Context, delay time.Duration) error { timer := time.NewTimer(delay) defer timer.Stop() select { case <-timer.C: return nil case <-ctx.Done(): return ctx.Err() } } func audioOutputFormat(frame playback.AudioFrame) (audioFormat, error) { if frame.Channels == 0 || frame.Channels > math.MaxInt32 || frame.SampleRateNumerator <= 0 || frame.SampleRateDenominator <= 0 || frame.SampleRateNumerator%frame.SampleRateDenominator != 0 { return audioFormat{}, fmt.Errorf( "%w: channels=%d rate=%d/%d", ErrInvalidAudioFormat, frame.Channels, frame.SampleRateNumerator, frame.SampleRateDenominator, ) } frequency := frame.SampleRateNumerator / frame.SampleRateDenominator if frequency <= 0 || frequency > math.MaxInt32 { return audioFormat{}, fmt.Errorf( "%w: frequency=%d", ErrInvalidAudioFormat, frequency, ) } return audioFormat{ channels: int32(frame.Channels), frequency: int32(frequency), }, nil } func (s *SDLAudioSink) ConsumeAudio( ctx context.Context, frame playback.AudioFrame, ) error { if err := ctx.Err(); err != nil { return err } format, err := audioOutputFormat(frame) if err != nil { return err } interleaved, err := InterleaveF32(frame) if err != nil { return err } if s.stream == 0 || s.format != format { if err := s.recreateStream(format); err != nil { return err } } if s.maxQueuedBatches <= 0 || len(interleaved) > math.MaxInt32/int(s.maxQueuedBatches) { return fmt.Errorf( "%w: batch bytes=%d queue batches=%d", ErrInvalidAudioFormat, len(interleaved), s.maxQueuedBatches, ) } maxQueuedBytes := int32(len(interleaved)) * s.maxQueuedBatches for { queued := s.backend.Queued(s.stream) if queued < 0 { return fmt.Errorf("%w: query queued bytes: %s", ErrQueueAudioData, s.backend.Error()) } if queued <= maxQueuedBytes { break } if err := s.wait(ctx, s.queuePoll); err != nil { if ctx.Err() != nil { return ctx.Err() } return err } } if !s.backend.Put(s.stream, interleaved) { return fmt.Errorf("%w: %s", ErrQueueAudioData, s.backend.Error()) } return nil } func (s *SDLAudioSink) recreateStream(format audioFormat) error { if s.stream != 0 { s.backend.Destroy(s.stream) s.stream = 0 s.format = audioFormat{} } stream := s.backend.Open(s.deviceID, sdl.AudioSpec{ Format: sdl.AudioF32, Channels: format.channels, Freq: format.frequency, }) if stream == 0 { return fmt.Errorf("%w: %s", ErrOpenAudioStream, s.backend.Error()) } if !s.backend.Resume(stream) { s.backend.Destroy(stream) return fmt.Errorf("%w: %s", ErrResumeAudioStream, s.backend.Error()) } s.stream = stream s.format = format return nil } func (s *SDLAudioSink) Close() error { if s.stream != 0 { s.backend.Destroy(s.stream) s.stream = 0 } s.format = audioFormat{} return nil }