Files
go-mxl-player/cmd/mxl-audio/main.go
T
2026-08-23 12:40:59 +03:00

145 lines
3.2 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"mxl-player/internal/sdl"
"mxl-player/internal/source"
)
func main() {
domain := flag.String("d", "/dev/shm/mxl", "MXL domain")
flowID := flag.String("a", "", "MXL audio flow UUID")
list := flag.Bool("l", false, "List playback audio devices and exit")
audioDeviceId := flag.Uint("ad", uint(sdl.AudioDeviceDefaultPlayback), "Audio device SDL id")
flag.Parse()
if err := sdl.Load(); err != nil {
log.Fatal(err)
}
if !sdl.Init(sdl.InitVideo | sdl.InitAudio) {
log.Fatalf("SDL_Init: %s", sdl.GetError())
}
defer sdl.Quit()
if *list {
for _, d := range sdl.GetAudioPlaybackDevices() {
fmt.Printf("%d: %s\n", d.ID, d.Name)
}
return
}
if *flowID == "" {
log.Fatal("missing -a <audio-flow-uuid>")
}
src, err := source.OpenAudio(*domain, *flowID)
if err != nil {
log.Fatal(err)
}
defer src.Close()
rate := src.Rate()
chans := src.Channels()
fmt.Printf("audio: %dch %d/%d Hz\n", chans, rate.Num, rate.Den)
stream := sdl.OpenAudioDeviceStream(uint32(*audioDeviceId), sdl.AudioSpec{
Format: sdl.AudioF32,
Channels: int32(chans),
Freq: int32(rate.Num / rate.Den),
})
if stream == 0 {
log.Fatalf("OpenAudioDeviceStream: %s", sdl.GetError())
}
defer sdl.DestroyAudioStream(stream)
if !sdl.ResumeAudioStreamDevice(stream) {
log.Fatalf("ResumeAudioStreamDevice: %s", sdl.GetError())
}
// ~10ms batch: sampleRate / 100
batch := uint64(rate.Num / (100 * rate.Den))
if batch == 0 {
batch = 1
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
go func() {
<-stop
cancel()
}()
var debugCount int
for {
select {
case <-ctx.Done():
fmt.Println("\nstopped")
return
default:
}
// Backpressure: if SDL has > 200ms buffered, wait for it to drain.
queued := sdl.GetAudioStreamQueued(stream)
maxQueued := int32(rate.Num/(100*rate.Den)) * int32(chans) * 4 * 20 // 200ms
if queued > maxQueued {
time.Sleep(10 * time.Millisecond)
continue
}
f, err := src.NextAudio(ctx, batch, 20*time.Millisecond)
if err != nil {
if ctx.Err() != nil {
fmt.Println("\nstopped")
return
}
log.Printf("audio read: %v", err)
continue
}
// Debug: scan for non-zero samples
if debugCount < 5 {
sizes := make([]int, len(f.Samples))
nonZero := 0
for i, s := range f.Samples {
sizes[i] = len(s)
for _, b := range s {
if b != 0 {
nonZero++
}
}
}
fmt.Printf("read idx=%d batch=%d sampleSizes=%v queued=%d nonZeroBytes=%d\n",
f.Index, batch, sizes, queued, nonZero)
debugCount++
}
// Interleave per-channel Float32 into a single buffer.
frameBytes := int(batch) * int(chans) * 4
interleaved := make([]byte, frameBytes)
for ch := uint64(0); ch < chans; ch++ {
srcBytes := f.Samples[ch]
for i := uint64(0); i < batch; i++ {
srcOff := i * 4
dstOff := (i*chans + ch) * 4
if srcOff+4 <= uint64(len(srcBytes)) {
copy(interleaved[dstOff:dstOff+4], srcBytes[srcOff:srcOff+4])
}
}
}
if !sdl.PutAudioStreamData(stream, interleaved) {
log.Printf("PutAudioStreamData: %s", sdl.GetError())
}
}
}