100 lines
3.1 KiB
QML
100 lines
3.1 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Services.Pipewire
|
|
|
|
// Live audio state straight from PipeWire, so the bar and OSD react the instant
|
|
// a volume key is pressed rather than on the next poll.
|
|
Singleton {
|
|
id: root
|
|
|
|
readonly property PwNode sink: Pipewire.defaultAudioSink
|
|
readonly property PwNode source: Pipewire.defaultAudioSource
|
|
|
|
// Binding node.audio.* only works while the node is tracked.
|
|
PwObjectTracker {
|
|
objects: [root.sink, root.source].filter(n => n !== null)
|
|
}
|
|
|
|
readonly property bool ready: sink !== null && sink.audio !== null
|
|
|
|
readonly property real volume: sink?.audio?.volume ?? 0
|
|
readonly property bool muted: sink?.audio?.muted ?? true
|
|
readonly property int volumePercent: Math.round(volume * 100)
|
|
readonly property string sinkName: sink?.nickname || sink?.description || sink?.name || "No output"
|
|
|
|
readonly property real micVolume: source?.audio?.volume ?? 0
|
|
readonly property bool micMuted: source?.audio?.muted ?? true
|
|
readonly property int micPercent: Math.round(micVolume * 100)
|
|
readonly property string sourceName: source?.nickname || source?.description || source?.name || "No input"
|
|
|
|
// Every sink on the box, for the output picker in the audio popout.
|
|
readonly property var sinks: Pipewire.nodes.values.filter(n =>
|
|
n.isSink && !n.isStream && n.audio !== null)
|
|
|
|
// Application streams, for the per-app mixer.
|
|
readonly property var streams: Pipewire.nodes.values.filter(n =>
|
|
n.isStream && n.audio !== null && n.isSink === false)
|
|
|
|
PwObjectTracker {
|
|
objects: root.streams
|
|
}
|
|
|
|
signal volumeBumped()
|
|
signal micBumped()
|
|
|
|
onVolumePercentChanged: volumeBumped()
|
|
onMutedChanged: volumeBumped()
|
|
onMicPercentChanged: micBumped()
|
|
onMicMutedChanged: micBumped()
|
|
|
|
function setVolume(v: real) {
|
|
if (!sink?.audio) return;
|
|
sink.audio.volume = Math.max(0, Math.min(1.5, v));
|
|
}
|
|
|
|
function changeVolume(delta: real) {
|
|
setVolume(volume + delta);
|
|
}
|
|
|
|
function toggleMute() {
|
|
if (!sink?.audio) return;
|
|
sink.audio.muted = !sink.audio.muted;
|
|
}
|
|
|
|
function setMicVolume(v: real) {
|
|
if (!source?.audio) return;
|
|
source.audio.volume = Math.max(0, Math.min(1.5, v));
|
|
}
|
|
|
|
function toggleMicMute() {
|
|
if (!source?.audio) return;
|
|
source.audio.muted = !source.audio.muted;
|
|
}
|
|
|
|
function setSink(node: PwNode) {
|
|
Pipewire.preferredDefaultAudioSink = node;
|
|
}
|
|
|
|
function streamName(node: PwNode): string {
|
|
if (!node) return "";
|
|
return node.properties?.["application.name"]
|
|
|| node.properties?.["media.name"]
|
|
|| node.nickname
|
|
|| node.description
|
|
|| node.name;
|
|
}
|
|
|
|
// Nerd Font glyph for the current output level.
|
|
readonly property string icon: {
|
|
if (muted) return "";
|
|
if (volumePercent === 0) return "";
|
|
if (volumePercent < 34) return "";
|
|
if (volumePercent < 67) return "";
|
|
return "";
|
|
}
|
|
|
|
readonly property string micIcon: micMuted ? "" : ""
|
|
}
|