109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
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)
|
|
}
|