package mixer import ( "strings" "github.com/veandco/go-sdl2/sdl" ) // Device describes an audio output device as seen through SDL's core // enumeration APIs. Name is the raw SDL device string used to open the device // and to persist the selection; Label is the humanized name shown to the user. type Device struct { Name string Label string } // humanizeRules maps raw SDL device names to human-readable labels. Rules are // evaluated in order; the first rule whose keyword is a case-insensitive // substring of the raw name wins. var humanizeRules = []struct { keywords []string label string }{ {[]string{"hdmi", "monitor", "tv"}, "HDMI | Monitor | TV"}, {[]string{"headset", "headphone"}, "Headset"}, {[]string{"usb"}, "USB"}, {[]string{"analog", "built-in", "builtin", "internal", "speaker"}, "Internal Speakers"}, } // HumanizeDeviceName maps a raw SDL audio device name to a human-readable // label. Raw names come from SDL_GetAudioDeviceName, e.g. PipeWire sink // descriptions ("Built-in Audio Analog Stereo", "Built-in Audio Digital // Stereo (HDMI)") or PulseAudio sink names ("alsa_output.pci-0000_00_1f.3. // analog-stereo"). If no rule matches, the raw name is returned unchanged. func HumanizeDeviceName(raw string) string { lower := strings.ToLower(raw) for _, rule := range humanizeRules { for _, kw := range rule.keywords { if strings.Contains(lower, kw) { return rule.label } } } return raw } // enumeratePlaybackDevices returns the available playback devices from SDL's // core audio enumeration, each with a humanized label. It is the single place // that touches SDL2's device enumeration APIs; a future SDL3 migration only // needs to change this function and the mixer.OpenAudioDevice boundary. func enumeratePlaybackDevices() []Device { n := sdl.GetNumAudioDevices(false) if n <= 0 { return nil } out := make([]Device, 0, n) for i := 0; i < n; i++ { if name := sdl.GetAudioDeviceName(i, false); name != "" { out = append(out, Device{Name: name, Label: HumanizeDeviceName(name)}) } } return out } // ListDevices returns the list of available output devices (from core SDL). // Name holds the raw device string (use it with SwitchDevice/CurrentDevice); // Label holds the humanized display name. func ListDevices() []Device { mu.Lock() defer mu.Unlock() return enumeratePlaybackDevices() } // matchDevice resolves a stored/preferred device name against enumerated // device names. An exact match wins; otherwise the first device whose name // contains the preferred string (case-insensitive) is returned, tolerating // PipeWire/PulseAudio node-name suffixes. Returns false when the preferred // name is empty or no device matches. func matchDevice(names []string, preferred string) (string, bool) { if preferred == "" { return "", false } for _, name := range names { if name == preferred { return name, true } } lowerPref := strings.ToLower(preferred) for _, name := range names { if strings.Contains(strings.ToLower(name), lowerPref) { return name, true } } return "", false } // resolvePreferredDevice finds the enumeration name to open for a persisted // device selection. Returns false when the preferred name is empty or no // enumerated device matches, in which case the caller should fall back to the // system default. func resolvePreferredDevice(preferred string) (string, bool) { devs := enumeratePlaybackDevices() names := make([]string, 0, len(devs)) for _, d := range devs { names = append(names, d.Name) } return matchDevice(names, preferred) }