mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-08 16:07:52 -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.
|
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem.
|
||||||
|
|
||||||
use std::borrow::Cow;
|
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.
|
/// Enumeration of all game assets with cross-platform loading support.
|
||||||
///
|
///
|
||||||
/// Each variant corresponds to a specific file that can be loaded either from
|
/// Each variant corresponds to a specific file that can be loaded either from
|
||||||
/// binary-embedded data or embedded filesystem (Emscripten).
|
/// binary-embedded data or embedded filesystem (Emscripten).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Asset {
|
pub enum Asset {
|
||||||
Waka(u8),
|
|
||||||
/// Main sprite atlas containing all game graphics (atlas.png)
|
/// Main sprite atlas containing all game graphics (atlas.png)
|
||||||
AtlasImage,
|
AtlasImage,
|
||||||
/// Terminal Vector font for text rendering (TerminalVector.ttf)
|
/// Terminal Vector font for text rendering (TerminalVector.ttf)
|
||||||
Font,
|
Font,
|
||||||
/// Sound effect for Pac-Man's death
|
/// Sound file assets
|
||||||
DeathSound,
|
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 {
|
impl Asset {
|
||||||
@@ -29,11 +70,16 @@ impl Asset {
|
|||||||
pub fn path(&self) -> &str {
|
pub fn path(&self) -> &str {
|
||||||
use Asset::*;
|
use Asset::*;
|
||||||
match self {
|
match self {
|
||||||
Waka(0) => "sound/pacman/waka/1.ogg",
|
SoundFile(Sound::Waka(0)) => "sound/pacman/waka/1.ogg",
|
||||||
Waka(1) => "sound/pacman/waka/2.ogg",
|
SoundFile(Sound::Waka(1)) => "sound/pacman/waka/2.ogg",
|
||||||
Waka(2) => "sound/pacman/waka/3.ogg",
|
SoundFile(Sound::Waka(2)) => "sound/pacman/waka/3.ogg",
|
||||||
Waka(3..=u8::MAX) => "sound/pacman/waka/4.ogg",
|
SoundFile(Sound::Waka(3..=u8::MAX)) => "sound/pacman/waka/4.ogg",
|
||||||
DeathSound => "sound/pacman/death.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",
|
AtlasImage => "atlas.png",
|
||||||
Font => "TerminalVector.ttf",
|
Font => "TerminalVector.ttf",
|
||||||
}
|
}
|
||||||
|
|||||||
79
src/audio.rs
79
src/audio.rs
@@ -1,11 +1,43 @@
|
|||||||
//! This module handles the audio playback for the game.
|
//! This module handles the audio playback for the game.
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::asset::{get_asset_bytes, Asset};
|
use crate::asset::{get_asset_bytes, Asset};
|
||||||
use sdl2::{
|
use sdl2::{
|
||||||
mixer::{self, Chunk, InitFlag, LoaderRWops, AUDIO_S16LSB, DEFAULT_CHANNELS},
|
mixer::{self, Chunk, InitFlag, LoaderRWops, AUDIO_S16LSB, DEFAULT_CHANNELS},
|
||||||
rwops::RWops,
|
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.
|
/// 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.
|
/// functions will silently do nothing.
|
||||||
pub struct Audio {
|
pub struct Audio {
|
||||||
_mixer_context: Option<mixer::Sdl2MixerContext>,
|
_mixer_context: Option<mixer::Sdl2MixerContext>,
|
||||||
sounds: Vec<Chunk>,
|
sounds: HashMap<Sound, Chunk>,
|
||||||
death_sound: Option<Chunk>,
|
next_waka_index: u8,
|
||||||
next_sound_index: usize,
|
|
||||||
muted: bool,
|
muted: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
}
|
}
|
||||||
@@ -54,9 +85,8 @@ impl Audio {
|
|||||||
tracing::warn!("Failed to open audio: {}. Audio will be disabled.", e);
|
tracing::warn!("Failed to open audio: {}. Audio will be disabled.", e);
|
||||||
return Self {
|
return Self {
|
||||||
_mixer_context: None,
|
_mixer_context: None,
|
||||||
sounds: Vec::new(),
|
sounds: HashMap::new(),
|
||||||
death_sound: None,
|
next_waka_index: 0u8,
|
||||||
next_sound_index: 0,
|
|
||||||
muted: false,
|
muted: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
};
|
};
|
||||||
@@ -77,9 +107,8 @@ impl Audio {
|
|||||||
tracing::warn!("Failed to initialize SDL2_mixer: {}. Audio will be disabled.", e);
|
tracing::warn!("Failed to initialize SDL2_mixer: {}. Audio will be disabled.", e);
|
||||||
return Self {
|
return Self {
|
||||||
_mixer_context: None,
|
_mixer_context: None,
|
||||||
sounds: Vec::new(),
|
sounds: HashMap::new(),
|
||||||
death_sound: None,
|
next_waka_index: 0u8,
|
||||||
next_sound_index: 0,
|
|
||||||
muted: false,
|
muted: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
};
|
};
|
||||||
@@ -87,12 +116,14 @@ impl Audio {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Try to load sounds, but don't panic if any fail
|
// Try to load sounds, but don't panic if any fail
|
||||||
let mut sounds = Vec::new();
|
let mut sounds = HashMap::new();
|
||||||
for (i, asset) in SOUND_ASSETS.iter().enumerate() {
|
for (i, asset) in Sound::iter().enumerate() {
|
||||||
match get_asset_bytes(*asset) {
|
match get_asset_bytes(Asset::SoundFile(asset)) {
|
||||||
Ok(data) => match RWops::from_bytes(&data) {
|
Ok(data) => match RWops::from_bytes(&data) {
|
||||||
Ok(rwops) => match rwops.load_wav() {
|
Ok(rwops) => match rwops.load_wav() {
|
||||||
Ok(chunk) => sounds.push(chunk),
|
Ok(chunk) => {
|
||||||
|
sounds.insert(asset, chunk);
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Failed to load sound {} from asset API: {}", i + 1, 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(data) => match RWops::from_bytes(&data) {
|
||||||
Ok(rwops) => match rwops.load_wav() {
|
Ok(rwops) => match rwops.load_wav() {
|
||||||
Ok(chunk) => Some(chunk),
|
Ok(chunk) => Some(chunk),
|
||||||
@@ -132,9 +163,8 @@ impl Audio {
|
|||||||
tracing::warn!("No sounds loaded successfully. Audio will be disabled.");
|
tracing::warn!("No sounds loaded successfully. Audio will be disabled.");
|
||||||
return Self {
|
return Self {
|
||||||
_mixer_context: Some(mixer_context),
|
_mixer_context: Some(mixer_context),
|
||||||
sounds: Vec::new(),
|
sounds: HashMap::new(),
|
||||||
death_sound: None,
|
next_waka_index: 0u8,
|
||||||
next_sound_index: 0,
|
|
||||||
muted: false,
|
muted: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
};
|
};
|
||||||
@@ -143,8 +173,7 @@ impl Audio {
|
|||||||
Audio {
|
Audio {
|
||||||
_mixer_context: Some(mixer_context),
|
_mixer_context: Some(mixer_context),
|
||||||
sounds,
|
sounds,
|
||||||
death_sound,
|
next_waka_index: 0u8,
|
||||||
next_sound_index: 0,
|
|
||||||
muted: false,
|
muted: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
}
|
}
|
||||||
@@ -160,17 +189,17 @@ impl Audio {
|
|||||||
return;
|
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) {
|
match mixer::Channel(0).play(chunk, 0) {
|
||||||
Ok(channel) => {
|
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) => {
|
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.
|
/// Plays the death sound effect.
|
||||||
@@ -179,7 +208,7 @@ impl Audio {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(chunk) = &self.death_sound {
|
if let Some(chunk) = self.sounds.get(&Sound::PacmanDeath) {
|
||||||
mixer::Channel::all().play(chunk, 0).ok();
|
mixer::Channel::all().play(chunk, 0).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
|||||||
use rand::rngs::ThreadRng;
|
use rand::rngs::ThreadRng;
|
||||||
|
|
||||||
use crate::asset::Asset;
|
use crate::asset::Asset;
|
||||||
|
use crate::audio::Sound;
|
||||||
use crate::error::{AssetError, PlatformError};
|
use crate::error::{AssetError, PlatformError};
|
||||||
|
|
||||||
/// Desktop platform implementation.
|
/// 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> {
|
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
|
||||||
match asset {
|
match asset {
|
||||||
Asset::Waka(0) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/1.ogg"))),
|
Asset::SoundFile(sound) => match sound {
|
||||||
Asset::Waka(1) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/2.ogg"))),
|
Sound::Waka(0) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/1.ogg"))),
|
||||||
Asset::Waka(2) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/3.ogg"))),
|
Sound::Waka(1) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/2.ogg"))),
|
||||||
Asset::Waka(3..=u8::MAX) => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman/waka/4.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::AtlasImage => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))),
|
||||||
Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))),
|
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 pacman::asset::Asset;
|
||||||
use speculoos::prelude::*;
|
use speculoos::prelude::*;
|
||||||
use strum::IntoEnumIterator;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn all_asset_paths_exist() {
|
fn all_asset_paths_exist() {
|
||||||
for asset in Asset::iter() {
|
for asset in Asset::into_iter() {
|
||||||
let path = asset.path();
|
let path = asset.path();
|
||||||
let full_path = format!("assets/game/{}", path);
|
let full_path = format!("assets/game/{}", path);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user