mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
309648d6d6
LocationTab: rework calendar settings SoundService: add simple service to play & loop sounds
114 lines
5.4 KiB
QML
114 lines
5.4 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import qs.Commons
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
Component.onCompleted: {
|
|
Logger.i("SoundService", "Service started");
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
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
|
|
|
|
// Build command with volume if supported
|
|
const volumeArg = volume < 1.0 ? Math.round(volume * 100) : "";
|
|
|
|
// Try different audio players in order of preference
|
|
let command = "";
|
|
|
|
if (repeat) {
|
|
// Repeat mode - use mpv or ffplay with loop, or paplay in a while loop
|
|
if (volumeArg && volumeArg > 0) {
|
|
command = `mpv --no-video --really-quiet --loop=inf --volume=${volumeArg} "${resolvedPath}" 2>/dev/null || ffplay -nodisp -loop -1 -loglevel quiet -volume ${volumeArg} "${resolvedPath}" 2>/dev/null || (while true; do paplay --volume=${volumeArg} "${resolvedPath}" 2>/dev/null || break; done)`;
|
|
} else {
|
|
command = `mpv --no-video --really-quiet --loop=inf "${resolvedPath}" 2>/dev/null || ffplay -nodisp -loop -1 -loglevel quiet "${resolvedPath}" 2>/dev/null || (while true; do paplay "${resolvedPath}" 2>/dev/null || break; done)`;
|
|
}
|
|
} else {
|
|
// Normal play once mode
|
|
if (volumeArg && volumeArg > 0) {
|
|
command = `paplay --volume=${volumeArg} "${resolvedPath}" 2>/dev/null || mpv --no-video --really-quiet --volume=${volumeArg} "${resolvedPath}" 2>/dev/null || ffplay -nodisp -autoexit -loglevel quiet -volume ${volumeArg} "${resolvedPath}" 2>/dev/null`;
|
|
} else {
|
|
command = `paplay "${resolvedPath}" 2>/dev/null || mpv --no-video --really-quiet "${resolvedPath}" 2>/dev/null || ffplay -nodisp -autoexit -loglevel quiet "${resolvedPath}" 2>/dev/null`;
|
|
}
|
|
}
|
|
|
|
// Add fallback to default notification sound if requested (only in non-repeat mode)
|
|
if (fallback && !repeat) {
|
|
const defaultSound = Quickshell.shellDir + "/Assets/Sounds/notification.mp3";
|
|
if (volumeArg && volumeArg > 0) {
|
|
command += ` || paplay --volume=${volumeArg} "${defaultSound}" 2>/dev/null || mpv --no-video --really-quiet --volume=${volumeArg} "${defaultSound}" 2>/dev/null || ffplay -nodisp -autoexit -loglevel quiet -volume ${volumeArg} "${defaultSound}" 2>/dev/null`;
|
|
} else {
|
|
command += ` || paplay "${defaultSound}" 2>/dev/null || mpv --no-video --really-quiet "${defaultSound}" 2>/dev/null || ffplay -nodisp -autoexit -loglevel quiet "${defaultSound}" 2>/dev/null`;
|
|
}
|
|
}
|
|
|
|
command += " || true"; // Always succeed
|
|
|
|
Logger.d("SoundService", "Playing sound:", resolvedPath, volumeArg ? `(volume: ${volumeArg}%)` : "", repeat ? "(repeat)" : "");
|
|
Quickshell.execDetached(["sh", "-c", command]);
|
|
}
|
|
|
|
/**
|
|
* Stop a playing sound by killing the audio player processes
|
|
* @param soundPath - Path to the sound file to stop (optional, if not provided stops all notification sounds)
|
|
*/
|
|
function stopSound(soundPath) {
|
|
let resolvedPath = soundPath;
|
|
|
|
if (soundPath) {
|
|
// Resolve path the same way as playSound
|
|
if (!soundPath.includes("/") && !soundPath.startsWith("file://")) {
|
|
resolvedPath = Quickshell.shellDir + "/Assets/Sounds/" + soundPath;
|
|
} else if (!soundPath.startsWith("/") && !soundPath.startsWith("file://")) {
|
|
resolvedPath = Quickshell.shellDir + "/" + soundPath;
|
|
} else if (soundPath.startsWith("file://")) {
|
|
resolvedPath = soundPath.substring(7);
|
|
}
|
|
|
|
// Kill processes playing this specific sound file
|
|
const command = `pkill -f "mpv.*${resolvedPath}" 2>/dev/null; pkill -f "ffplay.*${resolvedPath}" 2>/dev/null; pkill -f "paplay.*${resolvedPath}" 2>/dev/null; true`;
|
|
Logger.d("SoundService", "Stopping sound:", resolvedPath);
|
|
Quickshell.execDetached(["sh", "-c", command]);
|
|
} else {
|
|
// Kill all mpv/ffplay/paplay processes (be careful with this)
|
|
const command = `pkill -f "mpv.*--loop=inf" 2>/dev/null; pkill -f "ffplay.*-loop" 2>/dev/null; pkill -f "while true.*paplay" 2>/dev/null; true`;
|
|
Logger.d("SoundService", "Stopping all repeating sounds");
|
|
Quickshell.execDetached(["sh", "-c", command]);
|
|
}
|
|
}
|
|
}
|