Files
Verbatim/src/audio/mod.rs
T
Emil 9ce45cff4a feat: audio system with 15 procedurally generated sounds via soundgen
- AudioEngine with rodio (WAV playback, throttling, volume, toggle)
- 15 embedded sounds: jump, shoot, hit, explosion, death, pickup, descend,
  powerup, step, lava_bubble, acid_sizzle, water_splash, fire_crackle,
  ui_click, goblin_growl
- Sound events: shoot, hit, explosion (fireball), pickup, descend, powerup,
  jump, combat hit, ambient material sounds (lava/fire/acid/water)
- M key toggles audio in GPU modes
- Ambient sounds: scans 15-cell radius around player, plays throttled sounds
  for nearby lava/fire/acid/water
- AudioEngine gracefully degrades when no audio device available
- All 185 tests + 14 scenarios pass
2026-06-22 00:03:27 +03:00

150 lines
4.3 KiB
Rust

use rodio::source::Source;
use rodio::{OutputStream, Sink};
use std::collections::HashMap;
use std::io::Cursor;
use std::time::Instant;
pub struct AudioEngine {
_stream: Option<OutputStream>,
sink: Option<Sink>,
sounds: HashMap<&'static str, &'static [u8]>,
last_played: HashMap<&'static str, Instant>,
volume: f32,
enabled: bool,
}
impl AudioEngine {
pub fn new() -> Self {
let (stream, handle) = match OutputStream::try_default() {
Ok(s) => s,
Err(e) => {
eprintln!("Audio init failed: {}", e);
return Self::disabled();
}
};
let sink = match Sink::try_new(&handle) {
Ok(s) => s,
Err(e) => {
eprintln!("Audio sink init failed: {}", e);
return Self::disabled();
}
};
let mut sounds = HashMap::new();
sounds.insert(
"jump",
include_bytes!("../../assets/sounds/jump.wav") as &[u8],
);
sounds.insert("shoot", include_bytes!("../../assets/sounds/shoot.wav"));
sounds.insert("hit", include_bytes!("../../assets/sounds/hit.wav"));
sounds.insert(
"explosion",
include_bytes!("../../assets/sounds/explosion.wav"),
);
sounds.insert("death", include_bytes!("../../assets/sounds/death.wav"));
sounds.insert("pickup", include_bytes!("../../assets/sounds/pickup.wav"));
sounds.insert("descend", include_bytes!("../../assets/sounds/descend.wav"));
sounds.insert("powerup", include_bytes!("../../assets/sounds/powerup.wav"));
sounds.insert("step", include_bytes!("../../assets/sounds/step.wav"));
sounds.insert(
"lava_bubble",
include_bytes!("../../assets/sounds/lava_bubble.wav"),
);
sounds.insert(
"acid_sizzle",
include_bytes!("../../assets/sounds/acid_sizzle.wav"),
);
sounds.insert(
"water_splash",
include_bytes!("../../assets/sounds/water_splash.wav"),
);
sounds.insert(
"fire_crackle",
include_bytes!("../../assets/sounds/fire_crackle.wav"),
);
sounds.insert(
"ui_click",
include_bytes!("../../assets/sounds/ui_click.wav"),
);
sounds.insert(
"goblin_growl",
include_bytes!("../../assets/sounds/goblin_growl.wav"),
);
Self {
_stream: Some(stream),
sink: Some(sink),
sounds,
last_played: HashMap::new(),
volume: 0.5,
enabled: true,
}
}
fn disabled() -> Self {
Self {
_stream: None,
sink: None,
sounds: HashMap::new(),
last_played: HashMap::new(),
volume: 0.0,
enabled: false,
}
}
pub fn play(&mut self, name: &'static str) {
self.play_throttled(name, 50);
}
pub fn play_throttled(&mut self, name: &'static str, min_interval_ms: u64) {
if !self.enabled {
return;
}
let now = Instant::now();
if let Some(&last) = self.last_played.get(name) {
if now.duration_since(last).as_millis() < min_interval_ms as u128 {
return;
}
}
self.last_played.insert(name, now);
let data = match self.sounds.get(name) {
Some(d) => *d,
None => return,
};
let sink = match &self.sink {
Some(s) => s,
None => return,
};
if let Ok(decoder) = rodio::Decoder::new(Cursor::new(data.to_vec())) {
let source = decoder.amplify(self.volume).convert_samples::<f32>();
sink.append(source);
}
}
pub fn set_volume(&mut self, vol: f32) {
self.volume = vol.clamp(0.0, 1.0);
}
pub fn toggle(&mut self) {
self.enabled = !self.enabled;
if let Some(sink) = &self.sink {
if !self.enabled {
sink.pause();
} else {
sink.play();
}
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
}
impl Default for AudioEngine {
fn default() -> Self {
Self::new()
}
}