add SDL audio sink
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user