Source code for core.components.sound


from core.ecs import Component
from core.resources import ResourceManager
import pygame
import os
from core.logger import get_logger

_sound_logger = get_logger("sound")

[docs] class SoundComponent(Component): def __init__( self, file_path="", volume=1.0, loop=False, is_music=False, autoplay=False, spatialize=True, min_distance=0.0, max_distance=600.0, pan_distance=300.0 ): self.file_path = file_path self.volume = max(0.0, min(1.0, volume)) self.loop = loop self.is_music = is_music self.autoplay = autoplay self.spatialize = bool(spatialize) self.min_distance = max(0.0, float(min_distance)) self.max_distance = max(self.min_distance, float(max_distance)) self.pan_distance = max(0.0001, float(pan_distance)) self._sound = None self._loaded_path = None self._autoplay_handled = False self._is_paused = False self._channel = None self._spatial_attenuation = 1.0 self._pan = 0.0
[docs] def load(self) -> bool: """Loads the sound resource.""" if ResourceManager._headless: return False if not self.file_path: return False if self.is_music: # For music, we just verify the path exists. Streaming happens on play. path = ResourceManager.resolve_path(self.file_path) if os.path.exists(path): self._loaded_path = path return True _sound_logger.warning("Music file not found", path=path, original=self.file_path) return False else: # For sound effects, load into memory via ResourceManager self._sound = ResourceManager.load_sound(self.file_path) if self._sound: self._sound.set_volume(self.volume) return True return False
[docs] def play(self): """Plays the sound or music.""" if ResourceManager._headless: return # Ensure loaded if self.is_music: if not self._loaded_path: if not self.load(): return try: # Initialize mixer if not already done (safety check) if not pygame.mixer.get_init(): pygame.mixer.init() pygame.mixer.music.load(self._loaded_path) pygame.mixer.music.set_volume(self._effective_volume()) loops = -1 if self.loop else 0 pygame.mixer.music.play(loops) self._is_paused = False except Exception as e: _sound_logger.error("Failed to play music", path=self._loaded_path, error=str(e)) else: if not self._sound: if not self.load(): return if self._sound: loops = -1 if self.loop else 0 self._channel = self._sound.play(loops) self.apply_output()
[docs] def stop(self): """Stops playback.""" if self.is_music: if pygame.mixer.get_init(): pygame.mixer.music.stop() elif self._sound: self._sound.stop() self._channel = None
[docs] def pause(self): """Pauses playback.""" if self.is_music: if pygame.mixer.get_init(): pygame.mixer.music.pause() self._is_paused = True
# Sound effects in pygame don't have individual pause, only stop. # But mixer.pause() pauses ALL channels. We probably don't want that for individual sounds. # So for SFX, pause is often not supported per-sound without a channel reference. # We'll skip SFX pause for now unless we manage channels.
[docs] def unpause(self): """Resumes playback.""" if self.is_music: if pygame.mixer.get_init() and self._is_paused: pygame.mixer.music.unpause() self._is_paused = False
[docs] def set_volume(self, volume: float): """Sets the volume (0.0 to 1.0).""" self.volume = max(0.0, min(1.0, volume)) self.apply_output()
[docs] def set_spatial(self, attenuation: float, pan: float): self._spatial_attenuation = max(0.0, min(1.0, float(attenuation))) self._pan = max(-1.0, min(1.0, float(pan))) self.apply_output()
def _effective_volume(self) -> float: return max(0.0, min(1.0, self.volume * self._spatial_attenuation))
[docs] def apply_output(self): effective_volume = self._effective_volume() if self.is_music: if pygame.mixer.get_init(): pygame.mixer.music.set_volume(effective_volume) return # Try to use the channel if we have one and it's still playing if self._channel: # Check if channel is still active if hasattr(self._channel, "get_busy") and self._channel.get_busy(): pan = self._pan left = effective_volume * (1.0 - max(0.0, pan)) right = effective_volume * (1.0 + min(0.0, pan)) self._channel.set_volume(max(0.0, min(1.0, left)), max(0.0, min(1.0, right))) return else: # Channel is no longer playing, clear reference self._channel = None # If no active channel, set volume on the sound object for next play if self._sound: self._sound.set_volume(effective_volume)
[docs] def on_destroy(self): self.stop()