pygstormgames/sound.py

358 lines
12 KiB
Python

"""Sound management module for PygStormGames.
Handles all audio functionality including:
- Background music playback
- Sound effects with 2D/3D positional audio
- Volume control for master, BGM, and SFX
- Audio loading and resource management
"""
import os
import pyglet
import random
import re
import time
from os.path import isfile, join
from pyglet.window import key
class Sound:
"""Handles audio playback and management."""
def __init__(self, game):
"""Initialize sound system.
Args:
game (PygStormGames): Reference to main game object
"""
# Game reference for component access
self.game = game
# Volume control (0.0 - 1.0)
self.bgmVolume = 0.75 # Background music
self.sfxVolume = 1.0 # Sound effects
self.masterVolume = 1.0 # Master volume
# Current background music
self.currentBgm = None
# Load sound resources
self.sounds = self._load_sounds()
self.activeSounds = [] # Track playing sounds
def _load_sounds(self):
"""Load all sound files from sounds directory.
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:
print("No sounds directory found")
return {}
except Exception as e:
print(f"Error loading sounds: {e}")
return sounds
def play_bgm(self, music_file):
"""Play background music with proper volume.
Args:
music_file (str): Path to music file
"""
try:
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 = player
except Exception as e:
print(f"Error playing background music: {e}")
def pause_bgm(self):
"""Pause background music."""
if self.currentBgm and self.currentBgm.playing:
self.currentBgm.pause()
def resume_bgm(self):
"""Resume background music from paused state."""
if self.currentBgm and not self.currentBgm.playing:
self.currentBgm.play()
def play_sound(self, soundName, volume=1.0):
"""Play a sound effect with volume settings.
Args:
soundName (str): Name of sound to play
volume (float): Base volume for sound (0.0-1.0)
Returns:
pyglet.media.Player: Sound player object
"""
if soundName not in self.sounds:
return None
player = pyglet.media.Player()
player.queue(self.sounds[soundName])
player.volume = volume * self.sfxVolume * self.masterVolume
player.play()
self.activeSounds.append(player)
return player
def play_random(self, base_name, pause=False, interrupt=False):
"""Play random variation of a sound.
Args:
base_name (str): Base name of sound
pause (bool): Wait for sound to finish
interrupt (bool): Stop other sounds
"""
matches = [name for name in self.sounds.keys()
if re.match(f"^{base_name}.*", name)]
if not matches:
return None
if interrupt:
self.stop_all_sounds()
soundName = random.choice(matches)
player = self.play_sound(soundName)
if pause and player:
player.on_player_eos = lambda: None # Wait for completion
def calculate_positional_audio(self, source_pos, listener_pos, mode='2d'):
"""Calculate position for 3D audio.
Args:
source_pos: Either float (2D x-position) or tuple (3D x,y,z position)
listener_pos: Either float (2D x-position) or tuple (3D x,y,z position)
mode: '2d' or '3d' to specify positioning mode
Returns:
tuple: (x, y, z) position for sound source, or None if out of range
"""
if mode == '2d':
distance = abs(source_pos - listener_pos)
max_distance = 12
if distance > max_distance:
return None
return (source_pos - listener_pos, 0, -1)
else:
x = source_pos[0] - listener_pos[0]
y = source_pos[1] - listener_pos[1]
z = source_pos[2] - listener_pos[2]
distance = (x*x + y*y + z*z) ** 0.5
max_distance = 20 # Larger for 3D space
if distance > max_distance:
return None
return (x, y, z)
def play_positional(self, soundName, source_pos, listener_pos, mode='2d',
direction=None, cone_angles=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)
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
Returns:
pyglet.media.Player: Sound player object
"""
if soundName not in self.sounds:
return None
position = self.calculate_positional_audio(source_pos, listener_pos, mode)
if position is None: # Too far to hear
return None
player = pyglet.media.Player()
player.queue(self.sounds[soundName])
player.position = position
player.volume = self.sfxVolume * self.masterVolume
# 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
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',
direction=None):
"""Update position of a playing sound.
Args:
player: Sound player to update
source_pos: New source position
listener_pos: 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)
if position is None:
player.pause()
return
player.position = position
if direction and mode == '3d':
player.cone_orientation = direction
def cut_scene(self, soundName):
"""Play a sound as a cut scene, stopping other sounds and waiting for completion."""
# Stop all current sounds
self.stop_all_sounds()
if self.currentBgm:
self.currentBgm.pause()
# Find all matching sound variations
matches = [name for name in self.sounds.keys()
if re.match(f"^{soundName}.*", name)]
if not matches:
return
# Pick a random variation
selected_sound = random.choice(matches)
# Create and configure the player
player = pyglet.media.Player()
player.queue(self.sounds[selected_sound])
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[selected_sound].duration
pyglet.clock.tick()
# Wait for completion or skip
interrupted = self.game.wait_for_completion(
lambda: not player.playing or (time.time() - startTime) >= duration
)
# Ensure cleanup
if interrupted:
player.pause()
player.delete()
# Resume background music if it was playing
if self.currentBgm:
self.currentBgm.play()
def adjust_master_volume(self, change):
"""Adjust master volume.
Args:
change (float): Volume change (-1.0 to 1.0)
"""
if not -1.0 <= change <= 1.0:
return
self.masterVolume = max(0.0, min(1.0, self.masterVolume + change))
# Update BGM
if self.currentBgm:
self.currentBgm.volume = self.bgmVolume * self.masterVolume
# Update active sounds
for sound in self.activeSounds:
if sound.playing:
sound.volume *= self.masterVolume
def adjust_bgm_volume(self, change):
"""Adjust background music volume.
Args:
change (float): Volume change (-1.0 to 1.0)
"""
if not -1.0 <= change <= 1.0:
return
self.bgmVolume = max(0.0, min(1.0, self.bgmVolume + change))
if self.currentBgm:
self.currentBgm.volume = self.bgmVolume * self.masterVolume
def adjust_sfx_volume(self, change):
"""Adjust sound effects volume.
Args:
change (float): Volume change (-1.0 to 1.0)
"""
if not -1.0 <= change <= 1.0:
return
self.sfxVolume = max(0.0, min(1.0, self.sfxVolume + change))
for sound in self.activeSounds:
if sound.playing:
sound.volume *= self.sfxVolume
def get_volumes(self):
"""Get current volume levels.
Returns:
tuple: (masterVolume, bgmVolume, sfxVolume)
"""
return (self.masterVolume, self.bgmVolume, self.sfxVolume)
def pause(self):
"""Pause all audio."""
if self.currentBgm:
self.currentBgm.pause()
for sound in self.activeSounds:
if sound.playing:
sound.pause()
def resume(self):
"""Resume all audio."""
if self.currentBgm:
self.currentBgm.play()
for sound in self.activeSounds:
sound.play()
def stop_all_sounds(self):
"""Stop all playing sounds."""
for sound in self.activeSounds:
sound.pause()
self.activeSounds.clear()
def cleanup(self):
"""Clean up sound resources."""
if self.currentBgm:
self.currentBgm.pause()
self.stop_all_sounds()