Files
radiantwave-legacy/internal/mixer/device.go
T

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)
}