From 7043f08dab28f38ff370b9fc278507afd94b3955 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 23 Mar 2026 13:51:42 -0400 Subject: [PATCH] Simplify GStreamer sound playback --- src/cthulhu/meson.build | 1 - src/cthulhu/sound.py | 105 ++++++++++++----------------- src/cthulhu/sound_helper.py | 74 -------------------- src/cthulhu/sound_theme_manager.py | 14 ++-- 4 files changed, 49 insertions(+), 145 deletions(-) delete mode 100644 src/cthulhu/sound_helper.py diff --git a/src/cthulhu/meson.build b/src/cthulhu/meson.build index 059ccc3..e1d13b1 100644 --- a/src/cthulhu/meson.build +++ b/src/cthulhu/meson.build @@ -86,7 +86,6 @@ cthulhu_python_sources = files([ 'signal_manager.py', 'sleep_mode_manager.py', 'sound.py', - 'sound_helper.py', 'sound_generator.py', 'sound_theme_manager.py', 'speech_and_verbosity_manager.py', diff --git a/src/cthulhu/sound.py b/src/cthulhu/sound.py index f0fe7f1..a7d0279 100644 --- a/src/cthulhu/sound.py +++ b/src/cthulhu/sound.py @@ -31,13 +31,9 @@ __date__ = "$Date:$" __copyright__ = "Copyright (c) 2016 Cthulhu Team" __license__ = "LGPL" -import os -import subprocess -import sys - import gi from gi.repository import GLib -from typing import Optional, Any, Tuple +from typing import Optional, Any try: gi.require_version('Gst', '1.0') @@ -48,6 +44,7 @@ else: _gstreamerAvailable, args = Gst.init_check(None) from . import debug +from . import settings from .sound_generator import Icon, Tone _soundSystemFailureReason: Optional[str] = None @@ -58,6 +55,7 @@ class Player: def __init__(self) -> None: self._initialized: bool = False self._source: Optional[Any] = None # Optional[Gst.Element] + self._volume: Optional[Any] = None # Optional[Gst.Element] self._sink: Optional[Any] = None # Optional[Gst.Element] self._player: Optional[Any] = None # Optional[Gst.Element] self._pipeline: Optional[Any] = None # Optional[Gst.Pipeline] @@ -91,21 +89,43 @@ class Player: element.set_state(Gst.State.NULL) return False + @staticmethod + def _get_configured_volume() -> float: + """Returns the configured sound volume with a safe fallback.""" + + try: + from . import cthulhu + if cthulhu.cthulhuApp is not None: + volume = cthulhu.cthulhuApp.settingsManager.getSetting('soundVolume') + return max(0.0, float(volume)) + except Exception: + pass + + return max(0.0, float(settings.soundVolume)) + def _playIcon(self, icon: Icon, interrupt: bool = True) -> None: """Plays a sound icon, interrupting the current play first unless specified.""" + if not self._player: + return + if interrupt: self._player.set_state(Gst.State.NULL) + self._player.set_property('volume', self._get_configured_volume()) self._player.set_property('uri', f'file://{icon.path}') self._player.set_state(Gst.State.PLAYING) def _playIconAndWait(self, icon: Icon, interrupt: bool = True, timeout_seconds: Optional[int] = 10) -> bool: """Plays a sound icon and waits for completion.""" + if not self._player: + return False + if interrupt: self._player.set_state(Gst.State.NULL) + self._player.set_property('volume', self._get_configured_volume()) self._player.set_property('uri', f'file://{icon.path}') self._player.set_state(Gst.State.PLAYING) @@ -133,10 +153,18 @@ class Player: def _playTone(self, tone: Tone, interrupt: bool = True) -> None: """Plays a tone, interrupting the current play first unless specified.""" + if not self._pipeline or not self._source: + return + if interrupt: self._pipeline.set_state(Gst.State.NULL) - self._source.set_property('volume', tone.volume) + self._pipeline.set_state(Gst.State.NULL) + if self._volume is not None: + self._volume.set_property('volume', tone.volume) + self._source.set_property('volume', 1.0) + else: + self._source.set_property('volume', tone.volume) self._source.set_property('freq', tone.frequency) self._source.set_property('wave', tone.wave) self._pipeline.set_state(Gst.State.PLAYING) @@ -168,13 +196,20 @@ class Player: bus.connect("message", self._onPipelineMessage) self._source = Gst.ElementFactory.make('audiotestsrc', 'src') + self._volume = Gst.ElementFactory.make('volume', 'volume') self._sink = Gst.ElementFactory.make('autoaudiosink', 'output') if self._source is None or self._sink is None: return self._pipeline.add(self._source) + if self._volume is not None: + self._pipeline.add(self._volume) self._pipeline.add(self._sink) - self._source.link(self._sink) + if self._volume is not None: + self._source.link(self._volume) + self._volume.link(self._sink) + else: + self._source.link(self._sink) self._initialized = True @@ -255,59 +290,3 @@ def getSoundSystemFailureReason() -> Optional[str]: def isSoundSystemAvailable() -> bool: return _soundSystemFailureReason is None - -def _playIconInSubprocess(icon: Icon, timeoutSeconds: int = 10) -> Tuple[bool, Optional[str]]: - if not icon.isValid(): - return False, f"Invalid sound icon: {icon.path}" - - pythonPathEntries = [] - for path in sys.path: - if not path: - path = os.getcwd() - if os.path.isdir(path) and path not in pythonPathEntries: - pythonPathEntries.append(path) - - environment = os.environ.copy() - if pythonPathEntries: - environment["PYTHONPATH"] = os.pathsep.join(pythonPathEntries) - - command = [ - sys.executable, - "-m", - "cthulhu.sound_helper", - "--play-file", - icon.path, - "--timeout-seconds", - str(timeoutSeconds), - ] - - try: - result = subprocess.run( - command, - capture_output=True, - text=True, - timeout=timeoutSeconds + 2, - check=False, - env=environment, - ) - except subprocess.TimeoutExpired: - return False, f"Sound probe timed out after {timeoutSeconds} seconds" - except Exception as error: - return False, f"Sound probe failed to launch: {error}" - - if result.returncode == 0: - return True, None - - if result.returncode < 0: - reason = f"Sound probe exited via signal {-result.returncode}" - else: - reason = f"Sound probe exited with status {result.returncode}" - - stderr = (result.stderr or "").strip() - if stderr: - reason = f"{reason}: {stderr.splitlines()[0]}" - - return False, reason - -def playIconSafely(icon: Icon, timeoutSeconds: int = 10) -> Tuple[bool, Optional[str]]: - return _playIconInSubprocess(icon, timeoutSeconds) diff --git a/src/cthulhu/sound_helper.py b/src/cthulhu/sound_helper.py deleted file mode 100644 index 022e5b6..0000000 --- a/src/cthulhu/sound_helper.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2026 Stormux -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. - -"""Minimal sound playback helper used to isolate startup sound crashes.""" - -from __future__ import annotations - -import argparse -import pathlib -import sys - -import gi - -gi.require_version('Gst', '1.0') -from gi.repository import Gst - - -def play_file(soundPath: str, timeoutSeconds: int = 10) -> int: - available, _args = Gst.init_check(None) - if not available: - return 1 - - player = Gst.ElementFactory.make('playbin', 'cthulhu-sound-helper') - if player is None: - return 1 - - player.set_property('uri', pathlib.Path(soundPath).resolve().as_uri()) - player.set_state(Gst.State.PLAYING) - - bus = player.get_bus() - if bus is None: - player.set_state(Gst.State.NULL) - return 1 - - timeoutNs = int(timeoutSeconds * Gst.SECOND) - try: - message = bus.timed_pop_filtered( - timeoutNs, - Gst.MessageType.EOS | Gst.MessageType.ERROR - ) - if message is None: - return 1 - if message.type == Gst.MessageType.ERROR: - return 1 - return 0 - finally: - player.set_state(Gst.State.NULL) - - -def main() -> int: - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument("--play-file") - parser.add_argument("--timeout-seconds", type=int, default=10) - args, _unknown = parser.parse_known_args() - - if args.play_file: - return play_file(args.play_file, args.timeout_seconds) - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/cthulhu/sound_theme_manager.py b/src/cthulhu/sound_theme_manager.py index 8e95c06..e963030 100644 --- a/src/cthulhu/sound_theme_manager.py +++ b/src/cthulhu/sound_theme_manager.py @@ -349,14 +349,14 @@ class SoundThemeManager: try: icon = Icon(os.path.dirname(soundPath), os.path.basename(soundPath)) if icon.isValid(): - if wait: - success, reason = sound.playIconSafely(icon) - if not success: - if reason: - sound.disableSoundSystem(reason) - self.app.getSettingsManager().setSetting('enableSound', False) - return success player = sound.getPlayer() + if wait and hasattr(player, "playAndWait"): + success = player.playAndWait(icon, interrupt=interrupt) + if not success: + reason = "Failed to play theme sound via GStreamer" + sound.disableSoundSystem(reason) + self.app.getSettingsManager().setSetting('enableSound', False) + return success player.play(icon, interrupt=interrupt) return True except Exception as e: