mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-06-02 02:26:00 +00:00
AudioService: possible fix?
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+257
-129
@@ -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<PwNode> sinks: deviceNodes.sinks
|
||||
readonly property list<PwNode> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user