Compare commits

...

10 Commits

6 changed files with 915 additions and 90 deletions

643
README.md Normal file
View File

@ -0,0 +1,643 @@
# PygStormGames Library
A Python framework for creating accessible audio games with Pyglet.
## Overview
PygStormGames is a specialized game framework built on top of Pyglet, designed specifically for creating audio-focused games with accessibility in mind. The library provides a complete set of tools for building games with rich audio feedback, text-to-speech integration, menu systems, and more.
## Core Features
- **Accessible Interface**: Built-in text-to-speech support and screen reader compatibility
- **Audio Management**: Spatial audio, sound effects, and background music
- **Menu System**: Fully accessible menu with navigation sounds
- **Configuration**: Local and global settings management
- **Scoreboard**: High score tracking and persistence
- **Text Display**: Accessible text display with navigation
## Installation
PygStormGames requires Python 3.6+ and depends on the following libraries:
- pyglet
- pyperclip
- wx (wxPython)
- xdg
- speechd or accessible_output2 (for text-to-speech)
## Getting Started
### Basic Game Structure
```python
from pygstormgames import pygstormgames
class MyGame:
def __init__(self):
# Initialize the game with a title
self.game = pygstormgames("My Audio Game")
# Set up game-specific variables and components
self.score = 0
self.gameActive = False
# Set up the main game loop
pyglet.clock.schedule_interval(self.update, 1/60.0)
def update(self, dt):
# Main game update loop
if self.gameActive:
# Update game state
pass
def start(self):
# Show main menu
selection = self.game.menu.game_menu()
if selection == "play":
# Start the game
self.gameActive = True
self.game.run()
if __name__ == "__main__":
game = MyGame()
game.start()
```
## Main Components
### pygstormgames Class
The main class that coordinates all game systems. Initialize with the game title.
```python
game = pygstormgames("My Game Title")
```
#### Methods
- `run()`: Start the game loop
- `wait(validKeys=None, timeout=None)`: Wait for key press(es) with optional timeout
- `wait_for_completion(condition, validKeys=None, timeout=None)`: Wait for a condition to be met, keys to be pressed, or timeout
- `pause_game()`: Pause all game systems
- `exit_game()`: Clean up and exit the game
### Config
Handles loading and saving of both local and global game configurations.
```python
# Get a config value (section, key, default value, global or local)
volume = game.config.get_float("audio", "master_volume", 0.8)
# Save a config value
game.config.set_value("audio", "master_volume", 0.9)
```
#### Methods
- `get_value(section, key, default=None, globalConfig=False)`: Get string value
- `set_value(section, key, value, globalConfig=False)`: Set value
- `get_int(section, key, default=0, globalConfig=False)`: Get integer value
- `get_float(section, key, default=0.0, globalConfig=False)`: Get float value
- `get_bool(section, key, default=False, globalConfig=False)`: Get boolean value
### Display
Handles text display, navigation, and information presentation.
```python
# Display text with speech output
game.display.display_text(textLines, game.speech)
# Show a message box
game.display.messagebox("Game over! Your score: 1000")
# Get text input from the user
name = game.display.get_input("Enter your name:", "Player")
```
#### Methods
- `display_text(text, speech)`: Display and navigate text with speech output
- `instructions(speech)`: Display game instructions from file
- `credits(speech)`: Display game credits from file
- `messagebox(text)`: Display a simple message box with text
- `get_input(prompt="Enter text:", defaultText="")`: Display a dialog box for text input
- `donate(speech)`: Open the donation webpage
### Menu
Handles main menu and submenu functionality for games.
```python
# Show a menu and get selection
options = ["start", "options", "exit"]
selection = game.menu.show_menu(options, "Main Menu")
# Show the default game menu
selection = game.menu.game_menu()
```
#### Methods
- `show_menu(options, title=None, with_music=False)`: Display a menu and return selected option
- `game_menu()`: Show main game menu
- `learn_sounds()`: Interactive menu for learning game sounds
### Sound
Handles all audio functionality including background music, sound effects, and volume control.
```python
# Play a sound effect
game.sound.play_sound("explosion")
# Play background music
game.sound.play_bgm("sounds/music_level1.ogg")
# Adjust volume
game.sound.adjust_master_volume(0.1) # Increase by 10%
```
#### Methods
- `play_sound(soundName, volume=1.0)`: Play a sound effect
- `play_bgm(music_file)`: Play background music
- `pause_bgm()`: Pause background music
- `resume_bgm()`: Resume background music
- `play_random(base_name, pause=False, interrupt=False)`: Play random variation of a sound
- `play_positional(soundName, source_pos, listener_pos, mode='2d')`: Play sound with positional audio
- `update_positional(player, source_pos, listener_pos, mode='2d')`: Update position of a playing sound
- `cut_scene(soundName)`: Play a sound as a cut scene
- `adjust_master_volume(change)`: Adjust master volume
- `adjust_bgm_volume(change)`: Adjust background music volume
- `adjust_sfx_volume(change)`: Adjust sound effects volume
- `get_volumes()`: Get current volume levels
- `pause()`: Pause all audio
- `resume()`: Resume all audio
- `stop_all_sounds()`: Stop all playing sounds
### Speech
Provides text-to-speech functionality with screen text display support.
```python
# Speak text
game.speech.speak("Welcome to my game!")
# Speak with no interruption
game.speech.speak("This will wait for previous speech to finish", interrupt=False)
```
#### Methods
- `speak(text, interrupt=True)`: Speak text and display it on screen
### Scoreboard
Handles high score tracking with player names and score management.
```python
# Increase the score
game.scoreboard.increase_score(100)
# Check and add high score if qualified
if game.scoreboard.check_high_score():
game.scoreboard.add_high_score()
# Get high scores
highScores = game.scoreboard.get_high_scores()
```
#### Methods
- `get_score()`: Get current score
- `get_high_scores()`: Get list of high scores
- `decrease_score(points=1)`: Decrease the current score
- `increase_score(points=1)`: Increase the current score
- `check_high_score()`: Check if current score qualifies as a high score
- `add_high_score()`: Add current score to high scores if it qualifies
## Key Event Handling
PygStormGames provides a simple way to handle keyboard input:
```python
# Wait for any key
key, modifiers = game.wait()
# Wait for specific keys
validKeys = [pyglet.window.key.SPACE, pyglet.window.key.RETURN]
key, modifiers = game.wait(validKeys)
# Wait with timeout (5 seconds)
key, modifiers = game.wait(timeout=5.0)
# Check for specific key
if key == pyglet.window.key.SPACE:
# Space was pressed
pass
```
## Directory Structure
PygStormGames expects the following directory structure:
```
your_game/
â __init__.py
â main.py
â sounds/ # Sound effects and music
â â music_menu.ogg
â â game-intro.ogg
â â ...
â files/ # Game text files
â instructions.txt
â credits.txt
```
### Required Sound Files
PygStormGames looks for these sound files, though they're optional:
- `game-intro.ogg`: Played when the game starts
- `music_menu.ogg`: Played during menu navigation
- `menu-move.ogg`: Played when navigating menu items
- `menu-select.ogg`: Played when selecting a menu item
## Advanced Usage Examples
### Creating a Custom Menu
```python
def settings_menu(self):
options = ["sound volume", "music volume", "speech rate", "back"]
while True:
selection = self.game.menu.show_menu(options, "Settings")
if selection == "sound volume":
self.adjust_sound_volume()
elif selection == "music volume":
self.adjust_music_volume()
elif selection == "speech rate":
self.adjust_speech_rate()
elif selection == "back":
return
```
### Playing Positional Audio
```python
# For 2D games (x-position only)
def update_enemy_sounds(self):
for enemy in self.enemies:
# Player at x=0, enemy at some x position
self.game.sound.play_positional(
"enemy-sound",
enemy.x, # Source position
self.player.x, # Listener position
mode='2d'
)
# For 3D games
def update_enemy_sounds_3d(self):
for enemy in self.enemies:
# 3D positions (x, y, z)
source_pos = (enemy.x, enemy.y, enemy.z)
listener_pos = (self.player.x, self.player.y, self.player.z)
self.game.sound.play_positional(
"enemy-sound",
source_pos,
listener_pos,
mode='3d'
)
```
### Creating a Countdown Timer
```python
def start_countdown(self):
for i in range(3, 0, -1):
self.game.speech.speak(str(i))
self.game.sound.play_sound(f"countdown-{i}")
# Wait 1 second
self.game.wait(timeout=1.0)
self.game.speech.speak("Go!")
self.game.sound.play_sound("start")
self.gameActive = True
```
### Saving and Loading Game Progress
```python
def save_game(self):
# Save player progress
self.game.config.set_value("save", "level", self.currentLevel)
self.game.config.set_value("save", "score", self.score)
self.game.config.set_value("save", "health", self.playerHealth)
self.game.speech.speak("Game saved.")
def load_game(self):
# Load player progress
self.currentLevel = self.game.config.get_int("save", "level", 1)
self.score = self.game.config.get_int("save", "score", 0)
self.playerHealth = self.game.config.get_int("save", "health", 100)
self.game.speech.speak(f"Game loaded. Level {self.currentLevel}")
```
## Common Patterns
### Game Loop with Pause Support
```python
def update(self, dt):
# Skip if game is paused
if self.game._paused:
return
# Update game state
self.update_player(dt)
self.update_enemies(dt)
self.check_collisions()
self.update_score()
```
### Handling Game Over
```python
def game_over(self):
# Stop gameplay sounds
self.game.sound.stop_all_sounds()
# Play game over sound
self.game.sound.play_sound("game-over")
# Display final score
finalScore = self.game.scoreboard.get_score()
self.game.speech.speak(f"Game over! Your score: {finalScore}")
# Check for high score
if self.game.scoreboard.check_high_score():
self.game.scoreboard.add_high_score()
# Wait for key press
self.game.display.messagebox("Game over!")
# Return to main menu
self.game.menu.game_menu()
```
### Creating a Timed Event System
```python
class TimedEvent:
def __init__(self, time, callback):
self.triggerTime = time
self.callback = callback
self.triggered = False
class EventSystem:
def __init__(self, game):
self.game = game
self.events = []
self.gameTime = 0
def add_event(self, delay, callback):
triggerTime = self.gameTime + delay
self.events.append(TimedEvent(triggerTime, callback))
def update(self, dt):
self.gameTime += dt
for event in self.events:
if not event.triggered and self.gameTime >= event.triggerTime:
event.callback()
event.triggered = True
# Remove triggered events
self.events = [e for e in self.events if not e.triggered]
# Usage
events = EventSystem(game)
events.add_event(5.0, lambda: game.speech.speak("Warning! Enemy approaching!"))
```
## Tips and Best Practices
1. **Audio Cues**: Always provide clear audio feedback for important actions and events.
2. **Speech Rate**: Keep speech concise and prioritize critical information.
3. **Sound Balance**: Maintain a good balance between speech, sound effects, and music.
4. **Consistent Controls**: Keep controls consistent and provide easy ways to learn them.
5. **Performance**: Be mindful of audio channel usage, especially with spatial audio.
6. **Documentation**: Include clear instructions for controls and gameplay.
7. **File Structure**: Follow the expected file structure for sounds and text files.
## Troubleshooting
### Common Issues
1. **No speech output**
- Ensure speechd or accessible_output2 is installed
2. **Sound files not playing**
- Verify file paths and formats (ogg or wav)
- Check if sound files exist in the correct directory
- Check volume settings in the game
3. **Menus not responding**
- Ensure pyglet event loop is running
- Check if key handlers are properly registered
### Debugging Tips
- Use `print` statements to track game state
- Enable more verbose logging in the speech module
- Test with minimal sound files to isolate issues
## Complete Example Game
Here's a simple example of a complete game that demonstrates the key features of PygStormGames:
```python
import pyglet
from pygstormgames import pygstormgames
import random
import math
class SimpleGame:
def __init__(self):
# Initialize game
self.game = pygstormgames("Sound Hunter")
# Game variables
self.playerX = 0
self.playerY = 0
self.targetX = 0
self.targetY = 0
self.score = 0
self.timeLeft = 60
self.gameActive = False
# Set up game loop
pyglet.clock.schedule_interval(self.update, 1/60.0)
def start_game(self):
# Reset game state
self.score = 0
self.timeLeft = 60
self.gameActive = True
self.game.scoreboard.currentScore = 0
# Place target
self.place_new_target()
# Start background music
self.game.sound.play_bgm("sounds/game_music.ogg")
# Announce game start
self.game.speech.speak("Game started. Use arrow keys to find the target.")
def place_new_target(self):
# Place target randomly within range
distance = random.uniform(5, 15)
angle = random.uniform(0, 2 * math.pi)
self.targetX = math.cos(angle) * distance
self.targetY = math.sin(angle) * distance
# Play new target sound
self.game.sound.play_sound("new-target")
def update(self, dt):
if not self.gameActive or self.game._paused:
return
# Update timer
self.timeLeft -= dt
if self.timeLeft <= 0:
self.game_over()
return
# Play positional target sound every second
if int(self.timeLeft) != int(self.timeLeft - dt):
self.game.sound.play_positional(
"target-ping",
self.targetX,
self.playerX,
mode='2d'
)
# Calculate distance to target
distance = math.sqrt((self.playerX - self.targetX)**2 +
(self.playerY - self.targetY)**2)
# Check for target collection
if distance < 1.0:
self.collect_target()
def on_key_press(self, symbol, modifiers):
if not self.gameActive:
return
# Move player
if symbol == pyglet.window.key.UP:
self.playerY += 1
elif symbol == pyglet.window.key.DOWN:
self.playerY -= 1
elif symbol == pyglet.window.key.LEFT:
self.playerX -= 1
elif symbol == pyglet.window.key.RIGHT:
self.playerX += 1
# Announce position occasionally
if random.random() < 0.1:
self.announce_position()
def announce_position(self):
# Calculate distance and direction to target
dx = self.targetX - self.playerX
dy = self.targetY - self.playerY
distance = math.sqrt(dx*dx + dy*dy)
# Create directional hint
if abs(dx) > abs(dy):
direction = "east" if dx > 0 else "west"
else:
direction = "north" if dy > 0 else "south"
# Announce distance category
if distance < 2.0:
proximity = "very close"
elif distance < 5.0:
proximity = "close"
elif distance < 10.0:
proximity = "medium distance"
else:
proximity = "far away"
self.game.speech.speak(f"Target is {proximity} to the {direction}")
def collect_target(self):
# Play collection sound
self.game.sound.play_sound("target-collect")
# Update score
self.score += 1
self.game.scoreboard.increase_score(100)
# Announce collection
self.game.speech.speak(f"Target collected! Score: {self.score}")
# Place new target
self.place_new_target()
# Add time bonus
self.timeLeft += 5
def game_over(self):
self.gameActive = False
self.game.sound.stop_all_sounds()
self.game.sound.play_sound("game-over")
finalScore = self.game.scoreboard.get_score()
self.game.speech.speak(f"Game over! Final score: {finalScore}")
# Check for high score
self.game.scoreboard.add_high_score()
# Return to menu after delay
self.game.wait(timeout=3.0)
self.start()
def start(self):
# Register key handlers
self.game.display.window.push_handlers(self.on_key_press)
# Show main menu
while True:
selection = self.game.menu.game_menu()
if selection == "play":
self.start_game()
break
elif selection == "exit":
self.game.exit_game()
break
# Start game loop
self.game.run()
if __name__ == "__main__":
game = SimpleGame()
game.start()
```
This example demonstrates using menus, speech, positional audio, and scoreboards in a simple audio-based game where the player needs to find targets using audio cues.

