Simplify GStreamer sound playback
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user