Compare commits
5 Commits
0bbf35a4c5
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
1a6ad65139 | ||
|
f84a115bc6 | ||
|
7555429433 | ||
|
9996cdc08b | ||
|
8c57afe65b |
@@ -46,9 +46,9 @@ def initialize_gui(gameTitle):
|
|||||||
|
|
||||||
# Initialize audio system if not already done
|
# Initialize audio system if not already done
|
||||||
if not pygame.mixer.get_init():
|
if not pygame.mixer.get_init():
|
||||||
pygame.mixer.pre_init(44100, -16, 2, 1536)
|
pygame.mixer.pre_init(44100, -16, 2, 4096)
|
||||||
pygame.mixer.init()
|
pygame.mixer.init()
|
||||||
pygame.mixer.set_num_channels(64)
|
pygame.mixer.set_num_channels(48)
|
||||||
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
|
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
|
||||||
|
|
||||||
# Enable key repeat for volume controls
|
# Enable key repeat for volume controls
|
||||||
|
128
sound.py
128
sound.py
@@ -19,6 +19,93 @@ from .services import VolumeService
|
|||||||
# Global instance for backward compatibility
|
# Global instance for backward compatibility
|
||||||
volumeService = VolumeService.get_instance()
|
volumeService = VolumeService.get_instance()
|
||||||
|
|
||||||
|
def find_silent_channels():
|
||||||
|
"""Find channels that are playing but with zero volume (effectively silent)."""
|
||||||
|
silent_channels = []
|
||||||
|
for n in range(pygame.mixer.get_num_channels()):
|
||||||
|
channel = pygame.mixer.Channel(n)
|
||||||
|
if channel.get_busy():
|
||||||
|
# Get current volume setting (single value for both channels)
|
||||||
|
volume = channel.get_volume()
|
||||||
|
# Consider silent if volume is effectively zero
|
||||||
|
if volume <= 0.001:
|
||||||
|
silent_channels.append(channel)
|
||||||
|
return silent_channels
|
||||||
|
|
||||||
|
def find_available_channel():
|
||||||
|
"""Find an available channel, prioritizing silent channels over stopping active ones."""
|
||||||
|
# Try to find an idle channel first
|
||||||
|
for n in range(pygame.mixer.get_num_channels()):
|
||||||
|
channel = pygame.mixer.Channel(n)
|
||||||
|
if not channel.get_busy():
|
||||||
|
return channel
|
||||||
|
|
||||||
|
# If no idle channels, look for silent ones to reclaim
|
||||||
|
silent_channels = find_silent_channels()
|
||||||
|
if silent_channels:
|
||||||
|
# Stop the first silent channel and return it
|
||||||
|
channel = silent_channels[0]
|
||||||
|
channel.stop()
|
||||||
|
return channel
|
||||||
|
|
||||||
|
# If no silent channels, stop channel 1 as last resort (avoid channel 0 - reserved for cutscenes)
|
||||||
|
# This ensures sounds always play rather than being silently dropped
|
||||||
|
channel = pygame.mixer.Channel(1)
|
||||||
|
channel.stop()
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def play_sound_with_retry(sound, loop=False, max_retries=3):
|
||||||
|
"""Play a sound with retry logic, returns (channel, success)."""
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
# Try normal playback first
|
||||||
|
channel = sound.play(-1 if loop else 0)
|
||||||
|
if channel:
|
||||||
|
return channel, True
|
||||||
|
|
||||||
|
# If failed, force allocation and try again
|
||||||
|
try:
|
||||||
|
channel = find_available_channel()
|
||||||
|
channel.play(sound, -1 if loop else 0)
|
||||||
|
if channel.get_busy(): # Verify it's actually playing
|
||||||
|
return channel, True
|
||||||
|
except Exception as e:
|
||||||
|
if attempt == max_retries - 1: # Last attempt
|
||||||
|
print(f"Sound playback failed after {max_retries} attempts: {e}")
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def get_channel_usage():
|
||||||
|
"""Return channel usage info for debugging."""
|
||||||
|
total = pygame.mixer.get_num_channels()
|
||||||
|
busy = sum(1 for n in range(total) if pygame.mixer.Channel(n).get_busy())
|
||||||
|
silent = len(find_silent_channels())
|
||||||
|
return f"Channels: {busy}/{total} busy ({silent} silent)"
|
||||||
|
|
||||||
|
def print_channel_debug():
|
||||||
|
"""Print detailed channel usage for debugging."""
|
||||||
|
total = pygame.mixer.get_num_channels()
|
||||||
|
print(f"Channel usage: {get_channel_usage()}")
|
||||||
|
|
||||||
|
busy_channels = []
|
||||||
|
silent_channels = []
|
||||||
|
|
||||||
|
for n in range(total):
|
||||||
|
channel = pygame.mixer.Channel(n)
|
||||||
|
if channel.get_busy():
|
||||||
|
volume = channel.get_volume()
|
||||||
|
if volume <= 0.001:
|
||||||
|
silent_channels.append(n)
|
||||||
|
else:
|
||||||
|
busy_channels.append(n)
|
||||||
|
|
||||||
|
if busy_channels:
|
||||||
|
print(f"Active channels: {busy_channels}")
|
||||||
|
if silent_channels:
|
||||||
|
print(f"Silent channels: {silent_channels}")
|
||||||
|
if not busy_channels and not silent_channels:
|
||||||
|
print("No busy channels")
|
||||||
|
|
||||||
class Sound:
|
class Sound:
|
||||||
"""Handles sound loading and playback."""
|
"""Handles sound loading and playback."""
|
||||||
|
|
||||||
@@ -29,9 +116,10 @@ class Sound:
|
|||||||
self.volumeService = volumeService or VolumeService.get_instance()
|
self.volumeService = volumeService or VolumeService.get_instance()
|
||||||
|
|
||||||
if not pygame.mixer.get_init():
|
if not pygame.mixer.get_init():
|
||||||
|
pygame.mixer.pre_init(44100, -16, 2, 4096)
|
||||||
pygame.mixer.init()
|
pygame.mixer.init()
|
||||||
pygame.mixer.set_num_channels(64)
|
pygame.mixer.set_num_channels(48)
|
||||||
pygame.mixer.set_reserved(0)
|
pygame.mixer.set_reserved(1) # Reserve channel 0 for cutscenes
|
||||||
|
|
||||||
self.load_sounds()
|
self.load_sounds()
|
||||||
|
|
||||||
@@ -108,6 +196,7 @@ class Sound:
|
|||||||
|
|
||||||
# Check if sound exists
|
# Check if sound exists
|
||||||
if soundName not in self.sounds:
|
if soundName not in self.sounds:
|
||||||
|
print(f"Sound not found: {soundName}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Handle cut scene mode
|
# Handle cut scene mode
|
||||||
@@ -119,9 +208,10 @@ class Sound:
|
|||||||
pygame.event.clear()
|
pygame.event.clear()
|
||||||
pygame.mixer.stop()
|
pygame.mixer.stop()
|
||||||
|
|
||||||
# Play the sound
|
# Play the sound with retry logic
|
||||||
channel = self.sounds[soundName].play(-1 if loop else 0)
|
channel, success = play_sound_with_retry(self.sounds[soundName], loop)
|
||||||
if not channel:
|
if not success:
|
||||||
|
print(f"Failed to play sound: {soundName}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Apply appropriate volume settings
|
# Apply appropriate volume settings
|
||||||
@@ -239,8 +329,12 @@ class Sound:
|
|||||||
if not soundName:
|
if not soundName:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Play the sound
|
# Play the sound with retry logic
|
||||||
channel = self.sounds[soundName].play()
|
channel, success = play_sound_with_retry(self.sounds[soundName], False)
|
||||||
|
if not success:
|
||||||
|
print(f"Failed to play falling sound: {soundName}")
|
||||||
|
return None
|
||||||
|
|
||||||
if channel:
|
if channel:
|
||||||
channel.set_volume(
|
channel.set_volume(
|
||||||
finalLeft * self.volumeService.sfxVolume,
|
finalLeft * self.volumeService.sfxVolume,
|
||||||
@@ -256,7 +350,7 @@ class Sound:
|
|||||||
pygame.mixer.music.set_volume(self.volumeService.get_bgm_volume())
|
pygame.mixer.music.set_volume(self.volumeService.get_bgm_volume())
|
||||||
pygame.mixer.music.play(-1)
|
pygame.mixer.music.play(-1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error playing background music: {e}")
|
print(f"Failed to load background music {musicFile}: {e}")
|
||||||
|
|
||||||
def adjust_master_volume(self, change):
|
def adjust_master_volume(self, change):
|
||||||
"""Adjust the master volume for all sounds."""
|
"""Adjust the master volume for all sounds."""
|
||||||
@@ -358,7 +452,8 @@ def play_bgm(musicFile):
|
|||||||
pygame.mixer.music.load(musicFile)
|
pygame.mixer.music.load(musicFile)
|
||||||
pygame.mixer.music.set_volume(volumeService.get_bgm_volume())
|
pygame.mixer.music.set_volume(volumeService.get_bgm_volume())
|
||||||
pygame.mixer.music.play(-1)
|
pygame.mixer.music.play(-1)
|
||||||
except: pass
|
except Exception as e:
|
||||||
|
print(f"Failed to load background music {musicFile}: {e}")
|
||||||
|
|
||||||
def adjust_master_volume(change):
|
def adjust_master_volume(change):
|
||||||
"""Adjust the master volume."""
|
"""Adjust the master volume."""
|
||||||
@@ -424,9 +519,10 @@ 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:
|
||||||
# Play the sound
|
# Play the sound with retry logic
|
||||||
channel = sounds[sound_or_name].play(-1 if loop else 0)
|
channel, success = play_sound_with_retry(sounds[sound_or_name], loop)
|
||||||
if not channel:
|
if not success:
|
||||||
|
print(f"Failed to play sound: {sound_or_name}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Apply volume settings
|
# Apply volume settings
|
||||||
@@ -521,8 +617,12 @@ def play_random_falling(sounds, soundName, playerX, objectX, startY, currentY=0,
|
|||||||
if not matched_sound:
|
if not matched_sound:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Play the sound
|
# Play the sound with retry logic
|
||||||
channel = sounds[matched_sound].play()
|
channel, success = play_sound_with_retry(sounds[matched_sound], False)
|
||||||
|
if not success:
|
||||||
|
print(f"Failed to play falling sound: {matched_sound}")
|
||||||
|
return None
|
||||||
|
|
||||||
if channel:
|
if channel:
|
||||||
channel.set_volume(
|
channel.set_volume(
|
||||||
finalLeft * volumeService.sfxVolume,
|
finalLeft * volumeService.sfxVolume,
|
||||||
|
166
speech.py
166
speech.py
@@ -130,15 +130,34 @@ 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.
|
"""Display a message box with text and optional dialog support.
|
||||||
|
|
||||||
Shows a message that can be repeated until the user chooses to continue.
|
Shows a message that can be repeated until the user chooses to continue.
|
||||||
|
Supports both simple text messages and dialog sequences with character speech and sounds.
|
||||||
|
|
||||||
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 +166,144 @@ 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]
|
||||||
|
channel = sound_obj.play()
|
||||||
|
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]
|
||||||
|
channel = sound_obj.play()
|
||||||
|
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