View File

@ -89,46 +89,55 @@ class pygstormgames:
self.speech.cleanup()
pyglet.app.exit()
def wait(self, validKeys=None):
"""Wait for key press(es).
def wait(self, validKeys=None, timeout=None):
"""Wait for key press(es) with optional timeout.
Args:
validKeys (list, optional): List of pyglet.window.key values to wait for.
If None, accepts any key press.
timeout (float, optional): Time in seconds to wait for input.
If None, waits indefinitely.
Returns:
tuple: (key, modifiers) that were pressed
tuple: (key, modifiers) that were pressed. Returns (None, None) on timeout.
"""
keyResult = [None, None] # Use list to allow modification in closure
start_time = time.time()
def on_key_press(symbol, modifiers):
if validKeys is None or symbol in validKeys:
keyResult[0] = symbol
keyResult[1] = modifiers
return pyglet.event.EVENT_HANDLED
# Register temporary handler
self.display.window.push_handlers(on_key_press=on_key_press)
# Wait for valid key press
# Wait for valid key press or timeout
while keyResult[0] is None:
self.display.window.dispatch_events()
pyglet.clock.tick()
# Check for timeout
if timeout is not None and time.time() - start_time > timeout:
break
# Clean up
self.display.window.remove_handlers()
return tuple(keyResult)
def wait_for_completion(self, condition, validKeys=None):
"""Wait for either a condition to be met or valid keys to be pressed.
def wait_for_completion(self, condition, validKeys=None, timeout=None):
"""Wait for either a condition to be met, valid keys to be pressed, or timeout.
Args:
condition: Function that returns True when waiting should end
validKeys (list, optional): List of pyglet.window.key values that can interrupt
If None, uses ESCAPE, RETURN, and SPACE
timeout (float, optional): Time in seconds to wait before timing out
If None, waits indefinitely
Returns:
bool: True if interrupted by key press, False if condition was met
bool: True if interrupted by key press or timeout, False if condition was met
"""
if validKeys is None:
validKeys = [pyglet.window.key.ESCAPE,
@ -136,18 +145,24 @@ class pygstormgames:
pyglet.window.key.SPACE]
interrupted = [False] # Use list to allow modification in closure
start_time = time.time()
def on_key_press(symbol, modifiers):
if symbol in validKeys:
interrupted[0] = True
return pyglet.event.EVENT_HANDLED
self.display.window.push_handlers(on_key_press=on_key_press)
while not condition() and not interrupted[0]:
self.display.window.dispatch_events()
pyglet.clock.tick()
# Check for timeout
if timeout is not None and time.time() - start_time > timeout:
interrupted[0] = True
break
self.display.window.remove_handlers()
return interrupted[0]

