Expose live application audio groups

This commit is contained in:
Gabriel Brown
2026-08-18 15:00:18 -04:00
parent 9aea519e54
commit 0f3bddc452
3 changed files with 53 additions and 0 deletions
@@ -1,8 +1,10 @@
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Pipewire
import QtQuick import QtQuick
import "services/AudioStreams.js" as AudioStreams import "services/AudioStreams.js" as AudioStreams
import qs.services
ShellRoot { ShellRoot {
readonly property int audioOutStreamFlag: 4 readonly property int audioOutStreamFlag: 4
@@ -112,5 +114,23 @@ ShellRoot {
muted: chromium.nodes.map(node => node.audio.muted) muted: chromium.nodes.map(node => node.audio.muted)
}); });
} }
function serviceSummary(): string {
const applications = AudioDevices.applications;
return JSON.stringify({
count: applications.length,
validTypes: applications.every(application =>
application.nodes.every(node =>
(node.type & PwNodeType.AudioOutStream)
=== PwNodeType.AudioOutStream))
});
}
function invalidMutations(): string {
return JSON.stringify({
nullVolume: AudioDevices.setApplicationVolume(null, 0.5),
emptyMute: AudioDevices.setApplicationMuted({ nodes: [] }, true)
});
}
} }
} }
@@ -8,6 +8,8 @@ import Quickshell
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import QtQuick import QtQuick
import "AudioStreams.js" as AudioStreams
Singleton { Singleton {
id: root id: root
@@ -18,6 +20,13 @@ Singleton {
!node.isStream !node.isStream
&& (node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource) && (node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource)
readonly property var playbackStreams: Pipewire.nodes.values.filter(node =>
node.ready && node.audio
&& (node.type & PwNodeType.AudioOutStream) === PwNodeType.AudioOutStream)
readonly property var applications: AudioStreams.group(
root.playbackStreams, PwNodeType.AudioOutStream)
function nodes(output: bool): var { function nodes(output: bool): var {
return output ? root.outputs : root.inputs; return output ? root.outputs : root.inputs;
} }
@@ -40,4 +49,20 @@ Singleton {
return "Unknown device"; return "Unknown device";
return node.description || node.nickname || node.name || "Unknown device"; return node.description || node.nickname || node.name || "Unknown device";
} }
function applicationVolume(application: var): real {
return AudioStreams.volume(application);
}
function applicationMuted(application: var): bool {
return AudioStreams.muted(application);
}
function setApplicationVolume(application: var, value: real): bool {
return AudioStreams.setVolume(application, value);
}
function setApplicationMuted(application: var, muted: bool): bool {
return AudioStreams.setMuted(application, muted);
}
} }
@@ -77,6 +77,14 @@ mute_result="$(qs_for_harness ipc call application-volume-test mutateMute)"
jq -e '.changed == true and .muted == [true, true]' <<<"$mute_result" >/dev/null \ jq -e '.changed == true and .muted == [true, true]' <<<"$mute_result" >/dev/null \
|| fail "mute mutation did not reach every stream: $mute_result" || fail "mute mutation did not reach every stream: $mute_result"
service_summary="$(qs_for_harness ipc call application-volume-test serviceSummary)"
jq -e '.count >= 0 and .validTypes == true' <<<"$service_summary" >/dev/null \
|| fail "live service exposed invalid playback groups: $service_summary"
invalid_result="$(qs_for_harness ipc call application-volume-test invalidMutations)"
jq -e '.nullVolume == false and .emptyMute == false' <<<"$invalid_result" >/dev/null \
|| fail "service mutators accepted missing audio nodes: $invalid_result"
if rg -n 'ReferenceError|TypeError|Binding loop|Cannot assign|PwObjectTracker' "$shell_log"; then if rg -n 'ReferenceError|TypeError|Binding loop|Cannot assign|PwObjectTracker' "$shell_log"; then
fail 'isolated harness emitted a QML runtime error' fail 'isolated harness emitted a QML runtime error'
fi fi