From a4ab294ab508daccad02adc1fce136d336009d34 Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Sun, 23 Aug 2026 12:40:59 +0300 Subject: [PATCH] MXL audio sample player app --- cmd/mxl-audio/main.go | 144 ++++++++++++++++++++++++++++++++++++++++++ internal/sdl/sdl.go | 77 +++++++++++++++++++--- plan_step0.md | 8 --- 3 files changed, 213 insertions(+), 16 deletions(-) create mode 100644 cmd/mxl-audio/main.go delete mode 100644 plan_step0.md diff --git a/cmd/mxl-audio/main.go b/cmd/mxl-audio/main.go new file mode 100644 index 0000000..d1bde61 --- /dev/null +++ b/cmd/mxl-audio/main.go @@ -0,0 +1,144 @@ +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 ") + } + + 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()) + } + } +} diff --git a/internal/sdl/sdl.go b/internal/sdl/sdl.go index dd760f5..bcbdb41 100644 --- a/internal/sdl/sdl.go +++ b/internal/sdl/sdl.go @@ -20,19 +20,35 @@ const ( KeyEscape uint32 = 0x1B KeyF uint32 = 0x66 + + InitAudio uint32 = 0x00000010 + AudioDeviceDefaultPlayback uint32 = 0xFFFFFFFF + AudioF32 uint16 = 0x8120 ) var ( - sdlInit func(flags uint32) bool - sdlQuit func() - sdlGetError func() uintptr - sdlCreateWindow func(title *byte, w, h int32, flags uint64) uintptr - sdlDestroyWindow func(window uintptr) + sdlInit func(flags uint32) bool + sdlQuit func() + sdlGetError func() uintptr + + sdlCreateWindow func(title *byte, w, h int32, flags uint64) uintptr + sdlDestroyWindow func(window uintptr) + sdlVulkanGetInstanceExtensions func(count *uint32) uintptr sdlVulkanCreateSurface func(window, instance, allocator uintptr, surface *uint64) bool - sdlPollEvent func(event unsafe.Pointer) bool - sdlGetWindowSizeInPixels func(window uintptr, w, h *int32) bool - sdlSetWindowFullscreen func(window uintptr, fullscreen bool) bool + + sdlPollEvent func(event unsafe.Pointer) bool + + sdlGetWindowSizeInPixels func(window uintptr, w, h *int32) bool + sdlSetWindowFullscreen func(window uintptr, fullscreen bool) bool + + sdlOpenAudioDeviceStream func(devid uint32, spec unsafe.Pointer, callback uintptr, userdata uintptr) uintptr + sdlResumeAudioStreamDevice func(stream uintptr) bool + sdlPutAudioStreamData func(stream uintptr, buf unsafe.Pointer, length int32) bool + sdlGetAudioStreamQueued func(stream uintptr) int32 + sdlDestroyAudioStream func(stream uintptr) + sdlGetAudioPlaybackDevices func(count *int32) uintptr + sdlGetAudioDeviceName func(devid uint32) uintptr ) var loaded = false @@ -55,6 +71,14 @@ func Load() error { purego.RegisterLibFunc(&sdlPollEvent, h, "SDL_PollEvent") purego.RegisterLibFunc(&sdlGetWindowSizeInPixels, h, "SDL_GetWindowSizeInPixels") purego.RegisterLibFunc(&sdlSetWindowFullscreen, h, "SDL_SetWindowFullscreen") + // audio + purego.RegisterLibFunc(&sdlOpenAudioDeviceStream, h, "SDL_OpenAudioDeviceStream") + purego.RegisterLibFunc(&sdlResumeAudioStreamDevice, h, "SDL_ResumeAudioStreamDevice") + purego.RegisterLibFunc(&sdlPutAudioStreamData, h, "SDL_PutAudioStreamData") + purego.RegisterLibFunc(&sdlGetAudioStreamQueued, h, "SDL_GetAudioStreamQueued") + purego.RegisterLibFunc(&sdlDestroyAudioStream, h, "SDL_DestroyAudioStream") + purego.RegisterLibFunc(&sdlGetAudioPlaybackDevices, h, "SDL_GetAudioPlaybackDevices") + purego.RegisterLibFunc(&sdlGetAudioDeviceName, h, "SDL_GetAudioDeviceName") loaded = true return nil } @@ -113,3 +137,40 @@ func cbytes(s string) *byte { runtime.KeepAlive(b) return &b[0] } + +type AudioSpec struct { + Format uint16 + Channels int32 + Freq int32 +} + +func OpenAudioDeviceStream(devid uint32, spec AudioSpec) uintptr { + return sdlOpenAudioDeviceStream(devid, unsafe.Pointer(&spec), 0, 0) +} +func ResumeAudioStreamDevice(stream uintptr) bool { return sdlResumeAudioStreamDevice(stream) } +func DestroyAudioStream(stream uintptr) { sdlDestroyAudioStream(stream) } +func PutAudioStreamData(stream uintptr, buf []byte) bool { + return sdlPutAudioStreamData(stream, unsafe.Pointer(&buf[0]), int32(len(buf))) +} +func GetAudioStreamQueued(stream uintptr) int32 { return sdlGetAudioStreamQueued(stream) } + +// playback device +type AudioDevice struct { + ID uint32 + Name string +} + +func GetAudioPlaybackDevices() []AudioDevice { + var count int32 + ptr := sdlGetAudioPlaybackDevices(&count) + if ptr == 0 || count == 0 { + return nil + } + ids := unsafe.Slice((*uint32)(unsafe.Pointer(ptr)), count) + devs := make([]AudioDevice, 0, count) + for _, id := range ids { + name := cstr(sdlGetAudioDeviceName(id)) + devs = append(devs, AudioDevice{ID: id, Name: name}) + } + return devs +} diff --git a/plan_step0.md b/plan_step0.md deleted file mode 100644 index ad103b2..0000000 --- a/plan_step0.md +++ /dev/null @@ -1,8 +0,0 @@ -M0.1 Go module + SDL3 window -M0.2 Vulkan instance -M0.3 enumerate/select GPU -M0.4 SDL Vulkan surface -M0.5 logical device + queues -M0.6 swapchain -M0.7 clear window to a solid color -M0.8 resize + clean teardown