diff --git a/sounds/default/editbox.wav b/sounds/default/focus_mode.wav similarity index 100% rename from sounds/default/editbox.wav rename to sounds/default/focus_mode.wav diff --git a/sounds/default/start.wav b/sounds/default/start.wav new file mode 100644 index 0000000..872b2e4 Binary files /dev/null and b/sounds/default/start.wav differ diff --git a/sounds/default/stop.wav b/sounds/default/stop.wav new file mode 100644 index 0000000..3ee4420 Binary files /dev/null and b/sounds/default/stop.wav differ diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index e923900..344df60 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -219,6 +219,7 @@ from . import settings from . import settings_manager from . import speech from . import sound +from . import sound_theme_manager from . import mouse_review from .ax_object import AXObject from .ax_utilities import AXUtilities @@ -871,6 +872,7 @@ def shutdown(script=None, inputEvent=None): cthulhu_state.activeScript.presentationInterrupt() cthulhuApp.getSignalManager().emitSignal('stop-application-completed') + sound_theme_manager.getManager().playStopSound(wait=True) cthulhuApp.getPluginSystemManager().unloadAllPlugins(ForceAllPlugins=True) # Deactivate the event manager first so that it clears its queue and will not @@ -993,6 +995,7 @@ def main(): debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Initialized.", True) script = cthulhu_state.activeScript + sound_theme_manager.getManager().playStartSound(wait=True) cthulhuApp.getSignalManager().emitSignal('start-application-completed') if script: window = script.utilities.activeWindow() diff --git a/src/cthulhu/sound.py b/src/cthulhu/sound.py index 575a32a..a427d10 100644 --- a/src/cthulhu/sound.py +++ b/src/cthulhu/sound.py @@ -93,6 +93,36 @@ class Player: self._player.set_property('uri', f'file://{icon.path}') self._player.set_state(Gst.State.PLAYING) + def _playIconAndWait(self, icon, interrupt=True, timeout_seconds=10): + """Plays a sound icon and waits for completion.""" + + if interrupt: + self._player.set_state(Gst.State.NULL) + + self._player.set_property('uri', f'file://{icon.path}') + self._player.set_state(Gst.State.PLAYING) + + bus = self._player.get_bus() + if not bus: + return False + + if timeout_seconds is None: + timeout_ns = Gst.CLOCK_TIME_NONE + else: + timeout_ns = int(timeout_seconds * Gst.SECOND) + + message = bus.timed_pop_filtered( + timeout_ns, + Gst.MessageType.EOS | Gst.MessageType.ERROR + ) + if message and message.type == Gst.MessageType.ERROR: + error, info = message.parse_error() + msg = f'SOUND ERROR: {error}' + debug.printMessage(debug.LEVEL_INFO, msg, True) + + self._player.set_state(Gst.State.NULL) + return message is not None and message.type == Gst.MessageType.EOS + def _playTone(self, tone, interrupt=True): """Plays a tone, interrupting the current play first unless specified.""" @@ -152,6 +182,25 @@ class Player: tokens = ["SOUND ERROR:", item, "is not an Icon or Tone"] debug.printTokens(debug.LEVEL_INFO, tokens, True) + def playAndWait(self, item, interrupt=True, timeout_seconds=10): + """Plays a sound and blocks until completion or timeout.""" + + if not self._player: + if _gstreamerAvailable and not self._initialized: + self.init() + if not self._player: + return False + + if isinstance(item, Icon): + return self._playIconAndWait( + item, + interrupt=interrupt, + timeout_seconds=timeout_seconds + ) + + self.play(item, interrupt) + return False + def stop(self, element=None): """Stops play.""" diff --git a/src/cthulhu/sound_theme_manager.py b/src/cthulhu/sound_theme_manager.py index 3fe5b32..58c2dde 100644 --- a/src/cthulhu/sound_theme_manager.py +++ b/src/cthulhu/sound_theme_manager.py @@ -47,9 +47,11 @@ from .sound_generator import Icon _settingsManager = settings_manager.getManager() # Sound event constants - add new events here for easy extensibility -SOUND_FOCUS_MODE = "editbox" +SOUND_FOCUS_MODE = "focus_mode" SOUND_BROWSE_MODE = "browse_mode" SOUND_BUTTON = "button" +SOUND_START = "start" +SOUND_STOP = "stop" # Special theme name for no sounds THEME_NONE = "none" @@ -144,17 +146,23 @@ class SoundThemeManager: return None - def playSound(self, soundName, interrupt=True): - """Play a sound from the current theme if enabled. + def _playThemeSound(self, soundName, interrupt=True, wait=False, + requireModeChangeSetting=False, requireSoundSetting=False): + """Play a themed sound with optional gating and blocking. Args: soundName: The name of the sound file (without extension) interrupt: Whether to interrupt currently playing sounds + wait: Whether to block until the sound finishes playing + requireModeChangeSetting: Honor enableModeChangeSound setting + requireSoundSetting: Honor enableSound setting Returns: True if sound was played, False otherwise """ - if not _settingsManager.getSetting('enableModeChangeSound'): + if requireModeChangeSetting and not _settingsManager.getSetting('enableModeChangeSound'): + return False + if requireSoundSetting and not _settingsManager.getSetting('enableSound'): return False themeName = _settingsManager.getSetting('soundTheme') @@ -175,6 +183,8 @@ class SoundThemeManager: icon = Icon(os.path.dirname(soundPath), os.path.basename(soundPath)) if icon.isValid(): player = sound.getPlayer() + if wait and hasattr(player, "playAndWait"): + return player.playAndWait(icon, interrupt=interrupt) player.play(icon, interrupt=interrupt) return True except Exception as e: @@ -183,6 +193,24 @@ class SoundThemeManager: return False + def playSound(self, soundName, interrupt=True, wait=False): + """Play a sound from the current theme if enabled. + + Args: + soundName: The name of the sound file (without extension) + interrupt: Whether to interrupt currently playing sounds + wait: Whether to block until the sound finishes playing + + Returns: + True if sound was played, False otherwise + """ + return self._playThemeSound( + soundName, + interrupt=interrupt, + wait=wait, + requireModeChangeSetting=True + ) + def playFocusModeSound(self): """Play sound for entering focus mode.""" return self.playSound(SOUND_FOCUS_MODE) @@ -195,6 +223,24 @@ class SoundThemeManager: """Play sound for button focus (future use).""" return self.playSound(SOUND_BUTTON) + def playStartSound(self, wait=False): + """Play sound for application startup.""" + return self._playThemeSound( + SOUND_START, + interrupt=True, + wait=wait, + requireSoundSetting=True + ) + + def playStopSound(self, wait=False): + """Play sound for application shutdown.""" + return self._playThemeSound( + SOUND_STOP, + interrupt=True, + wait=wait, + requireSoundSetting=True + ) + _manager = None