358 lines
12 KiB
Python
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()
|