Fixed critical sound timing bug. Finally, after a couple of days of banging my head against this problem and sound getting progressively worse, it's fixed. Turned out the fix was a very simple timing issue for when to play sounds.
This commit is contained in:
35
sound.py
35
sound.py
@@ -120,24 +120,25 @@ class Sound:
|
|||||||
pygame.event.clear()
|
pygame.event.clear()
|
||||||
pygame.mixer.stop()
|
pygame.mixer.stop()
|
||||||
|
|
||||||
# Play the sound
|
|
||||||
channel = self.sounds[soundName].play(-1 if loop else 0)
|
|
||||||
if not channel:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Apply appropriate volume settings
|
# Apply appropriate volume settings
|
||||||
sfx_volume = self.volumeService.get_sfx_volume()
|
sfx_volume = self.volumeService.get_sfx_volume()
|
||||||
|
|
||||||
# Handle positional audio if positions are provided
|
# Handle positional audio if positions are provided - check range BEFORE starting sound
|
||||||
if playerPos is not None and objPos is not None:
|
if playerPos is not None and objPos is not None:
|
||||||
# Calculate stereo panning
|
# Calculate stereo panning
|
||||||
left_vol, right_vol = self._get_stereo_panning(playerPos, objPos, centerDistance)
|
left_vol, right_vol = self._get_stereo_panning(playerPos, objPos, centerDistance)
|
||||||
|
|
||||||
# Don't play if out of range
|
# Don't play if out of range
|
||||||
if left_vol == 0 and right_vol == 0:
|
if left_vol == 0 and right_vol == 0:
|
||||||
channel.stop()
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Play the sound
|
||||||
|
channel = self.sounds[soundName].play(-1 if loop else 0)
|
||||||
|
if not channel:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Apply volume settings
|
||||||
|
if playerPos is not None and objPos is not None:
|
||||||
# Apply positional volume adjustments
|
# Apply positional volume adjustments
|
||||||
channel.set_volume(volume * left_vol * sfx_volume, volume * right_vol * sfx_volume)
|
channel.set_volume(volume * left_vol * sfx_volume, volume * right_vol * sfx_volume)
|
||||||
else:
|
else:
|
||||||
@@ -351,6 +352,10 @@ def _find_matching_sound(soundPattern, sounds):
|
|||||||
keys = [k for k in sounds.keys() if re.match("^" + soundPattern + ".*", k)]
|
keys = [k for k in sounds.keys() if re.match("^" + soundPattern + ".*", k)]
|
||||||
return random.choice(keys) if keys else None
|
return random.choice(keys) if keys else None
|
||||||
|
|
||||||
|
def get_available_channel():
|
||||||
|
"""Get an available channel for playing sounds."""
|
||||||
|
return pygame.mixer.find_channel()
|
||||||
|
|
||||||
# Global functions for backward compatibility
|
# Global functions for backward compatibility
|
||||||
def play_bgm(musicFile):
|
def play_bgm(musicFile):
|
||||||
"""Play background music with proper volume settings."""
|
"""Play background music with proper volume settings."""
|
||||||
@@ -425,20 +430,22 @@ def play_sound(sound_or_name, volume=1.0, loop=False, playerPos=None, objPos=Non
|
|||||||
|
|
||||||
# Case 4: Sound name with dictionary
|
# Case 4: Sound name with dictionary
|
||||||
elif isinstance(sounds, dict) and isinstance(sound_or_name, str) and sound_or_name in sounds:
|
elif isinstance(sounds, dict) and isinstance(sound_or_name, str) and sound_or_name in sounds:
|
||||||
|
# Apply volume settings
|
||||||
|
sfx_vol = volumeService.get_sfx_volume()
|
||||||
|
|
||||||
|
# Handle positional audio - check range BEFORE starting sound
|
||||||
|
if playerPos is not None and objPos is not None:
|
||||||
|
left_vol, right_vol = _get_stereo_panning(playerPos, objPos, centerDistance)
|
||||||
|
if left_vol == 0 and right_vol == 0:
|
||||||
|
return None # Don't start sound if out of range
|
||||||
|
|
||||||
# Play the sound
|
# Play the sound
|
||||||
channel = sounds[sound_or_name].play(-1 if loop else 0)
|
channel = sounds[sound_or_name].play(-1 if loop else 0)
|
||||||
if not channel:
|
if not channel:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Apply volume settings
|
# Apply volume settings
|
||||||
sfx_vol = volumeService.get_sfx_volume()
|
|
||||||
|
|
||||||
# Handle positional audio
|
|
||||||
if playerPos is not None and objPos is not None:
|
if playerPos is not None and objPos is not None:
|
||||||
left_vol, right_vol = _get_stereo_panning(playerPos, objPos, centerDistance)
|
|
||||||
if left_vol == 0 and right_vol == 0:
|
|
||||||
channel.stop()
|
|
||||||
return None
|
|
||||||
channel.set_volume(volume * left_vol * sfx_vol, volume * right_vol * sfx_vol)
|
channel.set_volume(volume * left_vol * sfx_vol, volume * right_vol * sfx_vol)
|
||||||
else:
|
else:
|
||||||
channel.set_volume(volume * sfx_vol)
|
channel.set_volume(volume * sfx_vol)
|
||||||
|
171
speech.py
171
speech.py
@@ -130,15 +130,31 @@ def speak(text, interrupt=True):
|
|||||||
_speechInstance = Speech.get_instance()
|
_speechInstance = Speech.get_instance()
|
||||||
_speechInstance.speak(text, interrupt)
|
_speechInstance.speak(text, interrupt)
|
||||||
|
|
||||||
def messagebox(text):
|
def messagebox(text, sounds=None):
|
||||||
"""Display a simple message box with text.
|
"""Enhanced messagebox with dialog support.
|
||||||
|
|
||||||
Shows a message that can be repeated until the user chooses to continue.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text (str): Message to display
|
text (str or dict): Simple string message or dialog configuration dict
|
||||||
|
sounds (Sound object, optional): Sound system for playing dialog audio
|
||||||
"""
|
"""
|
||||||
speech = Speech.get_instance()
|
speech = Speech.get_instance()
|
||||||
|
|
||||||
|
# Handle simple string (backward compatibility)
|
||||||
|
if isinstance(text, str):
|
||||||
|
_show_simple_message(speech, text)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle dialog format
|
||||||
|
if isinstance(text, dict) and "entries" in text:
|
||||||
|
_show_dialog_sequence(speech, text, sounds)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fallback to simple message if format not recognized
|
||||||
|
_show_simple_message(speech, str(text))
|
||||||
|
|
||||||
|
|
||||||
|
def _show_simple_message(speech, text):
|
||||||
|
"""Show a simple text message (original messagebox behavior)."""
|
||||||
speech.speak(text + "\nPress any key to repeat or enter to continue.")
|
speech.speak(text + "\nPress any key to repeat or enter to continue.")
|
||||||
while True:
|
while True:
|
||||||
event = pygame.event.wait()
|
event = pygame.event.wait()
|
||||||
@@ -147,3 +163,148 @@ def messagebox(text):
|
|||||||
speech.speak(" ")
|
speech.speak(" ")
|
||||||
return
|
return
|
||||||
speech.speak(text + "\nPress any key to repeat or enter to continue.")
|
speech.speak(text + "\nPress any key to repeat or enter to continue.")
|
||||||
|
|
||||||
|
|
||||||
|
def _show_dialog_sequence(speech, dialog_config, sounds):
|
||||||
|
"""Show a dialog sequence with character speech and optional sounds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
speech: Speech instance for text-to-speech
|
||||||
|
dialog_config (dict): Dialog configuration with entries list and optional settings
|
||||||
|
sounds: Sound system for playing audio files
|
||||||
|
"""
|
||||||
|
entries = dialog_config.get("entries", [])
|
||||||
|
allow_skip = dialog_config.get("allow_skip", False)
|
||||||
|
dialog_sound = dialog_config.get("sound", None)
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
return
|
||||||
|
|
||||||
|
entry_index = 0
|
||||||
|
while entry_index < len(entries):
|
||||||
|
entry = entries[entry_index]
|
||||||
|
|
||||||
|
# Play sound before showing dialog
|
||||||
|
_play_dialog_sound(entry, dialog_config, sounds)
|
||||||
|
|
||||||
|
# Format and show the dialog text
|
||||||
|
formatted_text = _format_dialog_entry(entry)
|
||||||
|
if not formatted_text:
|
||||||
|
entry_index += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Show dialog with appropriate controls (only on first entry)
|
||||||
|
if entry_index == 0:
|
||||||
|
if allow_skip:
|
||||||
|
control_text = "\nPress any key to repeat, enter for next, or escape to skip all."
|
||||||
|
else:
|
||||||
|
control_text = "\nPress any key to repeat or enter for next."
|
||||||
|
else:
|
||||||
|
control_text = "" # No instructions after first entry
|
||||||
|
|
||||||
|
speech.speak(formatted_text + control_text)
|
||||||
|
|
||||||
|
# Handle user input
|
||||||
|
while True:
|
||||||
|
event = pygame.event.wait()
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_ESCAPE:
|
||||||
|
if allow_skip:
|
||||||
|
speech.speak(" ")
|
||||||
|
return # Skip entire dialog sequence
|
||||||
|
else:
|
||||||
|
# Escape acts like enter if skip not allowed
|
||||||
|
speech.speak(" ")
|
||||||
|
entry_index += 1
|
||||||
|
break
|
||||||
|
elif event.key == pygame.K_RETURN:
|
||||||
|
speech.speak(" ")
|
||||||
|
entry_index += 1
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Repeat current entry (include instructions only on first entry)
|
||||||
|
repeat_text = formatted_text
|
||||||
|
if entry_index == 0:
|
||||||
|
repeat_text += control_text
|
||||||
|
speech.speak(repeat_text)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_dialog_entry(entry):
|
||||||
|
"""Format a dialog entry for display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry (dict): Dialog entry with text, optional speaker, and optional narrative flag
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted text for speech
|
||||||
|
"""
|
||||||
|
text = entry.get("text", "")
|
||||||
|
speaker = entry.get("speaker", None)
|
||||||
|
is_narrative = entry.get("narrative", False)
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if is_narrative:
|
||||||
|
# Narrative text - no speaker name
|
||||||
|
return text
|
||||||
|
elif speaker:
|
||||||
|
# Character dialog - include speaker name
|
||||||
|
return f"{speaker}: \"{text}\""
|
||||||
|
else:
|
||||||
|
# Plain text - no special formatting
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _play_dialog_sound(entry, dialog_config, sounds):
|
||||||
|
"""Play appropriate sound for a dialog entry and wait for it to complete.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry (dict): Dialog entry that may have a sound
|
||||||
|
dialog_config (dict): Dialog configuration that may have a default sound
|
||||||
|
sounds: Sound system (either Sound class instance or dictionary of sounds)
|
||||||
|
"""
|
||||||
|
if not sounds:
|
||||||
|
return
|
||||||
|
|
||||||
|
sound_to_play = None
|
||||||
|
|
||||||
|
# Determine which sound to play (priority order)
|
||||||
|
if entry.get("sound"):
|
||||||
|
# Entry-specific sound (highest priority)
|
||||||
|
sound_to_play = entry["sound"]
|
||||||
|
elif dialog_config.get("sound"):
|
||||||
|
# Dialog-specific sound (medium priority)
|
||||||
|
sound_to_play = dialog_config["sound"]
|
||||||
|
else:
|
||||||
|
# Default dialogue.ogg (lowest priority)
|
||||||
|
sound_to_play = "dialogue" # Will look for dialogue.ogg
|
||||||
|
|
||||||
|
if sound_to_play:
|
||||||
|
try:
|
||||||
|
# Handle both Sound class instances and sound dictionaries
|
||||||
|
if hasattr(sounds, 'sounds') and sound_to_play in sounds.sounds:
|
||||||
|
# Sound class instance (like from libstormgames Sound class)
|
||||||
|
sound_obj = sounds.sounds[sound_to_play]
|
||||||
|
from .sound import get_available_channel
|
||||||
|
channel = get_available_channel()
|
||||||
|
channel.play(sound_obj)
|
||||||
|
sound_duration = sound_obj.get_length()
|
||||||
|
if sound_duration > 0:
|
||||||
|
pygame.time.wait(int(sound_duration * 1000))
|
||||||
|
elif isinstance(sounds, dict) and sound_to_play in sounds:
|
||||||
|
# Dictionary of pygame sound objects (like from initialize_gui)
|
||||||
|
sound_obj = sounds[sound_to_play]
|
||||||
|
from .sound import get_available_channel
|
||||||
|
channel = get_available_channel()
|
||||||
|
channel.play(sound_obj)
|
||||||
|
sound_duration = sound_obj.get_length()
|
||||||
|
if sound_duration > 0:
|
||||||
|
pygame.time.wait(int(sound_duration * 1000))
|
||||||
|
elif hasattr(sounds, 'play'):
|
||||||
|
# Try using a play method if available
|
||||||
|
sounds.play(sound_to_play)
|
||||||
|
pygame.time.wait(500) # Default delay if can't get duration
|
||||||
|
except Exception:
|
||||||
|
# Sound missing or error - continue silently without crashing
|
||||||
|
pass
|
||||||
|
Reference in New Issue
Block a user