Clean up audio device choices
This commit is contained in:
@@ -52,6 +52,9 @@ Item {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var sinkAvailability: ({})
|
||||||
|
property bool sinkAvailabilityLoaded: false
|
||||||
|
|
||||||
// Identify true playback streams without reading node.properties here:
|
// Identify true playback streams without reading node.properties here:
|
||||||
// PwNode.properties is invalid until the node is bound, and reading it while
|
// PwNode.properties is invalid until the node is bound, and reading it while
|
||||||
// capture streams are appearing (for example, when Voxtype starts recording)
|
// capture streams are appearing (for example, when Voxtype starts recording)
|
||||||
@@ -72,7 +75,7 @@ Item {
|
|||||||
readonly property var audioSinks: {
|
readonly property var audioSinks: {
|
||||||
var list = []
|
var list = []
|
||||||
for (var i = 0; i < candidateSinks.length; i++)
|
for (var i = 0; i < candidateSinks.length; i++)
|
||||||
if (candidateSinks[i].audio) list.push(candidateSinks[i])
|
if (candidateSinks[i].audio && sinkAvailable(candidateSinks[i])) list.push(candidateSinks[i])
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,12 +296,66 @@ Item {
|
|||||||
if (source && source.audio) source.audio.muted = !source.audio.muted
|
if (source && source.audio) source.audio.muted = !source.audio.muted
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDefaultSink(node) { Pipewire.preferredDefaultAudioSink = node }
|
function setDefaultSink(node) {
|
||||||
function setDefaultSource(node) { Pipewire.preferredDefaultAudioSource = node }
|
if (!node) return
|
||||||
|
Pipewire.preferredDefaultAudioSink = node
|
||||||
|
if (root.bar && node.id !== undefined && node.name) {
|
||||||
|
var idArg = root.bar.shellQuote(String(node.id))
|
||||||
|
var nameArg = root.bar.shellQuote(String(node.name))
|
||||||
|
root.bar.run("wpctl set-default " + idArg + " 2>/dev/null || true; "
|
||||||
|
+ "pactl set-default-sink " + nameArg + " 2>/dev/null || true; "
|
||||||
|
+ "pactl list short sink-inputs 2>/dev/null | awk '{ print $1 }' | while read -r input; do "
|
||||||
|
+ "pactl move-sink-input \"$input\" " + nameArg + " 2>/dev/null || true; done")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultSource(node) {
|
||||||
|
if (!node) return
|
||||||
|
Pipewire.preferredDefaultAudioSource = node
|
||||||
|
if (root.bar && node.id !== undefined && node.name) {
|
||||||
|
var idArg = root.bar.shellQuote(String(node.id))
|
||||||
|
var nameArg = root.bar.shellQuote(String(node.name))
|
||||||
|
root.bar.run("wpctl set-default " + idArg + " 2>/dev/null || true; "
|
||||||
|
+ "pactl set-default-source " + nameArg + " 2>/dev/null || true; "
|
||||||
|
+ "pactl list short source-outputs 2>/dev/null | awk '{ print $1 }' | while read -r output; do "
|
||||||
|
+ "pactl move-source-output \"$output\" " + nameArg + " 2>/dev/null || true; done")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sinkAvailable(node) {
|
||||||
|
if (!node || !node.name || !sinkAvailabilityLoaded) return true
|
||||||
|
var name = String(node.name)
|
||||||
|
return sinkAvailability[name] !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSinkAvailability(raw) {
|
||||||
|
var next = {}
|
||||||
|
var lines = String(raw || "").split("\n")
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var line = lines[i].trim()
|
||||||
|
if (!line) continue
|
||||||
|
var parts = line.split("\t")
|
||||||
|
if (parts.length >= 2) next[parts[0]] = parts[1] !== "0"
|
||||||
|
}
|
||||||
|
sinkAvailability = next
|
||||||
|
sinkAvailabilityLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function friendlyDeviceLabel(text) {
|
||||||
|
var label = String(text || "").trim()
|
||||||
|
label = label.replace(/^sof-soundwire\s+/i, "")
|
||||||
|
label = label.replace(/^built-?in audio\s+/i, "")
|
||||||
|
label = label.replace(/\s+Output$/i, "")
|
||||||
|
label = label.replace(/\s+Input$/i, "")
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
function nodeLabel(node) {
|
function nodeLabel(node) {
|
||||||
if (!node) return "Unknown"
|
if (!node) return "Unknown"
|
||||||
return node.description || node.nickname || node.name || "Unknown"
|
var p = nodeProps(node)
|
||||||
|
var nickname = friendlyDeviceLabel(node.nickname || node.nick || p["node.nick"] || p["device.profile.description"] || "")
|
||||||
|
if (nickname) return nickname
|
||||||
|
return friendlyDeviceLabel(node.description || p["node.description"] || node.name || "Unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeProps(node) {
|
function nodeProps(node) {
|
||||||
@@ -345,6 +402,47 @@ Item {
|
|||||||
PwObjectTracker { objects: root.candidateSources }
|
PwObjectTracker { objects: root.candidateSources }
|
||||||
PwObjectTracker { objects: root.audioStreams }
|
PwObjectTracker { objects: root.audioStreams }
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: sinkAvailabilityProc
|
||||||
|
command: ["bash", "-lc", `
|
||||||
|
pactl list sinks 2>/dev/null | python3 -c '
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
|
||||||
|
name = re.search(r"(?m)^\\s*Name:\\s*(\\S+)", block)
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ports = []
|
||||||
|
in_ports = False
|
||||||
|
for line in block.splitlines():
|
||||||
|
if line.strip() == "Ports:":
|
||||||
|
in_ports = True
|
||||||
|
continue
|
||||||
|
if in_ports and line.startswith("\\tActive Port:"):
|
||||||
|
in_ports = False
|
||||||
|
if in_ports and line.startswith("\\t\\t"):
|
||||||
|
ports.append(line)
|
||||||
|
|
||||||
|
available = not ports or any("not available" not in port for port in ports)
|
||||||
|
print("%s\\t%d" % (name.group(1), 1 if available else 0))
|
||||||
|
'
|
||||||
|
`]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
waitForEnd: true
|
||||||
|
onStreamFinished: root.updateSinkAvailability(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 5000
|
||||||
|
running: root.popupOpen
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: if (!sinkAvailabilityProc.running) sinkAvailabilityProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
// Lets a Hyprland keybind summon the panel without a click. Mirrors the
|
// Lets a Hyprland keybind summon the panel without a click. Mirrors the
|
||||||
// networkPanel IpcHandler pattern; KeyboardPanel grants Exclusive focus
|
// networkPanel IpcHandler pattern; KeyboardPanel grants Exclusive focus
|
||||||
// at map-time so j/k/h/l work the moment the panel appears.
|
// at map-time so j/k/h/l work the moment the panel appears.
|
||||||
|
|||||||
Reference in New Issue
Block a user