From e35c826b05bc0d1b8412fb36c7598d9c6bf2837f Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 11 Mar 2025 20:51:45 -0400 Subject: [PATCH] Sound code updated to support subdirectories of the sounds directory. Should help with organization. --- menu.py | 128 +++++++++++++++++++++++-------------------- sound.py | 163 ++++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 194 insertions(+), 97 deletions(-) diff --git a/menu.py b/menu.py index 59a1436..75345af 100644 --- a/menu.py +++ b/menu.py @@ -155,70 +155,78 @@ class Menu: elif selection == "exit": self.game.exit_game() - def learn_sounds(self): - """Interactive menu for learning game sounds. +def learn_sounds(self): + """Interactive menu for learning game sounds. - Allows users to: - - Navigate through available sounds - - Play selected sounds - - Return to menu with escape key + Allows users to: + - Navigate through available sounds + - Play selected sounds + - Return to menu with escape key - Returns: - str: "menu" if user exits with escape - """ - try: - self.game.sound.currentBgm.pause() - except: - pass + Returns: + str: "menu" if user exits with escape + """ + try: + self.game.sound.currentBgm.pause() + except: + pass - self.currentIndex = 0 + # Get list of available sounds, excluding music and ambiance directories + soundList = self.game.sound.get_sound_list() - # Get list of available sounds, excluding special sounds - soundFiles = [f for f in os.listdir("sounds/") - if isfile(join("sounds/", f)) - and (f.split('.')[1].lower() in ["ogg", "opus", "wav"]) - and (f.split('.')[0].lower() not in ["game-intro", "music_menu"]) - and (not f.lower().startswith("_"))] - - if not soundFiles: - self.game.speech.speak("No sounds available to learn.") + if not soundList: + self.game.speech.speak("No sounds available to learn.") + return "menu" + + # Sort sounds by name + soundList.sort() + + self.currentIndex = 0 + + validKeys = [ + pyglet.window.key.ESCAPE, + pyglet.window.key.RETURN, + pyglet.window.key.UP, + pyglet.window.key.DOWN, + pyglet.window.key.W, + pyglet.window.key.S + ] + + # Speak initial instructions + self.game.speech.speak("Learn game sounds. Use up and down arrow keys or W/S to navigate, Enter to play sound, Escape to exit.") + + # Speak initial sound name + soundName = soundList[self.currentIndex] + displayName = soundName.replace("/", " in folder ") + self.game.speech.speak(f"Sound 1 of {len(soundList)}: {displayName}") + + while True: + key, _ = self.game.wait(validKeys) + + if key == pyglet.window.key.ESCAPE: + try: + self.game.sound.currentBgm.play() + except: + pass return "menu" - validKeys = [ - pyglet.window.key.ESCAPE, - pyglet.window.key.RETURN, - pyglet.window.key.UP, - pyglet.window.key.DOWN, - pyglet.window.key.W, - pyglet.window.key.S - ] - - # Speak initial sound name - self.game.speech.speak(soundFiles[self.currentIndex][:-4]) - - while True: - key, _ = self.game.wait(validKeys) - - if key == pyglet.window.key.ESCAPE: - try: - self.game.sound.currentBgm.play() - except: - pass - return "menu" - - if key in [pyglet.window.key.DOWN, pyglet.window.key.S]: - if self.currentIndex < len(soundFiles) - 1: - self.game.sound.stop_all_sounds() - self.currentIndex += 1 - self.game.speech.speak(soundFiles[self.currentIndex][:-4]) - - if key in [pyglet.window.key.UP, pyglet.window.key.W]: - if self.currentIndex > 0: - self.game.sound.stop_all_sounds() - self.currentIndex -= 1 - self.game.speech.speak(soundFiles[self.currentIndex][:-4]) - - if key == pyglet.window.key.RETURN: - soundName = soundFiles[self.currentIndex][:-4] + if key in [pyglet.window.key.DOWN, pyglet.window.key.S]: + if self.currentIndex < len(soundList) - 1: self.game.sound.stop_all_sounds() - self.game.sound.play_sound(soundName) + self.currentIndex += 1 + soundName = soundList[self.currentIndex] + displayName = soundName.replace("/", " in folder ") + self.game.speech.speak(f"Sound {self.currentIndex + 1} of {len(soundList)}: {displayName}") + + if key in [pyglet.window.key.UP, pyglet.window.key.W]: + if self.currentIndex > 0: + self.game.sound.stop_all_sounds() + self.currentIndex -= 1 + soundName = soundList[self.currentIndex] + displayName = soundName.replace("/", " in folder ") + self.game.speech.speak(f"Sound {self.currentIndex + 1} of {len(soundList)}: {displayName}") + + if key == pyglet.window.key.RETURN: + soundName = soundList[self.currentIndex] + self.game.sound.stop_all_sounds() + self.game.sound.play_sound(soundName) diff --git a/sound.py b/sound.py index b2fcf92..1271682 100644 --- a/sound.py +++ b/sound.py @@ -5,6 +5,7 @@ Handles all audio functionality including: - Sound effects with 2D/3D positional audio - Volume control for master, BGM, and SFX - Audio loading and resource management +- Support for organizing sounds in subdirectories """ import os @@ -12,12 +13,15 @@ import pyglet import random import re import time -from os.path import isfile, join +from os.path import isfile, join, isdir from pyglet.window import key class Sound: """Handles audio playback and management.""" + # Directories to exclude from the learn sounds menu + excludedLearnDirs = ['music', 'ambiance'] + def __init__(self, game): """Initialize sound system. @@ -36,30 +40,74 @@ class Sound: self.currentBgm = None # Load sound resources - self.sounds = self._load_sounds() + self.sounds = {} # Dictionary of loaded sound objects + self.soundPaths = {} # Dictionary to track original paths + self.soundDirectories = {} # Track which directory each sound belongs to + self._load_sounds() + self.activeSounds = [] # Track playing sounds def _load_sounds(self): - """Load all sound files from sounds directory. - + """Load all sound files from sounds directory and subdirectories. + Returns: dict: Dictionary of loaded sound objects """ - sounds = {} - try: - soundFiles = [f for f in os.listdir("sounds/") - if isfile(join("sounds/", f)) - and f.lower().endswith(('.wav', '.ogg', '.opus'))] - for f in soundFiles: - name = os.path.splitext(f)[0] - sounds[name] = pyglet.media.load(f"sounds/{f}", streaming=False) - except FileNotFoundError: + if not os.path.exists("sounds/"): print("No sounds directory found") - return {} + return + + try: + # Walk through all subdirectories + for root, dirs, files in os.walk("sounds/"): + # Process sound files in this directory + for file in files: + if file.lower().endswith(('.wav', '.ogg', '.opus')): + # Get relative path from sounds directory + rel_path = os.path.relpath(os.path.join(root, file), "sounds/") + + # Extract name without extension + basename = os.path.splitext(file)[0] + + # Get directory relative to sounds folder + subdir = os.path.dirname(rel_path) + + # Create a sound key that maintains subdirectory structure if needed + if subdir: + sound_key = f"{subdir}/{basename}" + directory = subdir.split('/')[0] # Get top-level directory + else: + sound_key = basename + directory = "" + + # Full path to the sound file + fullPath = f"sounds/{rel_path}" + + # Load the sound + try: + self.sounds[sound_key] = pyglet.media.load(fullPath, streaming=False) + self.soundPaths[sound_key] = fullPath + self.soundDirectories[sound_key] = directory + except Exception as e: + print(f"Error loading sound {fullPath}: {e}") except Exception as e: print(f"Error loading sounds: {e}") + + def get_sound_list(self, excludeDirs=None): + """Get a list of available sounds, optionally excluding certain directories. - return sounds + Args: + excludeDirs (list): List of directory names to exclude + + Returns: + list: List of sound keys + """ + if excludeDirs is None: + excludeDirs = self.excludedLearnDirs + + # Filter sounds based on their directories + return [key for key, directory in self.soundDirectories.items() + if directory not in excludeDirs] def play_bgm(self, music_file): """Play background music with proper volume. @@ -97,13 +145,33 @@ class Sound: """Play a sound effect with volume settings. Args: - soundName (str): Name of sound to play + sound_name (str): Name of sound to play, can include subdirectory path + e.g. "explosion" or "monsters/growl" volume (float): Base volume for sound (0.0-1.0) Returns: pyglet.media.Player: Sound player object """ if soundName not in self.sounds: + # Try adding .ogg extension for direct file paths + if not soundName.endswith(('.wav', '.ogg', '.opus')): + # Try to find the sound with various extensions + for ext in ['.ogg', '.wav', '.opus']: + testName = f"{soundName}{ext}" + if os.path.exists(f"sounds/{testName}"): + try: + sound = pyglet.media.load(f"sounds/{testName}", streaming=False) + player = pyglet.media.Player() + player.queue(sound) + player.volume = volume * self.sfxVolume * self.masterVolume + player.play() + + self.activeSounds.append(player) + return player + except Exception: + pass + + print(f"Sound not found: {soundName}") return None player = pyglet.media.Player() @@ -114,16 +182,24 @@ class Sound: self.activeSounds.append(player) return player - def play_random(self, base_name, pause=False, interrupt=False): + def play_random(self, baseName, pause=False, interrupt=False): """Play random variation of a sound. Args: - base_name (str): Base name of sound + baseName (str): Base name of sound, can include subdirectory pause (bool): Wait for sound to finish interrupt (bool): Stop other sounds """ + # Check if baseName includes a directory + if '/' in baseName: + dirPart = os.path.dirname(baseName) + namePart = os.path.basename(baseName) + pattern = f"^{dirPart}/.*{namePart}.*" + else: + pattern = f"^{baseName}.*" + matches = [name for name in self.sounds.keys() - if re.match(f"^{base_name}.*", name)] + if re.match(pattern, name)] if not matches: return None @@ -169,25 +245,26 @@ class Sound: return (x, y, z) - def play_positional(self, soundName, source_pos, listener_pos, mode='2d', - direction=None, cone_angles=None): + def play_positional(self, soundName, sourcePos, listenerPos, mode='2d', + direction=None, coneAngles=None): """Play sound with positional audio. Args: - soundName (str): Name of sound to play - source_pos: Position of sound source (float for 2D, tuple for 3D) - listener_pos: Position of listener (float for 2D, tuple for 3D) + soundName (str): Name of sound to play, can include subdirectory + sourcePos: Position of sound source (float for 2D, tuple for 3D) + listenerPos: Position of listener (float for 2D, tuple for 3D) mode: '2d' or '3d' to specify positioning mode direction: Optional tuple (x,y,z) for directional sound - cone_angles: Optional tuple (inner, outer) angles for sound cone + coneAngles: Optional tuple (inner, outer) angles for sound cone Returns: pyglet.media.Player: Sound player object """ if soundName not in self.sounds: + print(f"Sound not found for positional audio: {soundName}") return None - position = self.calculate_positional_audio(source_pos, listener_pos, mode) + position = self.calculate_positional_audio(sourcePos, listenerPos, mode) if position is None: # Too far to hear return None @@ -199,29 +276,29 @@ class Sound: # Set up directional audio if specified if direction and mode == '3d': player.cone_orientation = direction - if cone_angles: - player.cone_inner_angle, player.cone_outer_angle = cone_angles + if coneAngles: + player.cone_inner_angle, player.cone_outer_angle = coneAngles player.cone_outer_gain = 0.5 # Reduced volume outside cone player.play() self.activeSounds.append(player) return player - def update_positional(self, player, source_pos, listener_pos, mode='2d', + def update_positional(self, player, sourcePos, listenerPos, mode='2d', direction=None): """Update position of a playing sound. Args: player: Sound player to update - source_pos: New source position - listener_pos: New listener position + sourcePos: New source position + listenerPos: New listener position mode: '2d' or '3d' positioning mode direction: Optional new direction for directional sound """ if not player or not player.playing: return - position = self.calculate_positional_audio(source_pos, listener_pos, mode) + position = self.calculate_positional_audio(sourcePos, listenerPos, mode) if position is None: player.pause() return @@ -231,25 +308,37 @@ class Sound: player.cone_orientation = direction def cut_scene(self, soundName): - """Play a sound as a cut scene, stopping other sounds and waiting for completion.""" + """Play a sound as a cut scene, stopping other sounds and waiting for completion. + + Args: + soundName (str): Name of sound to play, can include subdirectory + """ # Stop all current sounds self.stop_all_sounds() if self.currentBgm: self.currentBgm.pause() # Find all matching sound variations + if '/' in soundName: + dirPart = os.path.dirname(soundName) + namePart = os.path.basename(soundName) + pattern = f"^{dirPart}/.*{namePart}.*" + else: + pattern = f"^{soundName}.*" + matches = [name for name in self.sounds.keys() - if re.match(f"^{soundName}.*", name)] + if re.match(pattern, name)] if not matches: + print(f"No matching sounds found for cut scene: {soundName}") return # Pick a random variation - selected_sound = random.choice(matches) + selectedSound = random.choice(matches) # Create and configure the player player = pyglet.media.Player() - player.queue(self.sounds[selected_sound]) + player.queue(self.sounds[selectedSound]) player.volume = self.sfxVolume * self.masterVolume # Start playback @@ -257,7 +346,7 @@ class Sound: # Make sure to give pyglet enough cycles to start playing startTime = time.time() - duration = self.sounds[selected_sound].duration + duration = self.sounds[selectedSound].duration pyglet.clock.tick() # Wait for completion or skip