View File

@ -37,6 +37,8 @@ class Display:
text (list): List of text lines to display
speech (Speech): Speech system for audio output
"""
# Stop bgm if present.
self.game.sound.pause_bgm()
# Store original text with blank lines for copying
self.originalText = text.copy()
@ -152,22 +154,31 @@ class Display:
def get_input(self, prompt="Enter text:", defaultText=""):
"""Display a dialog box for text input.
Args:
prompt (str): Prompt text to display
defaultText (str): Initial text in input box
Returns:
str: User input text, or None if cancelled
"""
app = wx.App(False)
dialog = wx.TextEntryDialog(None, prompt, "Input", defaultText)
dialog.SetValue(defaultText)
# Bring dialog to front and give it focus
dialog.Raise()
dialog.SetFocus()
if dialog.ShowModal() == wx.ID_OK:
userInput = dialog.GetValue()
else:
userInput = None
dialog.Destroy()
# Return focus to game window
self.window.activate()
return userInput
def donate(self, speech):

38
menu.py
View File

@ -26,7 +26,7 @@ class Menu:
try:
if self.game.sound.currentBgm:
self.game.sound.currentBgm.pause()
self.game.sound.play_bgm("sounds/music_menu.ogg")
self.game.sound.play_bgm("music_menu")
except:
pass
@ -171,19 +171,18 @@ class Menu:
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", "wav"])
and (f.split('.')[0].lower() not in ["game-intro", "music_menu"])
and (not f.lower().startswith("_"))]
if not soundFiles:
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,
@ -193,8 +192,13 @@ class Menu:
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
self.game.speech.speak(soundFiles[self.currentIndex][:-4])
soundName = soundList[self.currentIndex]
displayName = soundName.replace("/", " in folder ")
self.game.speech.speak(f"{displayName}: 1 of {len(soundList)}")
while True:
key, _ = self.game.wait(validKeys)
@ -207,18 +211,22 @@ class Menu:
return "menu"
if key in [pyglet.window.key.DOWN, pyglet.window.key.S]:
if self.currentIndex < len(soundFiles) - 1:
if self.currentIndex < len(soundList) - 1:
self.game.sound.stop_all_sounds()
self.currentIndex += 1
self.game.speech.speak(soundFiles[self.currentIndex][:-4])
soundName = soundList[self.currentIndex]
displayName = soundName.replace("/", " in folder ")
self.game.speech.speak(f"{displayName}: {self.currentIndex + 1} of {len(soundList)}")
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])
soundName = soundList[self.currentIndex]
displayName = soundName.replace("/", " in folder ")
self.game.speech.speak(f"{displayName}: {self.currentIndex + 1} of {len(soundList)}")
if key == pyglet.window.key.RETURN:
soundName = soundFiles[self.currentIndex][:-4]
soundName = soundList[self.currentIndex]
self.game.sound.stop_all_sounds()
self.game.sound.play_sound(soundName)

View File

@ -3,6 +3,7 @@
Handles high score tracking with player names and score management.
"""
import pyglet
import time
class Scoreboard:
@ -93,34 +94,40 @@ class Scoreboard:
def add_high_score(self):
"""Add current score to high scores if it qualifies.
Returns:
bool: True if score was added, False if not
"""
position = self.check_high_score()
if position is None:
return False
# Get player name
self.game.speech.speak("New high score! Enter your name:")
name = self.game.display.get_input("New high score! Enter your name:", "Player")
if name is None: # User cancelled
name = "Player"
# Insert new score at correct position
self.highScores.insert(position - 1, {
'name': name,
'score': self.currentScore
})
# Keep only top 10
self.highScores = self.highScores[:10]
# Save to config
for i, entry in enumerate(self.highScores):
self.game.config.set_value("scoreboard", f"score_{i+1}", str(entry['score']))
self.game.config.set_value("scoreboard", f"name_{i+1}", entry['name'])
self.game.speech.speak(f"Congratulations {name}! You got position {position} on the scoreboard!")
# Force window refresh after dialog
self.game.display.window.dispatch_events()
self.game.display.messagebox(f"Congratulations {name}! You got position {position} on the scoreboard!")
time.sleep(1)
# Make sure window is still responsive
self.game.display.window.dispatch_events()
return True

