Files
noctalia-shell/Services/Media/AudioService.qml
T

212 lines
6.4 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
Singleton {
id: root
readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => {
if (!node.isStream) {
if (node.isSink) {
acc.sinks.push(node)
} else if (node.audio) {
acc.sources.push(node)
}
}
return acc
}, {
"sources": [],
"sinks": []
})
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode rawSource: Pipewire.defaultAudioSource
readonly property PwNode source: (rawSource && !rawSource.isSink && (!rawSource.mediaClass || rawSource.mediaClass.startsWith("Audio/Source"))) ? rawSource : null
readonly property bool hasInput: !!source
Component.onCompleted: updateInputVolume()
readonly property list<PwNode> sinks: nodes.sinks
readonly property list<PwNode> sources: nodes.sources
// Volume [0..1] is readonly from outside
readonly property alias volume: root._volume
property real _volume: sink?.audio?.volume ?? 0
readonly property alias muted: root._muted
property bool _muted: !!sink?.audio?.muted
// Input volume [0..1] is readonly from outside
readonly property alias inputVolume: root._inputVolume
property real _inputVolume: 0
readonly property alias inputMuted: root._inputMuted
property bool _inputMuted: !!source?.audio?.muted
readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0
function updateInputVolume() {
if (source && source.audio) {
var vol = source.audio.volume
if (vol !== undefined && !isNaN(vol)) {
root._inputVolume = vol
}
// Don't reset to 0 if volume is undefined/NaN - preserve last known value
root._inputMuted = !!source.audio.muted
} else {
// Don't reset volume to 0 when source is unavailable - preserve last known value
// Only reset muted state
root._inputMuted = true
}
}
// Update input volume when source property changes
onSourceChanged: {
updateInputVolume()
}
PwObjectTracker {
objects: [...root.sinks, ...root.sources]
}
Connections {
target: sink?.audio ? sink?.audio : null
function onVolumeChanged() {
var vol = (sink?.audio.volume ?? 0)
if (isNaN(vol)) {
return
}
// Only update if the value actually changed to prevent spurious signals
if (Math.abs(root._volume - vol) > 0.001) {
root._volume = vol
}
}
function onMutedChanged() {
var newMuted = (sink?.audio.muted ?? true)
// Only update if the value actually changed
if (root._muted !== newMuted) {
root._muted = newMuted
Logger.i("AudioService", "OnMuteChanged:", root._muted)
}
}
}
Connections {
target: source?.audio ? source?.audio : null
function onVolumeChanged() {
var vol = source?.audio?.volume
if (vol === undefined || isNaN(vol)) {
// Don't reset to 0 if volume is undefined/NaN - preserve last known value
return
}
// Only update if the value actually changed to prevent spurious signals
if (Math.abs(root._inputVolume - vol) > 0.001) {
root._inputVolume = vol
}
}
function onMutedChanged() {
var newMuted = (source?.audio.muted ?? true)
// Only update if the value actually changed
if (root._inputMuted !== newMuted) {
root._inputMuted = newMuted
}
}
}
Connections {
target: Pipewire
function onDefaultAudioSinkChanged() {}
function onDefaultAudioSourceChanged() {
updateInputVolume()
}
}
function increaseVolume() {
setVolume(volume + stepVolume)
}
function decreaseVolume() {
setVolume(volume - stepVolume)
}
function setVolume(newVolume: real) {
if (sink?.ready && sink?.audio) {
// Clamp it accordingly
sink.audio.muted = false
sink.audio.volume = Math.max(0, Math.min(Settings.data.audio.volumeOverdrive ? 1.5 : 1.0, newVolume))
//Logger.i("AudioService", "SetVolume", sink.audio.volume);
} else {
Logger.w("AudioService", "No sink available")
}
}
function setOutputMuted(muted: bool) {
if (sink?.ready && sink?.audio) {
sink.audio.muted = muted
} else {
Logger.w("AudioService", "No sink available")
}
}
function increaseInputVolume() {
setInputVolume(inputVolume + stepVolume)
}
function decreaseInputVolume() {
setInputVolume(inputVolume - stepVolume)
}
function setInputVolume(newVolume: real) {
if (source?.ready && source?.audio) {
// Clamp it accordingly
source.audio.muted = false
source.audio.volume = Math.max(0, Math.min(Settings.data.audio.volumeOverdrive ? 1.5 : 1.0, newVolume))
} else {
Logger.w("AudioService", "No source available")
}
}
function setInputMuted(muted: bool) {
if (source?.ready && source?.audio) {
source.audio.muted = muted
} else {
Logger.w("AudioService", "No source available")
}
}
function setAudioSink(newSink: PwNode): void {
Pipewire.preferredDefaultAudioSink = newSink
// Volume is changed by the sink change
root._volume = newSink?.audio?.volume ?? 0
root._muted = !!newSink?.audio?.muted
}
function setAudioSource(newSource: PwNode): void {
Pipewire.preferredDefaultAudioSource = newSource
// The source property will update automatically, which triggers onSourceChanged
// which calls updateInputVolume()
}
function getOutputIcon() {
if (muted) {
return "volume-mute"
}
return (volume <= Number.EPSILON) ? "volume-zero" : (volume <= 0.5) ? "volume-low" : "volume-high"
}
function getInputIcon() {
if (inputMuted) {
return "microphone-mute"
}
return (inputVolume <= Number.EPSILON) ? "microphone-mute" : "microphone"
}
}