add local MXL audio adapter
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
package mxladapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"time"
|
||||
|
||||
"mxl-player/internal/playback"
|
||||
"mxl-player/internal/source"
|
||||
|
||||
mxl "github.com/qvest-digital/go-mxl/mxl"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAudioReadTimeout = 20 * time.Millisecond
|
||||
DefaultAudioBatchDuration = 10 * time.Millisecond
|
||||
DefaultAudioUnavailableAfter = 2 * time.Second
|
||||
DefaultAudioTemporaryDelay = 10 * time.Millisecond
|
||||
)
|
||||
|
||||
var ErrInvalidAudioBatch = errors.New("invalid audio batch configuration")
|
||||
|
||||
type AudioFactory struct {
|
||||
ReadTimeout time.Duration
|
||||
BatchDuration time.Duration
|
||||
UnavailableAfter time.Duration
|
||||
}
|
||||
|
||||
type localAudioSource interface {
|
||||
ReadAudioOnceCtx(context.Context, uint64, time.Duration) (source.AudioFrame, error)
|
||||
Rate() mxl.Rational
|
||||
Channels() uint64
|
||||
Close() error
|
||||
}
|
||||
|
||||
type audioReader struct {
|
||||
source localAudioSource
|
||||
readTimeout time.Duration
|
||||
batch uint64
|
||||
channels uint64
|
||||
rateNumerator int64
|
||||
rateDenominator int64
|
||||
unavailableAfter time.Duration
|
||||
retryDelay time.Duration
|
||||
now func() time.Time
|
||||
wait temporaryWaitFunc
|
||||
}
|
||||
|
||||
var _ playback.AudioReaderFactory = AudioFactory{}
|
||||
var _ playback.AudioReader = (*audioReader)(nil)
|
||||
|
||||
func audioBatchSize(
|
||||
rateNumerator int64,
|
||||
rateDenominator int64,
|
||||
duration time.Duration,
|
||||
) (uint64, error) {
|
||||
if rateNumerator <= 0 || rateDenominator <= 0 || duration <= 0 {
|
||||
return 0, fmt.Errorf(
|
||||
"%w: rate=%d/%d duration=%s",
|
||||
ErrInvalidAudioBatch,
|
||||
rateNumerator,
|
||||
rateDenominator,
|
||||
duration,
|
||||
)
|
||||
}
|
||||
|
||||
denominator := uint64(rateDenominator)
|
||||
seconds := uint64(time.Second)
|
||||
if denominator > ^uint64(0)/seconds {
|
||||
return 0, fmt.Errorf("%w: denominator overflow", ErrInvalidAudioBatch)
|
||||
}
|
||||
denominator *= seconds
|
||||
|
||||
high, low := bits.Mul64(uint64(rateNumerator), uint64(duration))
|
||||
if high >= denominator {
|
||||
return 0, fmt.Errorf("%w: sample count overflow", ErrInvalidAudioBatch)
|
||||
}
|
||||
batch, _ := bits.Div64(high, low, denominator)
|
||||
if batch == 0 {
|
||||
batch = 1
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
func (f AudioFactory) OpenAudio(
|
||||
ctx context.Context,
|
||||
config playback.FeedConfig,
|
||||
) (playback.AudioReader, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, &source.SourceError{
|
||||
Op: "validate audio feed",
|
||||
Kind: source.ErrorKindInvalidConfig,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if !config.IsConfigured() {
|
||||
return nil, &source.SourceError{
|
||||
Op: "validate audio feed",
|
||||
Kind: source.ErrorKindInvalidConfig,
|
||||
Err: errors.New("audio feed is not configured"),
|
||||
}
|
||||
}
|
||||
|
||||
src, err := source.OpenAudio(config.Domain, config.UUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open local MXL audio: %w", err)
|
||||
}
|
||||
closeOnError := true
|
||||
defer func() {
|
||||
if closeOnError {
|
||||
_ = src.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
readTimeout := f.ReadTimeout
|
||||
if readTimeout <= 0 {
|
||||
readTimeout = DefaultAudioReadTimeout
|
||||
}
|
||||
batchDuration := f.BatchDuration
|
||||
if batchDuration <= 0 {
|
||||
batchDuration = DefaultAudioBatchDuration
|
||||
}
|
||||
unavailableAfter := f.UnavailableAfter
|
||||
if unavailableAfter <= 0 {
|
||||
unavailableAfter = DefaultAudioUnavailableAfter
|
||||
}
|
||||
|
||||
rate := src.Rate()
|
||||
batch, err := audioBatchSize(rate.Num, rate.Den, batchDuration)
|
||||
if err != nil {
|
||||
return nil, &source.SourceError{
|
||||
Op: "calculate audio batch",
|
||||
Kind: source.ErrorKindInvalidConfig,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
reader := &audioReader{
|
||||
source: src,
|
||||
readTimeout: readTimeout,
|
||||
batch: batch,
|
||||
channels: src.Channels(),
|
||||
rateNumerator: rate.Num,
|
||||
rateDenominator: rate.Den,
|
||||
unavailableAfter: unavailableAfter,
|
||||
retryDelay: DefaultAudioTemporaryDelay,
|
||||
now: time.Now,
|
||||
wait: waitForTemporaryRetry,
|
||||
}
|
||||
closeOnError = false
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (r *audioReader) ReadAudio(ctx context.Context) (playback.AudioFrame, error) {
|
||||
var unavailableSince time.Time
|
||||
|
||||
for {
|
||||
frame, err := r.source.ReadAudioOnceCtx(ctx, r.batch, r.readTimeout)
|
||||
if err == nil {
|
||||
return playback.AudioFrame{
|
||||
Index: frame.Index,
|
||||
SampleCount: frame.SampleCount,
|
||||
Channels: frame.Channels,
|
||||
SampleRateNumerator: r.rateNumerator,
|
||||
SampleRateDenominator: r.rateDenominator,
|
||||
Samples: frame.Samples,
|
||||
}, nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return playback.AudioFrame{}, ctx.Err()
|
||||
}
|
||||
if source.KindOf(err) != source.ErrorKindTemporary {
|
||||
return playback.AudioFrame{}, err
|
||||
}
|
||||
|
||||
now := r.now()
|
||||
if unavailableSince.IsZero() {
|
||||
unavailableSince = now
|
||||
} else if now.Sub(unavailableSince) >= r.unavailableAfter {
|
||||
return playback.AudioFrame{}, &source.SourceError{
|
||||
Op: "read local MXL audio",
|
||||
Kind: source.ErrorKindUnavailable,
|
||||
Err: fmt.Errorf(
|
||||
"no audio data for %s: %w",
|
||||
r.unavailableAfter,
|
||||
err,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.wait(ctx, r.retryDelay); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return playback.AudioFrame{}, ctx.Err()
|
||||
}
|
||||
return playback.AudioFrame{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *audioReader) Close() error {
|
||||
return r.source.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user