pragma Singleton pragma ComponentBehavior: Bound import QtCore import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import qs.Common Singleton { id: root readonly property PwNode sink: Pipewire.defaultAudioSink readonly property PwNode source: Pipewire.defaultAudioSource property bool soundsAvailable: false property bool gsettingsAvailable: false property var availableSoundThemes: [] property string currentSoundTheme: "" property var soundFilePaths: ({}) property var volumeChangeSound: null property var powerPlugSound: null property var powerUnplugSound: null property var normalNotificationSound: null property var criticalNotificationSound: null property real notificationsVolume: 1.0 property bool notificationsAudioMuted: false property var mediaDevices: null property var mediaDevicesConnections: null property var deviceAliases: ({}) property string wireplumberConfigPath: { const homeUrl = StandardPaths.writableLocation(StandardPaths.HomeLocation); const homePath = homeUrl.toString().replace("file://", ""); return homePath + "/.config/wireplumber/wireplumber.conf.d/51-dms-audio-aliases.conf"; } property bool wireplumberReloading: false readonly property int sinkMaxVolume: { const name = sink?.name ?? ""; if (!name) return 100; return SessionData.deviceMaxVolumes[name] ?? 100; } signal micMuteChanged signal audioOutputCycled(string deviceName) signal deviceAliasChanged(string nodeName, string newAlias) signal wireplumberReloadStarted signal wireplumberReloadCompleted(bool success) function getMaxVolumePercent(node) { if (!node?.name) return 100; return SessionData.deviceMaxVolumes[node.name] ?? 100; } Connections { target: SessionData function onDeviceMaxVolumesChanged() { if (!root.sink?.audio) return; const maxVol = root.sinkMaxVolume; const currentPercent = Math.round(root.sink.audio.volume * 100); if (currentPercent > maxVol) root.sink.audio.volume = maxVol / 100; } } function getAvailableSinks() { return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream); } function cycleAudioOutput() { const sinks = getAvailableSinks(); if (sinks.length < 2) return null; const currentSink = root.sink; let currentIndex = -1; for (let i = 0; i < sinks.length; i++) { if (sinks[i] === currentSink) { currentIndex = i; break; } } const nextIndex = (currentIndex + 1) % sinks.length; const nextSink = sinks[nextIndex]; Pipewire.preferredDefaultAudioSink = nextSink; const name = displayName(nextSink); audioOutputCycled(name); return name; } function getDeviceAlias(nodeName) { if (!nodeName) return null; return deviceAliases[nodeName] || null; } function hasDeviceAlias(nodeName) { if (!nodeName) return false; return deviceAliases.hasOwnProperty(nodeName) && deviceAliases[nodeName] !== null && deviceAliases[nodeName] !== ""; } function setDeviceAlias(nodeName, customAlias) { if (!nodeName) { console.error("AudioService: Cannot set alias - nodeName is empty"); return false; } if (!customAlias || customAlias.trim() === "") { return removeDeviceAlias(nodeName); } const trimmedAlias = customAlias.trim(); const updated = Object.assign({}, deviceAliases); updated[nodeName] = trimmedAlias; deviceAliases = updated; writeWireplumberConfig(); deviceAliasChanged(nodeName, trimmedAlias); return true; } function removeDeviceAlias(nodeName) { if (!nodeName) return false; if (!hasDeviceAlias(nodeName)) return false; const updated = Object.assign({}, deviceAliases); delete updated[nodeName]; deviceAliases = updated; writeWireplumberConfig(); deviceAliasChanged(nodeName, ""); return true; } function writeWireplumberConfig() { const homeUrl = StandardPaths.writableLocation(StandardPaths.HomeLocation); const homePath = homeUrl.toString().replace("file://", ""); const configDir = homePath + "/.config/wireplumber/wireplumber.conf.d"; const configContent = generateWireplumberConfig(); const shellCmd = `mkdir -p "${configDir}" && cat > "${wireplumberConfigPath}" << 'EOFCONFIG' ${configContent} EOFCONFIG `; Proc.runCommand("writeWireplumberConfig", ["sh", "-c", shellCmd], (output, exitCode) => { if (exitCode !== 0) { console.error("AudioService: Failed to write WirePlumber config. Exit code:", exitCode); console.error("AudioService: Error output:", output); ToastService.showError(I18n.tr("Failed to save audio config"), output || ""); return; } reloadWireplumberConfig(); }, 0); } function generateWireplumberConfig() { let config = "# Generated by DankMaterialShell - Audio Device Aliases\n"; config += "# Do not edit manually - changes will be overwritten\n"; config += "# Last updated: " + new Date().toISOString() + "\n\n"; const aliasKeys = Object.keys(deviceAliases); if (aliasKeys.length === 0) { config += "# No device aliases configured\n"; return config; } const alsaAliases = []; const bluezAliases = []; const otherAliases = []; for (const nodeName of aliasKeys) { const alias = deviceAliases[nodeName]; if (!alias) continue; const rule = { nodeName: nodeName, alias: alias }; if (nodeName.includes("alsa")) { alsaAliases.push(rule); } else if (nodeName.includes("bluez")) { bluezAliases.push(rule); } else { otherAliases.push(rule); } } if (alsaAliases.length > 0) { config += "monitor.alsa.rules = [\n"; for (let i = 0; i < alsaAliases.length; i++) { const rule = alsaAliases[i]; config += " {\n"; config += ` matches = [ { "node.name" = "${rule.nodeName}" } ]\n`; config += ` actions = { update-props = { "node.description" = "${rule.alias}" } }\n`; config += " }"; if (i < alsaAliases.length - 1) config += ","; config += "\n"; } config += "]\n\n"; } if (bluezAliases.length > 0) { config += "monitor.bluez.rules = [\n"; for (let i = 0; i < bluezAliases.length; i++) { const rule = bluezAliases[i]; config += " {\n"; config += ` matches = [ { "node.name" = "${rule.nodeName}" } ]\n`; config += ` actions = { update-props = { "node.description" = "${rule.alias}" } }\n`; config += " }"; if (i < bluezAliases.length - 1) config += ","; config += "\n"; } config += "]\n\n"; } if (otherAliases.length > 0) { config += "# Other device aliases (RAOP, USB, and other devices)\n"; config += "wireplumber.rules = [\n"; for (let i = 0; i < otherAliases.length; i++) { const rule = otherAliases[i]; config += " {\n"; config += ` matches = [\n`; config += ` { "node.name" = "${rule.nodeName}" }\n`; config += ` ]\n`; config += ` actions = {\n`; config += ` update-props = {\n`; config += ` "node.description" = "${rule.alias}"\n`; config += ` "node.nick" = "${rule.alias}"\n`; config += ` "device.description" = "${rule.alias}"\n`; config += ` }\n`; config += ` }\n`; config += " }"; if (i < otherAliases.length - 1) config += ","; config += "\n"; } config += "]\n"; } return config; } function reloadWireplumberConfig() { if (wireplumberReloading) { return; } wireplumberReloading = true; wireplumberReloadStarted(); Proc.runCommand("restartWireplumber", ["systemctl", "--user", "restart", "wireplumber"], (output, exitCode) => { wireplumberReloading = false; if (exitCode === 0) { ToastService.showInfo(I18n.tr("Audio system restarted"), I18n.tr("Device names updated")); wireplumberReloadCompleted(true); } else { console.error("AudioService: Failed to restart WirePlumber:", output); ToastService.showError(I18n.tr("Failed to restart audio system"), output); wireplumberReloadCompleted(false); } }, 5000); } function loadDeviceAliases() { const homeUrl = StandardPaths.writableLocation(StandardPaths.HomeLocation); const homePath = homeUrl.toString().replace("file://", ""); const configPath = homePath + "/.config/wireplumber/wireplumber.conf.d/51-dms-audio-aliases.conf"; Proc.runCommand("readWireplumberConfig", ["cat", configPath], (output, exitCode) => { if (exitCode !== 0) { console.log("AudioService: No existing WirePlumber config found"); return; } const aliases = {}; const lines = output.split('\n'); let currentNodeName = null; for (const line of lines) { const nodeNameMatch = line.match(/"node\.name"\s*=\s*"([^"]+)"/); if (nodeNameMatch) { currentNodeName = nodeNameMatch[1]; } const descriptionMatch = line.match(/"node\.description"\s*=\s*"([^"]+)"/); if (descriptionMatch && currentNodeName) { aliases[currentNodeName] = descriptionMatch[1]; currentNodeName = null; } } if (Object.keys(aliases).length > 0) { deviceAliases = aliases; console.log("AudioService: Loaded", Object.keys(aliases).length, "device aliases"); } }, 0); } Connections { target: root.sink?.audio ?? null function onVolumeChanged() { if (SessionData.suppressOSD) return; root.playVolumeChangeSoundIfEnabled(); } } function detectSoundsAvailability() { try { const testObj = Qt.createQmlObject(` import QtQuick import QtMultimedia Item {} `, root, "AudioService.TestComponent"); if (testObj) { testObj.destroy(); } soundsAvailable = true; return true; } catch (e) { soundsAvailable = false; return false; } } function checkGsettings() { Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => { gsettingsAvailable = (exitCode === 0); if (gsettingsAvailable) { scanSoundThemes(); getCurrentSoundTheme(); } }, 0); } function scanSoundThemes() { const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS"); const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== "" ? xdgDataDirs.split(":").concat(Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))) : ["/usr/share", "/usr/local/share", Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))]; const basePaths = searchPaths.map(p => p + "/sounds").join(" "); const script = ` for base_dir in ${basePaths}; do [ -d "$base_dir" ] || continue for theme_dir in "$base_dir"/*; do [ -d "$theme_dir/stereo" ] || continue basename "$theme_dir" done done | sort -u `; Proc.runCommand("scanSoundThemes", ["sh", "-c", script], (output, exitCode) => { if (exitCode === 0 && output.trim()) { const themes = output.trim().split('\n').filter(t => t && t.length > 0); availableSoundThemes = themes; } else { availableSoundThemes = []; } }, 0); } function getCurrentSoundTheme() { Proc.runCommand("getCurrentSoundTheme", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null | sed \"s/'//g\""], (output, exitCode) => { if (exitCode === 0 && output.trim()) { currentSoundTheme = output.trim(); console.log("AudioService: Current system sound theme:", currentSoundTheme); if (SettingsData.useSystemSoundTheme) { discoverSoundFiles(currentSoundTheme); } } else { currentSoundTheme = ""; console.log("AudioService: No system sound theme found"); } }, 0); } function setSoundTheme(themeName) { if (!themeName || themeName === currentSoundTheme) { return; } Proc.runCommand("setSoundTheme", ["sh", "-c", `gsettings set org.gnome.desktop.sound theme-name '${themeName}'`], (output, exitCode) => { if (exitCode === 0) { currentSoundTheme = themeName; if (SettingsData.useSystemSoundTheme) { discoverSoundFiles(themeName); } } }, 0); } function discoverSoundFiles(themeName) { if (!themeName) { soundFilePaths = {}; if (soundsAvailable) { destroySoundPlayers(); createSoundPlayers(); } return; } const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS"); const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== "" ? xdgDataDirs.split(":").concat(Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))) : ["/usr/share", "/usr/local/share", Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation))]; const extensions = ["oga", "ogg", "wav", "mp3", "flac"]; const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName; const script = ` for event_key in audio-volume-change power-plug power-unplug message message-new-instant; do found=0 case "$event_key" in message) names="dialog-information message message-lowpriority bell" ;; message-new-instant) names="dialog-warning message-new-instant message-highlight" ;; *) names="$event_key" ;; esac for theme in ${themesToSearch}; do for event_name in $names; do for base_path in ${searchPaths.join(" ")}; do sounds_path="$base_path/sounds" for ext in ${extensions.join(" ")}; do file_path="$sounds_path/$theme/stereo/$event_name.$ext" if [ -f "$file_path" ]; then echo "$event_key=$file_path" found=1 break fi done [ $found -eq 1 ] && break done [ $found -eq 1 ] && break done [ $found -eq 1 ] && break done done `; Proc.runCommand("discoverSoundFiles", ["sh", "-c", script], (output, exitCode) => { const paths = {}; if (exitCode === 0 && output.trim()) { const lines = output.trim().split('\n'); for (let line of lines) { const parts = line.split('='); if (parts.length === 2) { paths[parts[0]] = "file://" + parts[1]; } } } soundFilePaths = paths; if (soundsAvailable) { destroySoundPlayers(); createSoundPlayers(); } }, 0); } function getSoundPath(soundEvent) { const soundMap = { "audio-volume-change": "../assets/sounds/freedesktop/audio-volume-change.wav", "power-plug": "../assets/sounds/plasma/power-plug.wav", "power-unplug": "../assets/sounds/plasma/power-unplug.wav", "message": "../assets/sounds/freedesktop/message.wav", "message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav" }; const specialConditions = { "smooth": ["audio-volume-change"] }; const themeLower = currentSoundTheme.toLowerCase(); if (SettingsData.useSystemSoundTheme && specialConditions[themeLower]?.includes(soundEvent)) { const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav"); console.log("AudioService: Using bundled sound (special condition) for", soundEvent, ":", bundledPath); return bundledPath; } if (SettingsData.useSystemSoundTheme && soundFilePaths[soundEvent]) { console.log("AudioService: Using system sound for", soundEvent, ":", soundFilePaths[soundEvent]); return soundFilePaths[soundEvent]; } const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav"); console.log("AudioService: Using bundled sound for", soundEvent, ":", bundledPath); return bundledPath; } function reloadSounds() { console.log("AudioService: Reloading sounds, useSystemSoundTheme:", SettingsData.useSystemSoundTheme, "currentSoundTheme:", currentSoundTheme); if (SettingsData.useSystemSoundTheme && currentSoundTheme) { discoverSoundFiles(currentSoundTheme); } else { soundFilePaths = {}; if (soundsAvailable) { destroySoundPlayers(); createSoundPlayers(); } } } function setupMediaDevices() { if (!soundsAvailable || mediaDevices) { return; } try { mediaDevices = Qt.createQmlObject(` import QtQuick import QtMultimedia MediaDevices { id: devices Component.onCompleted: { console.log("AudioService: MediaDevices initialized, default output:", defaultAudioOutput?.description) } } `, root, "AudioService.MediaDevices"); if (mediaDevices) { mediaDevicesConnections = Qt.createQmlObject(` import QtQuick Connections { target: root.mediaDevices function onDefaultAudioOutputChanged() { console.log("AudioService: Default audio output changed, recreating sound players") root.destroySoundPlayers() root.createSoundPlayers() } } `, root, "AudioService.MediaDevicesConnections"); } } catch (e) { console.log("AudioService: MediaDevices not available, using default audio output"); mediaDevices = null; } } function destroySoundPlayers() { if (volumeChangeSound) { volumeChangeSound.destroy(); volumeChangeSound = null; } if (powerPlugSound) { powerPlugSound.destroy(); powerPlugSound = null; } if (powerUnplugSound) { powerUnplugSound.destroy(); powerUnplugSound = null; } if (normalNotificationSound) { normalNotificationSound.destroy(); normalNotificationSound = null; } if (criticalNotificationSound) { criticalNotificationSound.destroy(); criticalNotificationSound = null; } } function createSoundPlayers() { if (!soundsAvailable) { return; } setupMediaDevices(); try { const deviceProperty = mediaDevices ? `device: root.mediaDevices.defaultAudioOutput\n ` : ""; const volumeChangePath = getSoundPath("audio-volume-change"); volumeChangeSound = Qt.createQmlObject(` import QtQuick import QtMultimedia MediaPlayer { source: "${volumeChangePath}" audioOutput: AudioOutput { ${deviceProperty}volume: notificationsVolume } } `, root, "AudioService.VolumeChangeSound"); const powerPlugPath = getSoundPath("power-plug"); powerPlugSound = Qt.createQmlObject(` import QtQuick import QtMultimedia MediaPlayer { source: "${powerPlugPath}" audioOutput: AudioOutput { ${deviceProperty}volume: notificationsVolume } } `, root, "AudioService.PowerPlugSound"); const powerUnplugPath = getSoundPath("power-unplug"); powerUnplugSound = Qt.createQmlObject(` import QtQuick import QtMultimedia MediaPlayer { source: "${powerUnplugPath}" audioOutput: AudioOutput { ${deviceProperty}volume: notificationsVolume } } `, root, "AudioService.PowerUnplugSound"); const messagePath = getSoundPath("message"); normalNotificationSound = Qt.createQmlObject(` import QtQuick import QtMultimedia MediaPlayer { source: "${messagePath}" audioOutput: AudioOutput { ${deviceProperty}volume: notificationsVolume } } `, root, "AudioService.NormalNotificationSound"); const messageNewInstantPath = getSoundPath("message-new-instant"); criticalNotificationSound = Qt.createQmlObject(` import QtQuick import QtMultimedia MediaPlayer { source: "${messageNewInstantPath}" audioOutput: AudioOutput { ${deviceProperty}volume: notificationsVolume } } `, root, "AudioService.CriticalNotificationSound"); } catch (e) { console.warn("AudioService: Error creating sound players:", e); } } function playVolumeChangeSound() { if (soundsAvailable && volumeChangeSound && !notificationsAudioMuted) { volumeChangeSound.play(); } } function playPowerPlugSound() { if (soundsAvailable && powerPlugSound && !notificationsAudioMuted) { powerPlugSound.play(); } } function playPowerUnplugSound() { if (soundsAvailable && powerUnplugSound && !notificationsAudioMuted) { powerUnplugSound.play(); } } function playNormalNotificationSound() { if (soundsAvailable && normalNotificationSound && !SessionData.doNotDisturb && !notificationsAudioMuted) { normalNotificationSound.play(); } } function playCriticalNotificationSound() { if (soundsAvailable && criticalNotificationSound && !SessionData.doNotDisturb && !notificationsAudioMuted) { criticalNotificationSound.play(); } } function playVolumeChangeSoundIfEnabled() { if (SettingsData.soundsEnabled && SettingsData.soundVolumeChanged && !notificationsAudioMuted) { playVolumeChangeSound(); } } function displayName(node) { if (!node) { return ""; } // FIRST: Check if we have a custom alias in our deviceAliases map // This ensures we always show the user's custom name, regardless of // whether WirePlumber has applied it to the node properties yet if (node.name && deviceAliases[node.name]) { return deviceAliases[node.name]; } // Check node.properties["node.description"] for WirePlumber-applied aliases // This is the live property updated by WirePlumber rules if (node.properties && node.properties["node.description"]) { const desc = node.properties["node.description"]; if (desc !== node.name) { return desc; } } // Check cached description as fallback if (node.description && node.description !== node.name) { return node.description; } // Fallback to device description property if (node.properties && node.properties["device.description"]) { return node.properties["device.description"]; } // Fallback to nickname if (node.nickname && node.nickname !== node.name) { return node.nickname; } // Fallback to friendly names based on node name patterns if (node.name.includes("analog-stereo")) { return "Built-in Audio Analog Stereo"; } if (node.name.includes("bluez")) { return "Bluetooth Audio"; } if (node.name.includes("usb")) { return "USB Audio"; } if (node.name.includes("hdmi")) { return "HDMI Audio"; } return node.name; } function originalName(node) { if (!node) { return ""; } // Get the original name without checking for custom aliases // Check pattern-based friendly names FIRST (before device.description) // This ensures we show user-friendly names like "Built-in Audio Analog Stereo" // instead of hardware chip names like "ALC274 Analog" if (node.name.includes("analog-stereo")) { return "Built-in Audio Analog Stereo"; } if (node.name.includes("bluez")) { return "Bluetooth Audio"; } if (node.name.includes("usb")) { return "USB Audio"; } if (node.name.includes("hdmi")) { return "HDMI Audio"; } if (node.name.includes("raop_sink")) { // Extract friendly name from RAOP node name const match = node.name.match(/raop_sink\.([^.]+)/); if (match) { return match[1].replace(/-/g, " "); } } // Fallback to device.description property if (node.properties && node.properties["device.description"]) { return node.properties["device.description"]; } // Fallback to nickname if (node.nickname && node.nickname !== node.name) { return node.nickname; } return node.name; } function subtitle(name) { if (!name) { return ""; } if (name.includes('usb-')) { if (name.includes('SteelSeries')) { return "USB Gaming Headset"; } if (name.includes('Generic')) { return "USB Audio Device"; } return "USB Audio"; } if (name.includes('pci-')) { if (name.includes('01_00.1') || name.includes('01:00.1')) { return "NVIDIA GPU Audio"; } return "PCI Audio"; } if (name.includes('bluez')) { return "Bluetooth Audio"; } if (name.includes('analog')) { return "Built-in Audio"; } if (name.includes('hdmi')) { return "HDMI Audio"; } return ""; } PwObjectTracker { objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream) } Connections { target: Pipewire function onDefaultAudioSinkChanged() { if (soundsAvailable) { Qt.callLater(root.destroySoundPlayers); Qt.callLater(root.createSoundPlayers); } } } function setVolume(percentage) { if (!root.sink?.audio) return "No audio sink available"; const maxVol = root.sinkMaxVolume; const clampedVolume = Math.max(0, Math.min(maxVol, percentage)); root.sink.audio.volume = clampedVolume / 100; return `Volume set to ${clampedVolume}%`; } function toggleMute() { if (!root.sink?.audio) { return "No audio sink available"; } root.sink.audio.muted = !root.sink.audio.muted; return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"; } function setMicVolume(percentage) { if (!root.source?.audio) { return "No audio source available"; } const clampedVolume = Math.max(0, Math.min(100, percentage)); root.source.audio.volume = clampedVolume / 100; return `Microphone volume set to ${clampedVolume}%`; } function toggleMicMute() { if (!root.source?.audio) { return "No audio source available"; } root.source.audio.muted = !root.source.audio.muted; return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted"; } IpcHandler { target: "audio" function setvolume(percentage: string): string { return root.setVolume(parseInt(percentage)); } function increment(step: string): string { if (!root.sink?.audio) return "No audio sink available"; if (root.sink.audio.muted) root.sink.audio.muted = false; const maxVol = root.sinkMaxVolume; const currentVolume = Math.round(root.sink.audio.volume * 100); const stepValue = parseInt(step || "5"); const newVolume = Math.max(0, Math.min(maxVol, currentVolume + stepValue)); root.sink.audio.volume = newVolume / 100; return `Volume increased to ${newVolume}%`; } function decrement(step: string): string { if (!root.sink?.audio) return "No audio sink available"; if (root.sink.audio.muted) root.sink.audio.muted = false; const maxVol = root.sinkMaxVolume; const currentVolume = Math.round(root.sink.audio.volume * 100); const stepValue = parseInt(step || "5"); const newVolume = Math.max(0, Math.min(maxVol, currentVolume - stepValue)); root.sink.audio.volume = newVolume / 100; return `Volume decreased to ${newVolume}%`; } function mute(): string { return root.toggleMute(); } function setmic(percentage: string): string { return root.setMicVolume(parseInt(percentage)); } function micmute(): string { const result = root.toggleMicMute(); root.micMuteChanged(); return result; } function status(): string { let result = "Audio Status:\n"; if (root.sink?.audio) { const volume = Math.round(root.sink.audio.volume * 100); const muteStatus = root.sink.audio.muted ? " (muted)" : ""; const maxVol = root.sinkMaxVolume; result += `Output: ${volume}%${muteStatus} (max: ${maxVol}%)\n`; } else { result += "Output: No sink available\n"; } if (root.source?.audio) { const micVolume = Math.round(root.source.audio.volume * 100); const muteStatus = root.source.audio.muted ? " (muted)" : ""; result += `Input: ${micVolume}%${muteStatus}`; } else { result += "Input: No source available"; } return result; } function getmaxvolume(): string { return `${root.sinkMaxVolume}`; } function setmaxvolume(percent: string): string { if (!root.sink?.name) return "No audio sink available"; const val = parseInt(percent); if (isNaN(val)) return "Invalid percentage"; SessionData.setDeviceMaxVolume(root.sink.name, val); return `Max volume set to ${SessionData.getDeviceMaxVolume(root.sink.name)}%`; } function getmaxvolumefor(nodeName: string): string { if (!nodeName) return "No node name specified"; return `${SessionData.getDeviceMaxVolume(nodeName)}`; } function setmaxvolumefor(nodeName: string, percent: string): string { if (!nodeName) return "No node name specified"; const val = parseInt(percent); if (isNaN(val)) return "Invalid percentage"; SessionData.setDeviceMaxVolume(nodeName, val); return `Max volume for ${nodeName} set to ${SessionData.getDeviceMaxVolume(nodeName)}%`; } function cycleoutput(): string { const result = root.cycleAudioOutput(); if (!result) return "Only one audio output available"; return `Switched to: ${result}`; } } Connections { target: SettingsData function onUseSystemSoundThemeChanged() { reloadSounds(); } } Component.onCompleted: { if (!detectSoundsAvailability()) { console.warn("AudioService: QtMultimedia not available - sound effects disabled"); } else { console.info("AudioService: Sound effects enabled"); checkGsettings(); Qt.callLater(createSoundPlayers); } loadDeviceAliases(); } }