// Audio source enumeration for the settings UI. package capture import ( "encoding/json" "os/exec" ) // AudioDevice describes a capturable PipeWire audio node. type AudioDevice struct { Serial uint64 // PipeWire object.serial, passed to the capture backend ID uint32 // PipeWire node id (informational) Name string // node name, e.g. "alsa_output...analog-stereo" Desc string // human-readable description IsOutput bool // true = a sink (system output); false = a source (mic) } // pwDumpNode is the subset of `pw-dump` output we parse. type pwDumpNode struct { ID uint32 `json:"id"` Info struct { Props map[string]any `json:"props"` } `json:"info"` } // ListAudioSources enumerates PipeWire audio sinks and sources via `pw-dump`. // To capture system audio we attach to a sink's monitor; to capture a // microphone we attach to an Audio/Source node. This lists the capturable // audio nodes so the GUI can present a picker instead of always using the // system default output. func ListAudioSources() ([]AudioDevice, error) { out, err := exec.Command("pw-dump").Output() if err != nil { return nil, err } var nodes []pwDumpNode if err := json.Unmarshal(out, &nodes); err != nil { return nil, err } var sources []AudioDevice for _, n := range nodes { props := n.Info.Props mediaClass, _ := props["media.class"].(string) if !audioNodeClass(mediaClass) { continue } desc, _ := props["node.description"].(string) name, _ := props["node.name"].(string) serial := toUint64(props["object.serial"]) if serial == 0 { continue } sources = append(sources, AudioDevice{ Serial: serial, ID: n.ID, Name: name, Desc: desc, IsOutput: mediaClass == "Audio/Sink", }) } return sources, nil } // audioNodeClass reports whether a media.class is a capturable audio node. func audioNodeClass(mediaClass string) bool { switch mediaClass { case "Audio/Sink", "Audio/Source": return true default: return false } } // toUint64 best-effort converts a pw-dump property value to uint64. // PipeWire serials are small, so the conversions cannot overflow in practice. // //nolint:gosec // safe: JSON numbers from pw-dump are small node serials func toUint64(v any) uint64 { switch t := v.(type) { case float64: return uint64(t) case int: return uint64(t) case int64: return uint64(t) case uint64: return t case json.Number: if n, err := t.Int64(); err == nil { return uint64(n) } } return 0 }