From e2854f207918a550002bdd6751ab5797c4fc5c71 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 26 Nov 2025 20:08:47 +0100 Subject: [PATCH] AudioService: possible fix? --- Modules/Panels/Settings/Tabs/AudioTab.qml | 14 +- Services/Media/AudioService.qml | 386 ++++++++++++++-------- 2 files changed, 270 insertions(+), 130 deletions(-) diff --git a/Modules/Panels/Settings/Tabs/AudioTab.qml b/Modules/Panels/Settings/Tabs/AudioTab.qml index 1b7bcb9b..e05dd68d 100644 --- a/Modules/Panels/Settings/Tabs/AudioTab.qml +++ b/Modules/Panels/Settings/Tabs/AudioTab.qml @@ -12,6 +12,17 @@ ColumnLayout { property real localVolume: AudioService.volume + Connections { + target: AudioService + function onSinkChanged() { + // Immediately update local volume when device changes to prevent old value from being applied + localVolume = AudioService.volume; + } + function onVolumeChanged() { + localVolume = AudioService.volume; + } + } + Connections { target: AudioService.sink?.audio ? AudioService.sink?.audio : null function onVolumeChanged() { @@ -42,7 +53,8 @@ ColumnLayout { running: true repeat: true onTriggered: { - if (Math.abs(localVolume - AudioService.volume) >= 0.01) { + // Don't set volume if device is switching - wait for new device's volume to be read + if (!AudioService.isSwitchingSink && Math.abs(localVolume - AudioService.volume) >= 0.01) { AudioService.setVolume(localVolume); } } diff --git a/Services/Media/AudioService.qml b/Services/Media/AudioService.qml index efd7a287..0c88265c 100644 --- a/Services/Media/AudioService.qml +++ b/Services/Media/AudioService.qml @@ -10,27 +10,39 @@ Singleton { // Devices - readonly property PwNode sink: Pipewire.defaultAudioSink + readonly property PwNode sink: Pipewire.ready ? Pipewire.defaultAudioSink : null readonly property PwNode source: validatedSource readonly property bool hasInput: !!source readonly property list sinks: deviceNodes.sinks readonly property list sources: deviceNodes.sources - // Output Volume + // Output Volume - read directly from device (like friend's version) + // Make it a regular property that updates when sink or volume changes + property real volume: { + if (!sink?.audio) return 0; + const vol = sink.audio.volume; + if (vol === undefined || isNaN(vol)) return 0; + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + return Math.max(0, Math.min(maxVolume, vol)); + } + readonly property bool muted: sink?.audio?.muted ?? true - readonly property real volume: volumeValue - readonly property bool muted: mutedValue - - // Input Volume - - readonly property real inputVolume: inputVolumeValue - readonly property bool inputMuted: inputMutedValue + // Input Volume - read directly from device (like friend's version) + // Make it a regular property that updates when source or volume changes + property real inputVolume: { + if (!source?.audio) return 0; + const vol = source.audio.volume; + if (vol === undefined || isNaN(vol)) return 0; + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + return Math.max(0, Math.min(maxVolume, vol)); + } + readonly property bool inputMuted: source?.audio?.muted ?? true readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0 // Filtered device nodes (non-stream sinks and sources) - readonly property var deviceNodes: Pipewire.nodes.values.reduce((acc, node) => { + readonly property var deviceNodes: Pipewire.ready ? Pipewire.nodes.values.reduce((acc, node) => { if (!node.isStream) { if (node.isSink) { acc.sinks.push(node); @@ -42,129 +54,206 @@ Singleton { }, { "sources": [], "sinks": [] - }) + }) : { "sources": [], "sinks": [] } // Validated source (ensures it's a proper audio source, not a sink) readonly property PwNode validatedSource: { + if (!Pipewire.ready) return null; const raw = Pipewire.defaultAudioSource; - if (!raw || raw.isSink) - return null; - if (raw.mediaClass && !raw.mediaClass.startsWith("Audio/Source")) - return null; + if (!raw || raw.isSink || !raw.audio) + return null; + // Optional: check type if available (type reflects media.class per docs) + if (raw.type && typeof raw.type === "string" && !raw.type.startsWith("Audio/Source")) + return null; return raw; } - // Internal state - property real volumeValue: 0 - property bool mutedValue: true - property real inputVolumeValue: 0 - property bool inputMutedValue: true - property bool isClampingOutput: false - property bool isClampingInput: false - - // Initialization - - Component.onCompleted: { - updateOutputVolume(); - updateInputVolume(); + // Internal state for feedback loop prevention + property bool isSettingOutputVolume: false + property bool isSettingInputVolume: false + + // Track device changes to prevent old volume from being applied + property int currentSinkId: -1 + property int currentSourceId: -1 + property bool blockOutputVolumeChanges: false + property bool blockInputVolumeChanges: false + + Timer { + id: unblockOutputTimer + interval: 1500 + onTriggered: { + blockOutputVolumeChanges = false; + } + } + + Timer { + id: unblockInputTimer + interval: 1500 + onTriggered: { + blockInputVolumeChanges = false; + } } - // Watchers + // Bind default sink and source to ensure their properties are available + PwObjectTracker { + id: sinkTracker + objects: root.sink ? [root.sink] : [] + } - onSinkChanged: updateOutputVolume() - onSourceChanged: updateInputVolume() + PwObjectTracker { + id: sourceTracker + objects: root.source ? [root.source] : [] + } // Bind all devices to ensure their properties are available PwObjectTracker { objects: [...root.sinks, ...root.sources] } - // Watch output device changes + // Track sink changes and force volume property to update + onSinkChanged: { + if (sink) { + const newSinkId = sink.id; + if (newSinkId !== currentSinkId) { + currentSinkId = newSinkId; + // Block volume changes for 1.5 seconds after device change + blockOutputVolumeChanges = true; + unblockOutputTimer.restart(); + // Force volume property to update by reading new device's volume + Qt.callLater(() => { + if (root.sink?.audio) { + const vol = root.sink.audio.volume; + if (vol !== undefined && !isNaN(vol)) { + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const clampedVol = Math.max(0, Math.min(maxVolume, vol)); + root.volume = clampedVol; // This will trigger volumeChanged signal + } + } + }); + } + } else { + currentSinkId = -1; + blockOutputVolumeChanges = true; + unblockOutputTimer.restart(); + root.volume = 0; // This will trigger volumeChanged signal + } + } + + // Watch output device changes for clamping and updating volume property Connections { target: sink?.audio ?? null function onVolumeChanged() { - const vol = sink?.audio?.volume ?? 0; - if (isNaN(vol)) + // Ignore volume changes if we're the one setting it (to prevent feedback loop) + if (root.isSettingOutputVolume) { return; - - // Clamp volume if it exceeds max when volumeOverdrive is disabled - if (!root.isClampingOutput) { - const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; - if (vol > maxVolume) { - root.isClampingOutput = true; - Qt.callLater(() => { - if (root.sink?.audio) { - root.sink.audio.volume = maxVolume; - } - root.isClampingOutput = false; - }); - return; - } } - if (Math.abs(root.volumeValue - vol) > 0.001) { - root.volumeValue = vol; + if (!root.sink?.audio) { + return; } - } - function onMutedChanged() { - const newMuted = sink?.audio?.muted ?? true; - if (root.mutedValue !== newMuted) { - root.mutedValue = newMuted; + const vol = root.sink.audio.volume; + if (vol === undefined || isNaN(vol)) { + return; + } + + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const clampedVol = Math.max(0, Math.min(maxVolume, vol)); + + // Update volume property to trigger change signal + if (Math.abs(root.volume - clampedVol) > 0.001) { + root.volume = clampedVol; + } + + // If volume exceeds max, clamp it (but only if we didn't just set it) + if (vol > maxVolume) { + root.isSettingOutputVolume = true; + Qt.callLater(() => { + if (root.sink?.audio && root.sink.audio.volume > maxVolume) { + root.sink.audio.volume = maxVolume; + } + root.isSettingOutputVolume = false; + }); } } } - // Watch input device changes + // Track source changes and force volume property to update + onSourceChanged: { + if (source) { + const newSourceId = source.id; + if (newSourceId !== currentSourceId) { + currentSourceId = newSourceId; + // Block volume changes for 1.5 seconds after device change + blockInputVolumeChanges = true; + unblockInputTimer.restart(); + // Force volume property to update by reading new device's volume + Qt.callLater(() => { + if (root.source?.audio) { + const vol = root.source.audio.volume; + if (vol !== undefined && !isNaN(vol)) { + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const clampedVol = Math.max(0, Math.min(maxVolume, vol)); + root.inputVolume = clampedVol; // This will trigger inputVolumeChanged signal + } + } + }); + } + } else { + currentSourceId = -1; + blockInputVolumeChanges = true; + unblockInputTimer.restart(); + root.inputVolume = 0; // This will trigger inputVolumeChanged signal + } + } + + // Watch input device changes for clamping and updating volume property Connections { target: source?.audio ?? null function onVolumeChanged() { - const vol = source?.audio?.volume; - if (vol === undefined || isNaN(vol)) + // Ignore volume changes if we're the one setting it (to prevent feedback loop) + if (root.isSettingInputVolume) { return; - - // Clamp volume if it exceeds max when volumeOverdrive is disabled - if (!root.isClampingInput) { - const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; - if (vol > maxVolume) { - root.isClampingInput = true; - Qt.callLater(() => { - if (root.source?.audio) { - root.source.audio.volume = maxVolume; - } - root.isClampingInput = false; - }); - return; - } } - if (Math.abs(root.inputVolumeValue - vol) > 0.001) { - root.inputVolumeValue = vol; + if (!root.source?.audio) { + return; } - } - function onMutedChanged() { - const newMuted = source?.audio?.muted ?? true; - if (root.inputMutedValue !== newMuted) { - root.inputMutedValue = newMuted; + const vol = root.source.audio.volume; + if (vol === undefined || isNaN(vol)) { + return; } - } - } - // Watch for default device changes - Connections { - target: Pipewire - - function onDefaultAudioSourceChanged() { - updateInputVolume(); + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const clampedVol = Math.max(0, Math.min(maxVolume, vol)); + + // Update inputVolume property to trigger change signal + if (Math.abs(root.inputVolume - clampedVol) > 0.001) { + root.inputVolume = clampedVol; + } + + // If volume exceeds max, clamp it (but only if we didn't just set it) + if (vol > maxVolume) { + root.isSettingInputVolume = true; + Qt.callLater(() => { + if (root.source?.audio && root.source.audio.volume > maxVolume) { + root.source.audio.volume = maxVolume; + } + root.isSettingInputVolume = false; + }); + } } } // Output Control function increaseVolume() { + if (!Pipewire.ready || !sink?.audio) { + return; + } const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; if (volume >= maxVolume) { return; @@ -173,6 +262,9 @@ Singleton { } function decreaseVolume() { + if (!Pipewire.ready || !sink?.audio) { + return; + } if (volume <= 0) { return; } @@ -180,19 +272,45 @@ Singleton { } function setVolume(newVolume: real) { - if (!sink?.audio) { - Logger.w("AudioService", "No sink available"); + if (!Pipewire.ready || !sink?.ready || !sink?.audio) { + Logger.w("AudioService", "No sink available or not ready"); return; } + // Block volume changes for 1.5 seconds after device change to prevent old values + if (blockOutputVolumeChanges) { + const currentVol = sink.audio.volume; + if (currentVol !== undefined && !isNaN(currentVol)) { + const diff = Math.abs(newVolume - currentVol); + // Only block if difference is significant (>5%) - allow small adjustments + if (diff > 0.05) { + Logger.w("AudioService", "Ignoring setVolume - device changed recently, volume difference:", diff, "requested:", newVolume, "current:", currentVol); + return; + } + } else { + // If we can't read current volume yet, block all changes + Logger.w("AudioService", "Ignoring setVolume - device changed recently and volume not ready yet"); + return; + } + } + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const clampedVolume = Math.max(0, Math.min(maxVolume, newVolume)); + + // Set flag to prevent feedback loop, then set the actual volume + isSettingOutputVolume = true; sink.audio.muted = false; - sink.audio.volume = Math.max(0, Math.min(maxVolume, newVolume)); + sink.audio.volume = clampedVolume; + + // Clear flag after a short delay to allow external changes to be detected + Qt.callLater(() => { + isSettingOutputVolume = false; + }); } function setOutputMuted(muted: bool) { - if (!sink?.audio) { - Logger.w("AudioService", "No sink available"); + if (!Pipewire.ready || !sink?.audio) { + Logger.w("AudioService", "No sink available or Pipewire not ready"); return; } @@ -217,31 +335,63 @@ Singleton { // Input Control function increaseInputVolume() { + if (!Pipewire.ready || !source?.audio) { + return; + } const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; if (inputVolume >= maxVolume) { return; } - setInputVolume(inputVolume + stepVolume); + setInputVolume(Math.min(maxVolume, inputVolume + stepVolume)); } function decreaseInputVolume() { - setInputVolume(inputVolume - stepVolume); + if (!Pipewire.ready || !source?.audio) { + return; + } + setInputVolume(Math.max(0, inputVolume - stepVolume)); } function setInputVolume(newVolume: real) { - if (!source?.audio) { - Logger.w("AudioService", "No source available"); + if (!Pipewire.ready || !source?.ready || !source?.audio) { + Logger.w("AudioService", "No source available or not ready"); return; } + // Block volume changes for 1.5 seconds after device change to prevent old values + if (blockInputVolumeChanges) { + const currentVol = source.audio.volume; + if (currentVol !== undefined && !isNaN(currentVol)) { + const diff = Math.abs(newVolume - currentVol); + // Only block if difference is significant (>5%) - allow small adjustments + if (diff > 0.05) { + Logger.w("AudioService", "Ignoring setInputVolume - device changed recently, volume difference:", diff, "requested:", newVolume, "current:", currentVol); + return; + } + } else { + // If we can't read current volume yet, block all changes + Logger.w("AudioService", "Ignoring setInputVolume - device changed recently and volume not ready yet"); + return; + } + } + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const clampedVolume = Math.max(0, Math.min(maxVolume, newVolume)); + + // Set flag to prevent feedback loop, then set the actual volume + isSettingInputVolume = true; source.audio.muted = false; - source.audio.volume = Math.max(0, Math.min(maxVolume, newVolume)); + source.audio.volume = clampedVolume; + + // Clear flag after a short delay to allow external changes to be detected + Qt.callLater(() => { + isSettingInputVolume = false; + }); } function setInputMuted(muted: bool) { - if (!source?.audio) { - Logger.w("AudioService", "No source available"); + if (!Pipewire.ready || !source?.audio) { + Logger.w("AudioService", "No source available or Pipewire not ready"); return; } @@ -258,40 +408,18 @@ Singleton { // Device Selection function setAudioSink(newSink: PwNode): void { + if (!Pipewire.ready) { + Logger.w("AudioService", "Pipewire not ready"); + return; + } Pipewire.preferredDefaultAudioSink = newSink; - // Values will update via onSinkChanged -> updateOutputVolume() } function setAudioSource(newSource: PwNode): void { + if (!Pipewire.ready) { + Logger.w("AudioService", "Pipewire not ready"); + return; + } Pipewire.preferredDefaultAudioSource = newSource; - // Values will update via onSourceChanged -> updateInputVolume() - } - - // Internal - - function updateOutputVolume() { - if (sink?.audio) { - const vol = sink.audio.volume; - if (vol !== undefined && !isNaN(vol)) { - volumeValue = vol; - } - mutedValue = !!sink.audio.muted; - } else { - mutedValue = true; - } - } - - function updateInputVolume() { - if (source?.audio) { - const vol = source.audio.volume; - if (vol !== undefined && !isNaN(vol)) { - inputVolumeValue = vol; - } - // Preserve last known volume if undefined/NaN - inputMutedValue = !!source.audio.muted; - } else { - // Only reset muted state when no source - inputMutedValue = true; - } } }