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

205 lines
4.8 KiB
Go

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
}