249
sound.py
View File

@ -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,50 +40,139 @@ 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'))]
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}")
return sounds
def play_bgm(self, music_file):
"""Play background music with proper volume.
def get_sound_list(self, excludeDirs=None, includeSpecial=False):
"""Get a list of available sounds, optionally excluding certain directories and special sounds.
Args:
music_file (str): Path to music file
excludeDirs (list): List of directory names to exclude
includeSpecial (bool): Whether to include special sounds (logo, music_menu, etc.)
Returns:
list: List of sound keys
"""
if excludeDirs is None:
excludeDirs = self.excludedLearnDirs
# Special sound names to exclude unless specifically requested
specialSounds = ["music_menu", "logo", "game-intro"]
# Filter sounds based on their directories and special names
filtered_sounds = []
for key, directory in self.soundDirectories.items():
# Skip sounds in excluded directories
if directory in excludeDirs:
continue
# Skip special sounds unless specifically requested
basename = key.split('/')[-1] # Get the filename without directory
if not includeSpecial and (basename in specialSounds or basename.startswith('_')):
continue
filtered_sounds.append(key)
return filtered_sounds
def play_bgm(self, music_name):
"""Play background music with proper volume and looping.
Args:
music_name (str): Name of the music file in the sounds directory
Can be just the name (e.g., "music_menu")
or include subdirectory (e.g., "music/title")
"""
try:
# Clean up old player
if self.currentBgm:
self.currentBgm.pause()
# Load and play new music
music = pyglet.media.load(music_file, streaming=True)
player = pyglet.media.Player()
player.queue(music)
player.loop = True
player.volume = self.bgmVolume * self.masterVolume
player.play()
self.currentBgm = None
# Check if the music is in the loaded sounds library
if music_name in self.sounds:
# Use the already loaded sound
music = self.sounds[music_name]
music_file = None
else:
# Try to load with extensions if not found
for ext in ['.ogg', '.wav', '.opus']:
if music_name.endswith(ext):
music_file = f"sounds/{music_name}"
break
elif os.path.exists(f"sounds/{music_name}{ext}"):
music_file = f"sounds/{music_name}{ext}"
break
# If we didn't find a file, try the direct path
if not music_file:
music_file = f"sounds/{music_name}"
if not os.path.exists(music_file):
music_file += ".ogg" # Default to .ogg if no extension
# Load the music file
music = pyglet.media.load(music_file, streaming=True)
# Create and configure player
player = pyglet.media.Player()
player.volume = self.bgmVolume * self.masterVolume
player.queue(music)
player.play()
player.on_eos = lambda: (player.queue(music), player.play())
# Store reference
self.currentBgm = player
except Exception as e:
print(f"Error playing background music: {e}")
@ -97,13 +190,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 +227,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 +290,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 +321,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,28 +353,47 @@ 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()
if soundName not in self.sounds:
# 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(pattern, name)]
if not matches:
print(f"No matching sounds found for cut scene: {soundName}")
return
# Pick a random variation
selectedSound = random.choice(matches)
# Create and configure the player
player = pyglet.media.Player()
player.queue(self.sounds[soundName])
player.queue(self.sounds[selectedSound])
player.volume = self.sfxVolume * self.masterVolume
# Start playback
player.play()
# Make sure to give pyglet enough cycles to start playing
startTime = time.time()
duration = self.sounds[soundName].duration
duration = self.sounds[selectedSound].duration
pyglet.clock.tick()
# Wait for completion or skip
interrupted = self.game.wait_for_completion(
lambda: not player.playing or (time.time() - startTime) >= duration