1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00
Files
DankMaterialShell/quickshell/Services/AudioService.qml

1042 lines
36 KiB
QML

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: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/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, string deviceIcon)
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() {
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
}
function cycleAudioOutput() {
const sinks = getAvailableSinks();
if (sinks.length < 2)
return null;
const currentName = root.sink?.name ?? "";
const currentIndex = sinks.findIndex(s => s.name === currentName);
const nextIndex = (currentIndex + 1) % sinks.length;
const nextSink = sinks[nextIndex];
Pipewire.preferredDefaultAudioSink = nextSink;
const name = displayName(nextSink);
audioOutputCycled(name, sinkIcon(nextSink));
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 configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/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 configPath = wireplumberConfigPath;
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 isMediaPlaying() {
return MprisController.activePlayer?.isPlaying ?? false;
}
function playVolumeChangeSound() {
if (!soundsAvailable || !volumeChangeSound || notificationsAudioMuted || isMediaPlaying())
return;
volumeChangeSound.play();
}
function playPowerPlugSound() {
if (!soundsAvailable || !powerPlugSound || notificationsAudioMuted || isMediaPlaying())
return;
powerPlugSound.play();
}
function playPowerUnplugSound() {
if (!soundsAvailable || !powerUnplugSound || notificationsAudioMuted || isMediaPlaying())
return;
powerUnplugSound.play();
}
function playNormalNotificationSound() {
if (!soundsAvailable || !normalNotificationSound || SessionData.doNotDisturb || notificationsAudioMuted || isMediaPlaying())
return;
normalNotificationSound.play();
}
function playCriticalNotificationSound() {
if (!soundsAvailable || !criticalNotificationSound || SessionData.doNotDisturb || notificationsAudioMuted || isMediaPlaying())
return;
criticalNotificationSound.play();
}
function playVolumeChangeSoundIfEnabled() {
if (SettingsData.soundsEnabled && SettingsData.soundVolumeChanged && !notificationsAudioMuted) {
playVolumeChangeSound();
}
}
function sinkIcon(node) {
if (!node)
return "speaker";
const props = node.properties || {};
const formFactor = (props["device.form-factor"] || "").toLowerCase();
switch (formFactor) {
case "headphone":
case "headset":
case "hands-free":
case "handset":
return "headset";
case "tv":
case "monitor":
return "tv";
case "speaker":
case "computer":
case "hifi":
case "portable":
case "car":
return "speaker";
}
const bus = (props["device.bus"] || "").toLowerCase();
if (bus === "bluetooth")
return "headset";
const name = (node.name || "").toLowerCase();
if (name.includes("hdmi"))
return "tv";
if (name.includes("iec958") || name.includes("spdif"))
return "speaker";
if (bus === "usb")
return "headset";
return "speaker";
}
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();
}
}