audio: add mono audio toggle with support detection (#3539)

This commit is contained in:
dion
2026-09-29 16:38:22 -04:00
committed by GitHub
parent f482642880
commit 13034cb85c
5 changed files with 169 additions and 0 deletions
+1
View File
@@ -258,6 +258,7 @@ Singleton {
property string calendarBackend: Spec.SPEC.calendarBackend.def
property string defaultTaskCalendarId: Spec.SPEC.defaultTaskCalendarId.def
property bool audioShowStreamDevices: Spec.SPEC.audioShowStreamDevices.def
property bool audioMono: Spec.SPEC.audioMono.def
property string clockFormat: Spec.SPEC.clockFormat.def
readonly property bool localeUses24Hour: {
const fmt = Qt.locale().timeFormat(Locale.ShortFormat).replace(/'[^']*'/g, "");
@@ -339,6 +339,9 @@ var LOCAL_SPEC = {
audioShowStreamDevices: {
def: false
},
audioMono: {
def: false
},
windSpeedUnit: {
def: "kmh"
},
+28
View File
@@ -8,6 +8,8 @@ import qs.Modules.Settings.Widgets
Item {
id: root
readonly property var log: Log.scoped("AudioTab")
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
@@ -119,6 +121,32 @@ Item {
SettingsPage {
id: mainColumn
SettingsCard {
tab: "audio"
tags: ["audio", "accessibility", "mono"]
title: I18n.tr("Mono Audio", "Audio settings: mono audio toggle")
settingKey: "audioMono"
iconName: "a11y"
SettingsToggleRow {
tab: "audio"
tags: ["audio", "mono"]
settingKey: "audioMono"
text: I18n.tr("Mono Audio", "Audio settings: mono audio toggle")
description: I18n.tr("Mix all stereo content into a single channel", "Audio settings mono description")
enabled: AudioService.monoSettingSupported
checked: SettingsData.audioMono
onToggled: checked => {
SettingsData.set("audioMono", checked);
AudioService.setMonoSetting(checked, (ok, message) => {
if (!ok) {
SettingsData.set("audioMono", !checked);
}
});
}
}
}
SettingsCard {
tab: "audio"
tags: ["audio", "device", "output", "speaker"]
+112
View File
@@ -62,6 +62,9 @@ Singleton {
readonly property int sourceVolumePercent: Math.round((source?.audio?.volume ?? 0) * 100)
readonly property int wheelVolumeStep: SettingsData.audioWheelScrollAmount
property bool monoSettingSupported: false
property string monoSettingBackend: ""
signal micMuteChanged
signal micVolumeChanged
signal audioOutputCycled(string deviceName, string deviceIcon)
@@ -134,6 +137,15 @@ Singleton {
}
}
Connections {
target: SettingsData
function onAudioMonoChanged() {
if (root.monoSettingSupported) {
root.setMonoSetting(SettingsData.audioMono);
}
}
}
function setSink(node: PwNode): bool {
if (!node)
return false;
@@ -1276,6 +1288,78 @@ EOFCONFIG
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted";
}
// Mono audio toggle
property bool _monoQueryInProgress: false
function queryMonoSetting(callback) {
if (root._monoQueryInProgress) {
return;
}
root._monoQueryInProgress = true;
// Try pactl first (PipeWire ≥1.4.10 /1.6.0)
Proc.runCommand("audio-query-mono", ["env", "LC_ALL=C", "pactl", "send-message", "/core", "pipewire-pulse:force-mono-output", ""], (output, exitCode) => {
if (exitCode === 0 && output) {
const trimmed = output.trim();
if (trimmed === "true" || trimmed === "false") {
root.monoSettingSupported = true;
root.monoSettingBackend = "pactl";
root._monoQueryInProgress = false;
callback(true, trimmed === "true");
return;
}
}
// Fallback to wpctl settings
Proc.runCommand("audio-query-mono-wpctl", ["env", "LC_ALL=C", "wpctl", "settings", "node.features.audio.mono"], (output2, exitCode2) => {
if (exitCode2 === 0 && output2) {
const m = output2.match(/\b(true|false)\b/);
if (m) {
root.monoSettingSupported = true;
root.monoSettingBackend = "wpctl";
root._monoQueryInProgress = false;
callback(true, m[1] === "true");
return;
}
}
root.monoSettingSupported = false;
root.monoSettingBackend = "";
root._monoQueryInProgress = false;
callback(false, false);
});
}, 0);
}
// Re-apply the saved preference: the live PipeWire value is runtime-only and resets on restart
function applyMonoStartupPreference() {
if (!SettingsData._hasLoaded)
return;
queryMonoSetting((supported, currentValue) => {
if (supported && currentValue !== SettingsData.audioMono)
root.setMonoSetting(SettingsData.audioMono);
});
}
function setMonoSetting(enabled, callback) {
const boolStr = enabled ? "true" : "false";
const backend = root.monoSettingBackend;
if (backend === "pactl") {
Proc.runCommand("audio-set-mono", ["env", "LC_ALL=C", "pactl", "send-message", "/core", "pipewire-pulse:force-mono-output", boolStr], (output, exitCode) => {
const ok = exitCode === 0;
if (callback) {
callback(ok, ok ? "Mono audio enabled" : (output || "Failed to set mono audio"));
}
}, 0);
} else if (backend === "wpctl") {
Proc.runCommand("audio-set-mono-wpctl", ["env", "LC_ALL=C", "wpctl", "settings", "node.features.audio.mono", boolStr], (output, exitCode) => {
const ok = exitCode === 0;
if (callback) {
callback(ok, ok ? "Mono audio enabled" : (output || "Failed to set mono audio"));
}
}, 0);
} else if (callback) {
callback(false, "Mono audio not supported");
}
}
function setMicVolume(percentage) {
if (!root.source?.audio) {
return "No audio source available";
@@ -1363,6 +1447,27 @@ EOFCONFIG
return root.toggleMicMute();
}
function setmono(status: string): string {
const enabled = status === "true";
if (!root.monoSettingSupported) {
return "Mono audio not supported";
}
// Async: persist setting only on success
root.setMonoSetting(enabled, (ok, message) => {
if (ok) {
SettingsData.set("audioMono", enabled);
}
});
return "Mono audio toggle sent";
}
function getmono(): string {
if (!root.monoSettingSupported) {
return "Mono audio not supported";
}
return `Mono Audio: ${root.monoSettingBackend} ${SettingsData.audioMono ? "enabled" : "disabled"}`;
}
function status(): string {
let result = "Audio Status:\n";
@@ -1436,10 +1541,17 @@ EOFCONFIG
checkSoundThemeSupport();
}
readonly property bool settingsLoaded: SettingsData._hasLoaded
onSettingsLoadedChanged: {
if (settingsLoaded)
root.applyMonoStartupPreference();
}
Component.onCompleted: {
rebuildTypedNodeLists();
loadDeviceAliases();
if (SettingsData.soundsEnabled && SettingsData.useSystemSoundTheme)
getCurrentSoundTheme();
root.applyMonoStartupPreference();
}
}
@@ -11284,6 +11284,31 @@
"parentLabel": "Sound & media",
"icon": "mic"
},
{
"section": "audioMono",
"label": "Mono Audio",
"tabIndex": 29,
"category": "Audio",
"keywords": [
"accessibility",
"audio",
"channel",
"content",
"headphones",
"media",
"microphone",
"mono",
"pipewire",
"single",
"sound",
"speaker",
"stereo",
"volume"
],
"parentLabel": "Sound & media",
"icon": "a11y",
"description": "Mix all stereo content into a single channel"
},
{
"section": "audioOutputDevices",
"label": "Output devices",