Add audio device switching UI
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user