MXL audio sample player app

This commit is contained in:
Dmitry Sergeev
2026-08-23 12:40:59 +03:00
parent 549813221d
commit a4ab294ab5
3 changed files with 213 additions and 16 deletions
+144
View File
@@ -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 <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())
}
}
}
+61
View File
@@ -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)
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
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
}
-8
View File
@@ -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