"""
Sound and music management for the game.
This module handles the loading, mixing, and playback of all audio assets,
including background music tracks, block-specific interaction sounds (mining,
placing, walking), and UI sound effects.
"""
import random
from typing import Any, Dict, List
import pygame as pg
from profiler import global_profiler
from settings import (
CACTUS,
COBBELSTONE,
DIRT,
GLASS,
GLOWSTONE,
GRASS,
GRAVEL,
LEAVES,
SAND,
SNOW,
STONE,
STONE_BRICKS,
WOOD,
WOOD_PLANKS,
get_path,
)
[docs]
class Sounds:
"""
Manages all audio assets, sound effects, and background music.
Handles loading sounds, randomizing playback for variety, and mapping specific
blocks to their respective material sound effects.
Args:
app (Any): The main application context.
"""
@global_profiler.profile_func('Sounds_Init')
def __init__(self, app: Any) -> None:
"""
Initializes the Pygame mixer, loads all block sounds, and starts the background music loop.
"""
# Variable assignments
self.app: Any = app
# Execute expressions
pg.mixer.init()
pg.mixer.set_num_channels(32)
# Define function
def load(filename: str) -> pg.mixer.Sound:
# Variable assignments
# Variable assignments
s: pg.mixer.Sound = pg.mixer.Sound(get_path(f'assets/audio/blocks/{filename}'))
# Return result
# Return result
return s
# Variable assignments
self.sounds: Dict[int, Dict[str, List[pg.mixer.Sound]]] = {}
self.sounds[SAND] = {
'break': [load(f'sand/Sand_dig{i}.ogg') for i in range(1, 5)],
'place': [load(f'sand/Sand_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'sand/Sand_mining{i}.ogg') for i in range(1, 6)],
'jump': [load(f'sand/Sand_hit{i}.ogg') for i in range(1, 5)],
'walk': [load(f'sand/Sand_hit{i}.ogg') for i in range(1, 6)],
}
self.sounds[GRASS] = {
'break': [load(f'grass/Grass_dig{i}.ogg') for i in range(1, 5)],
'place': [load(f'grass/Grass_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'grass/Grass_mining{i}.ogg') for i in range(1, 7)],
'jump': [load(f'grass/Grass_hit{i}.ogg') for i in range(1, 5)],
'walk': [load(f'grass/Grass_hit{i}.ogg') for i in range(1, 7)],
}
self.sounds[GRAVEL] = {
'break': [load(f'gravel/Gravel_dig{i}.ogg') for i in range(1, 5)],
'place': [load(f'gravel/Gravel_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'gravel/Gravel_mining{i}.ogg') for i in range(1, 5)],
'jump': [load(f'gravel/Gravel_hit{i}.ogg') for i in range(1, 5)],
'walk': [load(f'gravel/Gravel_hit{i}.ogg') for i in range(1, 5)],
}
self.sounds[STONE] = {
'break': [load(f'stone/Stone_dig{i}.ogg') for i in range(1, 5)],
'place': [load(f'stone/Stone_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'stone/Stone_mining{i}.ogg') for i in range(1, 7)],
'jump': [load(f'stone/Stone_hit{i}.ogg') for i in range(1, 7)],
'walk': [load(f'stone/Stone_hit{i}.ogg') for i in range(1, 7)],
}
self.sounds[SNOW] = {
'break': [load(f'snow/Snow_dig{i}.ogg') for i in range(1, 5)],
'place': [load(f'snow/Snow_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'snow/Snow_dig{i}.ogg') for i in range(1, 5)],
'jump': [load(f'snow/Snow_dig{i}.ogg') for i in range(1, 5)],
'walk': [load(f'snow/Snow_dig{i}.ogg') for i in range(1, 5)],
}
self.sounds[LEAVES] = self.sounds[GRASS]
self.sounds[WOOD] = {
'break': [load(f'wood/Wood_dig{i}.ogg') for i in range(1, 5)],
'place': [load(f'wood/Wood_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'wood/Wood_mining{i}.ogg') for i in range(1, 7)],
'jump': [load(f'wood/Wood_hit{i}.ogg') for i in range(1, 7)],
'walk': [load(f'wood/Wood_hit{i}.ogg') for i in range(1, 7)],
}
self.sounds[DIRT] = self.sounds[GRAVEL]
self.sounds[GLASS] = {
'break': [load(f'glass/Glass_dig{i}.ogg') for i in range(1, 4)],
'place': [load(f'stone/Stone_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'ice/Ice_mining{i}.ogg') for i in range(1, 7)],
'jump': [load(f'stone/Stone_hit{i}.ogg') for i in range(1, 7)],
'walk': [load(f'stone/Stone_hit{i}.ogg') for i in range(1, 7)],
}
self.sounds[WOOD_PLANKS] = self.sounds[WOOD]
self.sounds[COBBELSTONE] = self.sounds[STONE]
self.sounds[GLOWSTONE] = {
'break': [load(f'glass/Glass_dig{i}.ogg') for i in range(1, 4)],
'place': [load(f'stone/Stone_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'ice/Ice_mining{i}.ogg') for i in range(1, 7)],
'jump': [load(f'stone/Stone_hit{i}.ogg') for i in range(1, 7)],
'walk': [load(f'stone/Stone_hit{i}.ogg') for i in range(1, 7)],
}
self.sounds[GLASS] = {
'break': [load(f'glass/Glass_dig{i}.ogg') for i in range(1, 4)],
'place': [load(f'stone/Stone_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'ice/Ice_mining{i}.ogg') for i in range(1, 7)],
'jump': [load(f'stone/Stone_hit{i}.ogg') for i in range(1, 7)],
'walk': [load(f'stone/Stone_hit{i}.ogg') for i in range(1, 7)],
}
self.sounds[CACTUS] = {
'break': [load(f'cloth/Cloth_dig{i}.ogg') for i in range(1, 5)],
'place': [load(f'cloth/Cloth_dig{i}.ogg') for i in range(1, 5)],
'breaking': [load(f'cloth/Cloth_dig{i}.ogg') for i in range(1, 5)],
'jump': [load(f'cloth/Cloth_dig{i}.ogg') for i in range(1, 5)],
'walk': [load(f'cloth/Cloth_dig{i}.ogg') for i in range(1, 5)],
}
self.sounds[STONE_BRICKS] = self.sounds[STONE]
self.hit_index: int = 0
self.last_hit_time: int = 0
self.mining_index: int = -1
self.pop_sound: pg.mixer.Sound = pg.mixer.Sound(get_path('assets/audio/sfx/pickup-sound.ogg'))
# Execute expressions
self.set_sfx_volume(self.app.config.get('sfx_volume', 20))
# Variable assignments
self.music_tracks: List[str] = [
get_path('assets/audio/music/c418-aria-math-(minecraft-volume-beta).ogg'),
get_path('assets/audio/music/c418-minecraft.ogg'),
]
# Execute expressions
pg.mixer.music.load(random.choice(self.music_tracks))
pg.mixer.music.set_volume(self.app.config.get('music_volume', 50) / 100.0)
pg.mixer.music.play(-1)
[docs]
@global_profiler.profile_func('Sounds_SetSFXVolume')
def set_sfx_volume(self, value: float) -> None:
"""Updates the volume for all loaded sound effects."""
# Variable assignments
vol: float = value / 100.0
# Conditional logic
if hasattr(self, 'pop_sound'):
# Execute expressions
self.pop_sound.set_volume(min(1.0, vol * 5.0))
# Loop processing
for category_dict in self.sounds.values():
# Loop processing
for sound_list in category_dict.values():
# Loop processing
for s in sound_list:
# Execute expressions
s.set_volume(vol)
[docs]
@global_profiler.profile_func('Sounds_PlayWalk')
def play_walk(self, voxel_id: int) -> None:
"""
Plays a walking footstep sound based on the material of the block the player is standing on.
Automatically cycles through the available footstep variations.
"""
# Variable assignments
current_time: int = pg.time.get_ticks()
# Conditional logic
if current_time - self.last_hit_time > 500:
# Variable assignments
self.hit_index = 0
# Variable assignments
s_dict: Dict[str, List[pg.mixer.Sound]] = self.sounds.get(voxel_id, self.sounds[GRASS])
hits: List[pg.mixer.Sound] = s_dict['walk']
# Conditional logic
if self.hit_index >= len(hits):
# Variable assignments
self.hit_index = 0
# Execute expressions
hits[self.hit_index].play()
# Variable assignments
self.hit_index += 1
self.last_hit_time = current_time
[docs]
@global_profiler.profile_func('Sounds_PlayBreak')
def play_break(self, voxel_id: int) -> None:
"""
Plays a hard breaking sound when a block is fully destroyed.
"""
# Variable assignments
s_dict: Dict[str, List[pg.mixer.Sound]] = self.sounds.get(voxel_id, self.sounds[GRASS])
# Execute expressions
random.choice(s_dict['break']).play()
[docs]
@global_profiler.profile_func('Sounds_PlayPlace')
def play_place(self, voxel_id: int) -> None:
"""
Plays a block placement sound when adding a new block to the world.
"""
# Variable assignments
s_dict: Dict[str, List[pg.mixer.Sound]] = self.sounds.get(voxel_id, self.sounds[GRASS])
# Execute expressions
random.choice(s_dict['place']).play()
[docs]
@global_profiler.profile_func('Sounds_PlayJump')
def play_jump(self, voxel_id: int) -> None:
"""
Plays a jump sound when the player jumps.
"""
# Variable assignments
s_dict: Dict[str, List[pg.mixer.Sound]] = self.sounds.get(voxel_id, self.sounds[GRASS])
# Execute expressions
random.choice(s_dict['jump']).play()
[docs]
@global_profiler.profile_func('Sounds_PlayBreaking')
def play_breaking(self, voxel_id: int, mining_time: float, mining_duration: float) -> None:
"""
Plays a continuous sequence of hitting sounds mapped to the progress of mining a block.
"""
# Conditional logic
if mining_time == 0.0:
# Variable assignments
self.mining_index = -1
# Variable assignments
s_dict: Dict[str, List[pg.mixer.Sound]] = self.sounds.get(voxel_id, self.sounds[GRASS])
mining_sounds: List[pg.mixer.Sound] = s_dict['breaking']
num_sounds: int = len(mining_sounds)
progress: float = mining_time / mining_duration
target_index: int = int(progress * num_sounds)
target_index = min(target_index, num_sounds - 1)
# Conditional logic
if target_index > self.mining_index:
# Execute expressions
mining_sounds[target_index].play()
# Variable assignments
self.mining_index = target_index
[docs]
@global_profiler.profile_func('Sounds_PlayPlaceBlock')
def play_place_block(self) -> None:
"""
Plays a pop sound effect when a dropped item entity is collected and added
to the player's inventory.
"""
# Execute expressions
self.pop_sound.play()