Own XM/MOD replayer, audio mixer with crossfades, synthesised chip sound effects, music director
- src/xm.rs: FastTracker 2 XM and ProTracker MOD replayer (verified against libopenmpt) - src/audio.rs: music players with crossfade + effect voices, rendered on demand - src/sfx.rs: 29 synthesised chip sound effects - www/dc_plugin.js: AudioContext started on first input, streams samples from WASM - music follows the game: title, rotating build tracks, battle, gate assault, jingles - macroquad audio removed; all tests run natively with cargo test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2dba65be8b
commit
925be17e82
13 files changed
+1711
-205
No files matched your search
+14
-76
@@ -1,56 +1,20 @@
|
||||
// Sound playback. The game logic reports events (Sfx), they are played here.
|
||||
// Sounds will be generated later and registered via `SoundBank::set` (WAV bytes).
|
||||
// Events without a registered sound stay silent.
|
||||
// Sound effect dispatch: turns game sound events into positioned effect voices
|
||||
// (volume by distance to the camera, stereo panning along the screen's x axis).
|
||||
// The audio itself is rendered by audio.rs.
|
||||
|
||||
use crate::audio;
|
||||
use crate::game::{Sfx, SfxEvent, SFX_ALL};
|
||||
#[cfg(feature = "audio")]
|
||||
use macroquad::audio::{load_sound_from_bytes, play_sound, PlaySoundParams, Sound};
|
||||
#[cfg(not(feature = "audio"))]
|
||||
use stub::*;
|
||||
|
||||
// Silent stand-in when the "audio" feature is off (native tests)
|
||||
#[cfg(not(feature = "audio"))]
|
||||
mod stub {
|
||||
#[derive(Clone)]
|
||||
pub struct Sound;
|
||||
pub struct PlaySoundParams {
|
||||
pub looped: bool,
|
||||
pub volume: f32,
|
||||
}
|
||||
pub async fn load_sound_from_bytes(_: &[u8]) -> Result<Sound, ()> {
|
||||
Err(())
|
||||
}
|
||||
pub fn play_sound(_: &Sound, _: PlaySoundParams) {}
|
||||
}
|
||||
use macroquad::prelude::*;
|
||||
|
||||
pub struct SoundBank {
|
||||
sounds: Vec<Option<Sound>>,
|
||||
last: Vec<f64>,
|
||||
pub volume: f32,
|
||||
pub muted: bool,
|
||||
}
|
||||
|
||||
impl SoundBank {
|
||||
pub fn new() -> SoundBank {
|
||||
SoundBank { sounds: vec![None; SFX_ALL.len()], last: vec![-1.0; SFX_ALL.len()], volume: 0.7, muted: false }
|
||||
SoundBank { last: vec![-1.0; SFX_ALL.len()] }
|
||||
}
|
||||
|
||||
// register the sound for an event (WAV or OGG bytes)
|
||||
#[allow(dead_code)]
|
||||
pub async fn set(&mut self, sfx: Sfx, bytes: &[u8]) -> bool {
|
||||
match load_sound_from_bytes(bytes).await {
|
||||
Ok(s) => {
|
||||
self.sounds[sfx.index()] = Some(s);
|
||||
true
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
// procedurally generated sounds will be registered here
|
||||
pub async fn generate_all(&mut self) {}
|
||||
|
||||
// minimum gap between two identical sounds (avoids a wall of noise)
|
||||
fn min_gap(sfx: Sfx) -> f64 {
|
||||
match sfx {
|
||||
@@ -60,51 +24,25 @@ impl SoundBank {
|
||||
}
|
||||
}
|
||||
|
||||
// listener: camera target on the ground; hear: audible radius in tiles
|
||||
pub fn play(&mut self, ev: SfxEvent, listener: Vec2, hear: f32, now: f64) {
|
||||
if self.muted || self.volume <= 0.0 {
|
||||
return;
|
||||
}
|
||||
// listener: camera target on the ground; right: screen-right direction on the ground;
|
||||
// hear: audible radius in tiles
|
||||
pub fn play(&mut self, ev: SfxEvent, listener: Vec2, right: Vec2, hear: f32, now: f64) {
|
||||
let i = ev.sfx.index();
|
||||
let Some(sound) = &self.sounds[i] else { return };
|
||||
if now - self.last[i] < Self::min_gap(ev.sfx) {
|
||||
return;
|
||||
}
|
||||
// volume by distance to the listener
|
||||
let mut vol = self.volume;
|
||||
let mut vol = 1.0;
|
||||
let mut pan = 0.0;
|
||||
if let Some((wx, wy)) = ev.pos {
|
||||
let d = ((wx - listener.x).powi(2) + (wy - listener.y).powi(2)).sqrt();
|
||||
let f = 1.0 - ((d - hear * 0.5) / hear).clamp(0.0, 1.0);
|
||||
let d = vec2(wx, wy) - listener;
|
||||
let f = 1.0 - ((d.length() - hear * 0.5) / hear).clamp(0.0, 1.0);
|
||||
if f <= 0.05 {
|
||||
return;
|
||||
}
|
||||
vol *= f;
|
||||
pan = (d.dot(right) / hear).clamp(-1.0, 1.0) * 0.7;
|
||||
}
|
||||
self.last[i] = now;
|
||||
play_sound(sound, PlaySoundParams { looped: false, volume: vol });
|
||||
audio::play_sfx(i, vol, pan);
|
||||
}
|
||||
}
|
||||
|
||||
// helper for sound generation: samples (-1..1, mono) -> WAV (16 bit PCM)
|
||||
#[allow(dead_code)]
|
||||
pub fn wav_from_samples(samples: &[f32], rate: u32) -> Vec<u8> {
|
||||
let data_len = (samples.len() * 2) as u32;
|
||||
let mut out = Vec::with_capacity(44 + data_len as usize);
|
||||
out.extend_from_slice(b"RIFF");
|
||||
out.extend_from_slice(&(36 + data_len).to_le_bytes());
|
||||
out.extend_from_slice(b"WAVEfmt ");
|
||||
out.extend_from_slice(&16u32.to_le_bytes());
|
||||
out.extend_from_slice(&1u16.to_le_bytes()); // PCM
|
||||
out.extend_from_slice(&1u16.to_le_bytes()); // mono
|
||||
out.extend_from_slice(&rate.to_le_bytes());
|
||||
out.extend_from_slice(&(rate * 2).to_le_bytes());
|
||||
out.extend_from_slice(&2u16.to_le_bytes());
|
||||
out.extend_from_slice(&16u16.to_le_bytes());
|
||||
out.extend_from_slice(b"data");
|
||||
out.extend_from_slice(&data_len.to_le_bytes());
|
||||
for &s in samples {
|
||||
let v = (s.clamp(-1.0, 1.0) * 32767.0) as i16;
|
||||
out.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
Reference in new issue
Block a user