mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-08 08:07:54 -06:00
feat(audio): centralize sound management with proper enum, improved iterator protocols, introduce new sound files
This commit is contained in:
BIN
assets/game/sound/begin.ogg
Normal file
BIN
assets/game/sound/begin.ogg
Normal file
Binary file not shown.
BIN
assets/game/sound/intermission.ogg
Normal file
BIN
assets/game/sound/intermission.ogg
Normal file
Binary file not shown.
BIN
assets/game/sound/pacman/extra_life.ogg
Normal file
BIN
assets/game/sound/pacman/extra_life.ogg
Normal file
Binary file not shown.
BIN
assets/game/sound/pacman/fruit.ogg
Normal file
BIN
assets/game/sound/pacman/fruit.ogg
Normal file
Binary file not shown.
BIN
assets/game/sound/pacman/ghost.ogg
Normal file
BIN
assets/game/sound/pacman/ghost.ogg
Normal file
Binary file not shown.
66
src/asset.rs
66
src/asset.rs
@@ -2,21 +2,62 @@
|
||||
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use strum_macros::EnumIter;
|
||||
use std::iter;
|
||||
|
||||
use crate::audio::Sound;
|
||||
|
||||
/// Enumeration of all game assets with cross-platform loading support.
|
||||
///
|
||||
/// Each variant corresponds to a specific file that can be loaded either from
|
||||
/// binary-embedded data or embedded filesystem (Emscripten).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Asset {
|
||||
Waka(u8),
|
||||
/// Main sprite atlas containing all game graphics (atlas.png)
|
||||
AtlasImage,
|
||||
/// Terminal Vector font for text rendering (TerminalVector.ttf)
|
||||
Font,
|
||||
/// Sound effect for Pac-Man's death
|
||||
DeathSound,
|
||||
/// Sound file assets
|
||||
SoundFile(Sound),
|
||||
}
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
impl Asset {
|
||||
#[allow(dead_code)]
|
||||
pub fn into_iter() -> AssetIter {
|
||||
AssetIter {
|
||||
sound_iter: None,
|
||||
state: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct AssetIter {
|
||||
sound_iter: Option<iter::Map<<Sound as IntoEnumIterator>::Iterator, fn(Sound) -> Asset>>,
|
||||
state: u8,
|
||||
}
|
||||
|
||||
impl Iterator for AssetIter {
|
||||
type Item = Asset;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.state {
|
||||
0 => {
|
||||
self.state = 1;
|
||||
Some(Asset::AtlasImage)
|
||||
}
|
||||
1 => {
|
||||
self.state = 2;
|
||||
Some(Asset::Font)
|
||||
}
|
||||
2 => self
|
||||
.sound_iter
|
||||
.get_or_insert_with(|| Sound::iter().map(Asset::SoundFile))
|
||||
.next(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
@@ -29,11 +70,16 @@ impl Asset {
|
||||
pub fn path(&self) -> &str {
|
||||
use Asset::*;
|
||||
match self {
|
||||
Waka(0) => "sound/pacman/waka/1.ogg",
|
||||
Waka(1) => "sound/pacman/waka/2.ogg",
|
||||
Waka(2) => "sound/pacman/waka/3.ogg",
|
||||
Waka(3..=u8::MAX) => "sound/pacman/waka/4.ogg",
|
||||
DeathSound => "sound/pacman/death.ogg",
|
||||
SoundFile(Sound::Waka(0)) => "sound/pacman/waka/1.ogg",
|
||||
SoundFile(Sound::Waka(1)) => "sound/pacman/waka/2.ogg",
|
||||
SoundFile(Sound::Waka(2)) => "sound/pacman/waka/3.ogg",
|
||||
SoundFile(Sound::Waka(3..=u8::MAX)) => "sound/pacman/waka/4.ogg",
|
||||
SoundFile(Sound::PacmanDeath) => "sound/pacman/death.ogg",
|
||||
SoundFile(Sound::ExtraLife) => "sound/pacman/extra_life.ogg",
|
||||
SoundFile(Sound::Fruit) => "sound/pacman/fruit.ogg",
|
||||
SoundFile(Sound::Ghost) => "sound/pacman/ghost.ogg",
|
||||
SoundFile(Sound::Beginning) => "sound/begin.ogg",
|
||||
SoundFile(Sound::Intermission) => "sound/intermission.ogg",
|
||||
AtlasImage => "atlas.png",
|
||||
Font => "TerminalVector.ttf",
|
||||
}
|
||||
|
||||
79
src/audio.rs
79
src/audio.rs
@@ -1,11 +1,43 @@
|
||||
//! This module handles the audio playback for the game.
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::asset::{get_asset_bytes, Asset};
|
||||
use sdl2::{
|
||||
mixer::{self, Chunk, InitFlag, LoaderRWops, AUDIO_S16LSB, DEFAULT_CHANNELS},
|
||||
rwops::RWops,
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
const SOUND_ASSETS: [Asset; 4] = [Asset::Waka(0), Asset::Waka(1), Asset::Waka(2), Asset::Waka(3)];
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Sound {
|
||||
Waka(u8),
|
||||
PacmanDeath,
|
||||
ExtraLife,
|
||||
Fruit,
|
||||
Ghost,
|
||||
Beginning,
|
||||
Intermission,
|
||||
}
|
||||
|
||||
impl IntoEnumIterator for Sound {
|
||||
type Iterator = std::vec::IntoIter<Sound>;
|
||||
|
||||
fn iter() -> Self::Iterator {
|
||||
vec![
|
||||
Sound::Waka(0),
|
||||
Sound::Waka(1),
|
||||
Sound::Waka(2),
|
||||
Sound::Waka(3),
|
||||
Sound::PacmanDeath,
|
||||
Sound::ExtraLife,
|
||||
Sound::Fruit,
|
||||
Sound::Ghost,
|
||||
Sound::Beginning,
|
||||
Sound::Intermission,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// The audio system for the game.
|
||||
///
|
||||
@@ -14,9 +46,8 @@ const SOUND_ASSETS: [Asset; 4] = [Asset::Waka(0), Asset::Waka(1), Asset::Waka(2)
|
||||
/// functions will silently do nothing.
|
||||
pub struct Audio {
|
||||
_mixer_context: Option<mixer::Sdl2MixerContext>,
|
||||
sounds: Vec<Chunk>,
|
||||
death_sound: Option<Chunk>,
|
||||
next_sound_index: usize,
|
||||
sounds: HashMap<Sound, Chunk>,
|
||||
next_waka_index: u8,
|
||||
muted: bool,
|
||||
disabled: bool,
|
||||
}
|
||||
@@ -54,9 +85,8 @@ impl Audio {
|
||||
tracing::warn!("Failed to open audio: {}. Audio will be disabled.", e);
|
||||
return Self {
|
||||
_mixer_context: None,
|
||||
sounds: Vec::new(),
|
||||
death_sound: None,
|
||||
next_sound_index: 0,
|
||||
sounds: HashMap::new(),
|
||||
next_waka_index: 0u8,
|
||||
muted: false,
|
||||
disabled: true,
|
||||
};
|
||||
@@ -77,9 +107,8 @@ impl Audio {
|
||||
tracing::warn!("Failed to initialize SDL2_mixer: {}. Audio will be disabled.", e);
|
||||
return Self {
|
||||
_mixer_context: None,
|
||||
sounds: Vec::new(),
|
||||
death_sound: None,
|
||||
next_sound_index: 0,
|
||||
sounds: HashMap::new(),
|
||||
next_waka_index: 0u8,
|
||||
muted: false,
|
||||
disabled: true,
|
||||
};
|
||||
@@ -87,12 +116,14 @@ impl Audio {
|
||||
};
|
||||
|
||||
// Try to load sounds, but don't panic if any fail
|
||||
let mut sounds = Vec::new();
|
||||
for (i, asset) in SOUND_ASSETS.iter().enumerate() {
|
||||
match get_asset_bytes(*asset) {
|
||||
let mut sounds = HashMap::new();
|
||||
for (i, asset) in Sound::iter().enumerate() {
|
||||
match get_asset_bytes(Asset::SoundFile(asset)) {
|
||||
Ok(data) => match RWops::from_bytes(&data) {
|
||||
Ok(rwops) => match rwops.load_wav() {
|
||||
Ok(chunk) => sounds.push(chunk),
|
||||
Ok(chunk) => {
|
||||
sounds.insert(asset, chunk);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to load sound {} from asset API: {}", i + 1, e);
|
||||
}
|
||||
@@ -107,7 +138,7 @@ impl Audio {
|
||||
}
|
||||
}
|
||||
|
||||
let death_sound = match get_asset_bytes(Asset::DeathSound) {
|
||||
let death_sound = match get_asset_bytes(Asset::SoundFile(Sound::PacmanDeath)) {
|
||||
Ok(data) => match RWops::from_bytes(&data) {
|
||||
Ok(rwops) => match rwops.load_wav() {
|
||||
Ok(chunk) => Some(chunk),
|
||||
@@ -132,9 +163,8 @@ impl Audio {
|
||||
tracing::warn!("No sounds loaded successfully. Audio will be disabled.");
|
||||
return Self {
|
||||
_mixer_context: Some(mixer_context),
|
||||
sounds: Vec::new(),
|
||||
death_sound: None,
|
||||
next_sound_index: 0,
|
||||
sounds: HashMap::new(),
|
||||
next_waka_index: 0u8,
|
||||
muted: false,
|
||||
disabled: true,
|
||||
};
|
||||
@@ -143,8 +173,7 @@ impl Audio {
|
||||
Audio {
|
||||
_mixer_context: Some(mixer_context),
|
||||
sounds,
|
||||
death_sound,
|
||||
next_sound_index: 0,
|
||||
next_waka_index: 0u8,
|
||||
muted: false,
|
||||
disabled: false,
|
||||
}
|
||||
@@ -160,17 +189,17 @@ impl Audio {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(chunk) = self.sounds.get(self.next_sound_index) {
|
||||
if let Some(chunk) = self.sounds.get(&Sound::Waka(self.next_waka_index)) {
|
||||
match mixer::Channel(0).play(chunk, 0) {
|
||||
Ok(channel) => {
|
||||
tracing::trace!("Playing sound #{} on channel {:?}", self.next_sound_index + 1, channel);
|
||||
tracing::trace!("Playing sound #{} on channel {:?}", self.next_waka_index + 1, channel);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Could not play sound #{}: {}", self.next_sound_index + 1, e);
|
||||
tracing::warn!("Could not play sound #{}: {}", self.next_waka_index + 1, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len();
|
||||
self.next_waka_index = (self.next_waka_index + 1) & 3;
|
||||
}
|
||||
|
||||
/// Plays the death sound effect.
|
||||
@@ -179,7 +208,7 @@ impl Audio {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(chunk) = &self.death_sound {
|
||||
if let Some(chunk) = self.sounds.get(&Sound::PacmanDeath) {
|
||||
mixer::Channel::all().play(chunk, 0).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
||||
use rand::rngs::ThreadRng;
|
||||
|
||||
use crate::asset::Asset;
|
||||
use crate::audio::Sound;
|
||||
use crate::error::{AssetError, PlatformError};
|
||||
|
||||
/// Desktop platform implementation.
|
||||
@@ -59,13 +60,20 @@ pub fn init_console() -> Result<(), PlatformError> {
|
||||
|
||||
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
|
||||
match asset {
|
||||
Asset::Waka(0) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/1.ogg"))),
|
||||
Asset::Waka(1) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/2.ogg"))),
|
||||
Asset::Waka(2) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/3.ogg"))),
|
||||
Asset::Waka(3..=u8::MAX) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/4.ogg"))),
|
||||
Asset::SoundFile(sound) => match sound {
|
||||
Sound::Waka(0) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/1.ogg"))),
|
||||
Sound::Waka(1) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/2.ogg"))),
|
||||
Sound::Waka(2) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/3.ogg"))),
|
||||
Sound::Waka(3..=u8::MAX) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/4.ogg"))),
|
||||
Sound::PacmanDeath => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/death.ogg"))),
|
||||
Sound::ExtraLife => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/extra_life.ogg"))),
|
||||
Sound::Fruit => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/fruit.ogg"))),
|
||||
Sound::Ghost => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/ghost.ogg"))),
|
||||
Sound::Beginning => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/begin.ogg"))),
|
||||
Sound::Intermission => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/intermission.ogg"))),
|
||||
},
|
||||
Asset::AtlasImage => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))),
|
||||
Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))),
|
||||
Asset::DeathSound => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/death.ogg"))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use pacman::asset::Asset;
|
||||
use speculoos::prelude::*;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn all_asset_paths_exist() {
|
||||
for asset in Asset::iter() {
|
||||
for asset in Asset::into_iter() {
|
||||
let path = asset.path();
|
||||
let full_path = format!("assets/game/{}", path);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user