Soundtrack expanded to 35 pieces with a leitmotif variant generator and 8 new instruments

- generator: leitmotif as scale degrees, keys/modes, augmentation, diminution, inversion,
  fragment sequences, accompaniment and drum styles; hand-written title 2, boss themes, victory 2
- organ, bell, choir, sub bass, tom, crash, harp, timpani; 8-bit samples, unused instruments stripped
- src/tracks.rs generated with categories; music director with boss themes and room moods
- all pieces validated with libopenmpt; notes checked to stay in key

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 08:09:15 +01:00
1 parent 1e92e19089
commit 5158075d7b
42 files changed
+721 -98

No files matched your search

+26 -29
View File
@@ -3,32 +3,30 @@
// which starts the AudioContext on the first user interaction.
use crate::sfx;
use crate::tracks::TRACKS;
use crate::xm::{self, Module, Player};
use std::cell::RefCell;
use std::rc::Rc;
// Music categories; the tracks themselves are listed in tracks.rs (generated by the composer)
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Track {
pub enum Category {
Title,
Build1,
Build2,
Build3,
Battle1,
Battle2,
Build,
Battle,
Assault,
BossPaladin,
BossWarden,
Torture,
Temple,
Victory,
Defeat,
}
const TRACKS: [(Track, &[u8]); 8] = [
(Track::Title, include_bytes!("../assets/music/title.xm")),
(Track::Build1, include_bytes!("../assets/music/build1.xm")),
(Track::Build2, include_bytes!("../assets/music/build2.xm")),
(Track::Build3, include_bytes!("../assets/music/build3.xm")),
(Track::Battle1, include_bytes!("../assets/music/battle1.xm")),
(Track::Battle2, include_bytes!("../assets/music/battle2.xm")),
(Track::Victory, include_bytes!("../assets/music/victory.xm")),
(Track::Defeat, include_bytes!("../assets/music/defeat.xm")),
];
// indices of all tracks of a category, in play order
pub fn tracks_in(c: Category) -> Vec<usize> {
TRACKS.iter().enumerate().filter(|(_, t)| t.1 == c).map(|(i, _)| i).collect()
}
const MUSIC_GAIN: f32 = 0.45; // our replayer is ~4x louder than libopenmpt; leaves headroom for effects
const SFX_GAIN: f32 = 0.55;
@@ -61,7 +59,7 @@ struct State {
music_vol: f32,
sfx_vol: f32,
muted: bool,
track: Option<Track>,
track: Option<usize>,
engine: Option<Engine>,
}
@@ -69,24 +67,20 @@ thread_local! {
static STATE: RefCell<State> = RefCell::new(State { music_vol: 0.6, sfx_vol: 0.7, muted: false, track: None, engine: None });
}
fn track_index(t: Track) -> usize {
TRACKS.iter().position(|(k, _)| *k == t).unwrap_or(0)
}
impl Engine {
fn new(rate: f32) -> Engine {
let modules = TRACKS.iter().map(|(_, d)| Rc::new(xm::load(d).expect("built-in music"))).collect();
let modules = TRACKS.iter().map(|(_, _, d)| Rc::new(xm::load(d).expect("built-in music"))).collect();
let sfx = sfx::generate_all(rate).into_iter().map(Rc::new).collect();
Engine { rate, modules, music: Vec::new(), voices: Vec::new(), sfx, buf: Vec::new() }
}
fn start_track(&mut self, t: Track, fade: f32) {
fn start_track(&mut self, t: usize, fade: f32) {
let speed = 1.0 / fade.max(0.01);
for v in self.music.iter_mut() {
v.target = 0.0;
v.speed = speed;
}
let player = Player::new(self.modules[track_index(t)].clone(), self.rate);
let player = Player::new(self.modules[t.min(self.modules.len() - 1)].clone(), self.rate);
let instant = self.music.is_empty() || fade <= 0.0;
self.music.push(MusicVoice { player, gain: if instant { 1.0 } else { 0.0 }, target: 1.0, speed });
}
@@ -143,7 +137,7 @@ pub fn set_volumes(music: f32, sfx: f32, muted: bool) {
}
// Switch music with a crossfade (seconds); does nothing if the track already plays
pub fn play_music(t: Track, fade: f32) {
pub fn play_music(t: usize, fade: f32) {
STATE.with(|s| {
let mut s = s.borrow_mut();
if s.track == Some(t) {
@@ -164,7 +158,7 @@ pub fn music_loops() -> u32 {
})
}
pub fn current_track() -> Option<Track> {
pub fn current_track() -> Option<usize> {
STATE.with(|s| s.borrow().track)
}
@@ -225,11 +219,14 @@ mod tests {
#[test]
fn engine_crossfade_and_sfx() {
dc_audio_init(48000.0);
play_music(Track::Build1, 0.0);
let build = tracks_in(Category::Build);
let battle = tracks_in(Category::Battle);
assert_eq!(build.len(), 15);
play_music(build[0], 0.0);
let mut peak = 0.0f32;
for k in 0..200 {
if k == 50 {
play_music(Track::Battle1, 2.5);
play_music(battle[0], 2.5);
}
if k % 10 == 0 {
play_sfx(k % crate::game::SFX_ALL.len(), 1.0, 0.3);
@@ -243,6 +240,6 @@ mod tests {
}
}
assert!(peak > 0.05);
assert_eq!(current_track(), Some(Track::Battle1));
assert_eq!(current_track(), Some(battle[0]));
}
}