Simplify GStreamer sound playback
This commit is contained in:
@@ -86,7 +86,6 @@ cthulhu_python_sources = files([
|
|||||||
'signal_manager.py',
|
'signal_manager.py',
|
||||||
'sleep_mode_manager.py',
|
'sleep_mode_manager.py',
|
||||||
'sound.py',
|
'sound.py',
|
||||||
'sound_helper.py',
|
|
||||||
'sound_generator.py',
|
'sound_generator.py',
|
||||||
'sound_theme_manager.py',
|
'sound_theme_manager.py',
|
||||||
'speech_and_verbosity_manager.py',
|
'speech_and_verbosity_manager.py',
|
||||||
|
|||||||
@@ -31,13 +31,9 @@ __date__ = "$Date:$"
|
|||||||
__copyright__ = "Copyright (c) 2016 Cthulhu Team"
|
__copyright__ = "Copyright (c) 2016 Cthulhu Team"
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
from typing import Optional, Any, Tuple
|
from typing import Optional, Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
gi.require_version('Gst', '1.0')
|
gi.require_version('Gst', '1.0')
|
||||||
@@ -48,6 +44,7 @@ else:
|
|||||||
_gstreamerAvailable, args = Gst.init_check(None)
|
_gstreamerAvailable, args = Gst.init_check(None)
|
||||||
|
|
||||||
from . import debug
|
from . import debug
|
||||||
|
from . import settings
|
||||||
from .sound_generator import Icon, Tone
|
from .sound_generator import Icon, Tone
|
||||||
|
|
||||||
_soundSystemFailureReason: Optional[str] = None
|
_soundSystemFailureReason: Optional[str] = None
|
||||||
@@ -58,6 +55,7 @@ class Player:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._initialized: bool = False
|
self._initialized: bool = False
|
||||||
self._source: Optional[Any] = None # Optional[Gst.Element]
|
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._sink: Optional[Any] = None # Optional[Gst.Element]
|
||||||
self._player: Optional[Any] = None # Optional[Gst.Element]
|
self._player: Optional[Any] = None # Optional[Gst.Element]
|
||||||
self._pipeline: Optional[Any] = None # Optional[Gst.Pipeline]
|
self._pipeline: Optional[Any] = None # Optional[Gst.Pipeline]
|
||||||
@@ -91,21 +89,43 @@ class Player:
|
|||||||
element.set_state(Gst.State.NULL)
|
element.set_state(Gst.State.NULL)
|
||||||
return False
|
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:
|
def _playIcon(self, icon: Icon, interrupt: bool = True) -> None:
|
||||||
"""Plays a sound icon, interrupting the current play first unless specified."""
|
"""Plays a sound icon, interrupting the current play first unless specified."""
|
||||||
|
|
||||||
|
if not self._player:
|
||||||
|
return
|
||||||
|
|
||||||
if interrupt:
|
if interrupt:
|
||||||
self._player.set_state(Gst.State.NULL)
|
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_property('uri', f'file://{icon.path}')
|
||||||
self._player.set_state(Gst.State.PLAYING)
|
self._player.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
def _playIconAndWait(self, icon: Icon, interrupt: bool = True, timeout_seconds: Optional[int] = 10) -> bool:
|
def _playIconAndWait(self, icon: Icon, interrupt: bool = True, timeout_seconds: Optional[int] = 10) -> bool:
|
||||||
"""Plays a sound icon and waits for completion."""
|
"""Plays a sound icon and waits for completion."""
|
||||||
|
|
||||||
|
if not self._player:
|
||||||
|
return False
|
||||||
|
|
||||||
if interrupt:
|
if interrupt:
|
||||||
self._player.set_state(Gst.State.NULL)
|
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_property('uri', f'file://{icon.path}')
|
||||||
self._player.set_state(Gst.State.PLAYING)
|
self._player.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
@@ -133,10 +153,18 @@ class Player:
|
|||||||
def _playTone(self, tone: Tone, interrupt: bool = True) -> None:
|
def _playTone(self, tone: Tone, interrupt: bool = True) -> None:
|
||||||
"""Plays a tone, interrupting the current play first unless specified."""
|
"""Plays a tone, interrupting the current play first unless specified."""
|
||||||
|
|
||||||
|
if not self._pipeline or not self._source:
|
||||||
|
return
|
||||||
|
|
||||||
if interrupt:
|
if interrupt:
|
||||||
self._pipeline.set_state(Gst.State.NULL)
|
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('freq', tone.frequency)
|
||||||
self._source.set_property('wave', tone.wave)
|
self._source.set_property('wave', tone.wave)
|
||||||
self._pipeline.set_state(Gst.State.PLAYING)
|
self._pipeline.set_state(Gst.State.PLAYING)
|
||||||
@@ -168,13 +196,20 @@ class Player:
|
|||||||
bus.connect("message", self._onPipelineMessage)
|
bus.connect("message", self._onPipelineMessage)
|
||||||
|
|
||||||
self._source = Gst.ElementFactory.make('audiotestsrc', 'src')
|
self._source = Gst.ElementFactory.make('audiotestsrc', 'src')
|
||||||
|
self._volume = Gst.ElementFactory.make('volume', 'volume')
|
||||||
self._sink = Gst.ElementFactory.make('autoaudiosink', 'output')
|
self._sink = Gst.ElementFactory.make('autoaudiosink', 'output')
|
||||||
if self._source is None or self._sink is None:
|
if self._source is None or self._sink is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._pipeline.add(self._source)
|
self._pipeline.add(self._source)
|
||||||
|
if self._volume is not None:
|
||||||
|
self._pipeline.add(self._volume)
|
||||||
self._pipeline.add(self._sink)
|
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
|
self._initialized = True
|
||||||
|
|
||||||
@@ -255,59 +290,3 @@ def getSoundSystemFailureReason() -> Optional[str]:
|
|||||||
|
|
||||||
def isSoundSystemAvailable() -> bool:
|
def isSoundSystemAvailable() -> bool:
|
||||||
return _soundSystemFailureReason is None
|
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:
|
try:
|
||||||
icon = Icon(os.path.dirname(soundPath), os.path.basename(soundPath))
|
icon = Icon(os.path.dirname(soundPath), os.path.basename(soundPath))
|
||||||
if icon.isValid():
|
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()
|
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)
|
player.play(icon, interrupt=interrupt)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user