mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
186 lines
6.0 KiB
QML
186 lines
6.0 KiB
QML
pragma Singleton
|
|
import QtMultimedia
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import qs.Commons
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// Map to track active sound players: resolvedPath -> MediaPlayer instance
|
|
property var activePlayers: ({})
|
|
|
|
// Container for dynamically created players
|
|
Item {
|
|
id: playersContainer
|
|
}
|
|
|
|
// Component for creating MediaPlayer instances
|
|
Component {
|
|
id: playerComponent
|
|
MediaPlayer {
|
|
id: mediaPlayer
|
|
property string resolvedPath: ""
|
|
property bool shouldFallback: false
|
|
property real soundVolume: 1.0
|
|
|
|
audioOutput: AudioOutput {
|
|
volume: soundVolume
|
|
}
|
|
|
|
onErrorOccurred: {
|
|
Logger.w("SoundService", "Error playing sound:", source, error, errorString);
|
|
if (shouldFallback) {
|
|
const fallbackPath = Quickshell.shellDir + "/Assets/Sounds/notification.mp3";
|
|
if (fallbackPath !== resolvedPath) {
|
|
root.playSound(fallbackPath, {
|
|
volume: soundVolume,
|
|
fallback: false,
|
|
repeat: false
|
|
});
|
|
}
|
|
}
|
|
// Clean up on error
|
|
if (root.activePlayers[resolvedPath]) {
|
|
delete root.activePlayers[resolvedPath];
|
|
}
|
|
destroy();
|
|
}
|
|
|
|
onPlaybackStateChanged: function (state) {
|
|
if (state === MediaPlayer.StoppedState && loops === 1) {
|
|
// Clean up non-looping players when they finish
|
|
if (root.activePlayers[resolvedPath]) {
|
|
delete root.activePlayers[resolvedPath];
|
|
}
|
|
destroy();
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
play();
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
Logger.i("SoundService", "Service started");
|
|
}
|
|
|
|
/**
|
|
* Resolve sound path to absolute file path
|
|
* @param soundPath - Path to the sound file (absolute, relative to shellDir, or just filename for Assets/Sounds/)
|
|
* @returns Resolved absolute path
|
|
*/
|
|
function resolvePath(soundPath) {
|
|
if (!soundPath || soundPath === "") {
|
|
return "";
|
|
}
|
|
|
|
let resolvedPath = soundPath;
|
|
|
|
// If it's just a filename (no path separators), assume it's in Assets/Sounds/
|
|
if (!soundPath.includes("/") && !soundPath.startsWith("file://")) {
|
|
resolvedPath = Quickshell.shellDir + "/Assets/Sounds/" + soundPath;
|
|
} else if (!soundPath.startsWith("/") && !soundPath.startsWith("file://")) {
|
|
// Relative path - assume it's relative to shellDir
|
|
resolvedPath = Quickshell.shellDir + "/" + soundPath;
|
|
} else if (soundPath.startsWith("file://")) {
|
|
resolvedPath = soundPath.substring(7); // Remove "file://" prefix
|
|
}
|
|
// Absolute paths are used as-is
|
|
|
|
return resolvedPath;
|
|
}
|
|
|
|
/**
|
|
* Play a sound file
|
|
* @param soundPath - Path to the sound file (absolute, relative to shellDir, or just filename for Assets/Sounds/)
|
|
* @param options - Optional object with:
|
|
* - volume: Volume level (0.0 to 1.0, default: 1.0)
|
|
* - fallback: Whether to fallback to default notification sound if file not found (default: false)
|
|
* - repeat: Whether to repeat/loop the sound continuously (default: false)
|
|
*/
|
|
function playSound(soundPath, options) {
|
|
if (!soundPath || soundPath === "") {
|
|
Logger.w("SoundService", "No sound path provided");
|
|
return;
|
|
}
|
|
|
|
const opts = options || {};
|
|
const volume = opts.volume !== undefined ? opts.volume : 1.0;
|
|
const fallback = opts.fallback !== undefined ? opts.fallback : false;
|
|
const repeat = opts.repeat !== undefined ? opts.repeat : false;
|
|
|
|
// Resolve path
|
|
const resolvedPath = resolvePath(soundPath);
|
|
|
|
// Stop any existing player for this path if it's looping
|
|
if (repeat && activePlayers[resolvedPath]) {
|
|
stopSound(soundPath);
|
|
}
|
|
|
|
// Create MediaPlayer instance
|
|
const player = playerComponent.createObject(playersContainer, {
|
|
resolvedPath: resolvedPath,
|
|
source: "file://" + resolvedPath,
|
|
loops: repeat ? MediaPlayer.Infinite : 1,
|
|
soundVolume: Math.max(0, Math.min(1, volume)),
|
|
shouldFallback: fallback && !repeat
|
|
});
|
|
|
|
if (!player) {
|
|
Logger.w("SoundService", "Failed to create MediaPlayer for:", resolvedPath);
|
|
// Try fallback if requested
|
|
if (fallback && !repeat) {
|
|
const defaultSound = Quickshell.shellDir + "/Assets/Sounds/notification.mp3";
|
|
if (defaultSound !== resolvedPath) {
|
|
playSound(defaultSound, {
|
|
volume: volume,
|
|
fallback: false,
|
|
repeat: false
|
|
});
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Store player in activePlayers map
|
|
activePlayers[resolvedPath] = player;
|
|
|
|
Logger.d("SoundService", "Playing sound:", resolvedPath, `(volume: ${Math.round(volume * 100)}%)`, repeat ? "(repeat)" : "");
|
|
}
|
|
|
|
/**
|
|
* Stop a playing sound
|
|
* @param soundPath - Path to the sound file to stop (optional, if not provided stops all repeating sounds)
|
|
*/
|
|
function stopSound(soundPath) {
|
|
if (soundPath) {
|
|
// Resolve path the same way as playSound
|
|
const resolvedPath = resolvePath(soundPath);
|
|
|
|
// Stop and remove the player for this specific sound
|
|
if (activePlayers[resolvedPath]) {
|
|
const player = activePlayers[resolvedPath];
|
|
player.stop();
|
|
delete activePlayers[resolvedPath];
|
|
player.destroy();
|
|
Logger.d("SoundService", "Stopped sound:", resolvedPath);
|
|
}
|
|
} else {
|
|
// Stop all active players (typically used for repeating sounds)
|
|
const paths = Object.keys(activePlayers);
|
|
for (let i = 0; i < paths.length; i++) {
|
|
const path = paths[i];
|
|
const player = activePlayers[path];
|
|
player.stop();
|
|
player.destroy();
|
|
}
|
|
activePlayers = {};
|
|
Logger.d("SoundService", "Stopped all sounds");
|
|
}
|
|
}
|
|
}
|