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:
1 parent
1e92e19089
commit
5158075d7b
42 files changed
+721
-98
No files matched your search
@@ -41,8 +41,10 @@ An original dungeon management game (genre: dungeon building & defence) that run
|
|||||||
## Music & sound
|
## Music & sound
|
||||||
- Own **XM/MOD replayer in Rust** (`src/xm.rs`): instruments, envelopes, fadeout, auto-vibrato, linear/Amiga frequencies, common FT2 effects; verified against libopenmpt
|
- Own **XM/MOD replayer in Rust** (`src/xm.rs`): instruments, envelopes, fadeout, auto-vibrato, linear/Amiga frequencies, common FT2 effects; verified against libopenmpt
|
||||||
- **Shared mixer** for music + sound effects (`src/audio.rs`), streamed to the browser by the JS plugin (replaces macroquad audio)
|
- **Shared mixer** for music + sound effects (`src/audio.rs`), streamed to the browser by the JS plugin (replaces macroquad audio)
|
||||||
- Soundtrack in classic **Amiga chiptune** style, composed as `.xm` by `tools/compose_music.py` (editable in MilkyTracker): title/menu, **3 build tracks** (rotating), **2 battle tracks** (wave defence, gate assault), victory/defeat jingles; **the title melody is a recognisable leitmotif in every piece**
|
- Soundtrack in classic **Amiga chiptune** style, **35 pieces**, composed as `.xm` by `tools/compose_music.py` (editable in MilkyTracker): 2 title, 15 build, 7 wave battle, 4 gate assault, 2 boss (Paladin of Light, Gate Warden), 1 torture chamber mood, 1 temple mood, 2 victory, 1 defeat; **the title melody is a recognisable leitmotif in every piece**
|
||||||
- Music changes with **soft crossfades** (2.5 s): title → build → battle when heroes invade → storm music while attacking the gate → jingles
|
- Most pieces come from a **variant generator**: the leitmotif is stored as scale degrees and realised in different keys and modes (minor, harmonic minor, dorian, phrygian, lydian), augmented, diminished, inverted or as fragment sequences, with varying accompaniment, drum styles, instruments and forms; title 2, both boss themes and the second victory jingle are hand-written
|
||||||
|
- 16 chip instruments (pulse, square, saw pad, triangle bass, pluck, kick, snare, hihat, organ, bell, choir, sub bass, tom, crash, harp, timpani); 8-bit samples, each file only contains the instruments it uses
|
||||||
|
- Music changes with **soft crossfades** (2.5 s): title → build (rotating through all build pieces) → battle when heroes invade → assault while attacking the gate → boss themes while a Paladin of Light or the Gate Warden is alive → jingles; near a torture chamber or temple the matching **room mood** plays
|
||||||
- Sound effects are **synthesised chip sounds** (`src/sfx.rs`), positioned by distance and stereo panning
|
- Sound effects are **synthesised chip sounds** (`src/sfx.rs`), positioned by distance and stereo panning
|
||||||
|
|
||||||
## Map size
|
## Map size
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+26
-29
@@ -3,32 +3,30 @@
|
|||||||
// which starts the AudioContext on the first user interaction.
|
// which starts the AudioContext on the first user interaction.
|
||||||
|
|
||||||
use crate::sfx;
|
use crate::sfx;
|
||||||
|
use crate::tracks::TRACKS;
|
||||||
use crate::xm::{self, Module, Player};
|
use crate::xm::{self, Module, Player};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
// Music categories; the tracks themselves are listed in tracks.rs (generated by the composer)
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub enum Track {
|
pub enum Category {
|
||||||
Title,
|
Title,
|
||||||
Build1,
|
Build,
|
||||||
Build2,
|
Battle,
|
||||||
Build3,
|
Assault,
|
||||||
Battle1,
|
BossPaladin,
|
||||||
Battle2,
|
BossWarden,
|
||||||
|
Torture,
|
||||||
|
Temple,
|
||||||
Victory,
|
Victory,
|
||||||
Defeat,
|
Defeat,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRACKS: [(Track, &[u8]); 8] = [
|
// indices of all tracks of a category, in play order
|
||||||
(Track::Title, include_bytes!("../assets/music/title.xm")),
|
pub fn tracks_in(c: Category) -> Vec<usize> {
|
||||||
(Track::Build1, include_bytes!("../assets/music/build1.xm")),
|
TRACKS.iter().enumerate().filter(|(_, t)| t.1 == c).map(|(i, _)| i).collect()
|
||||||
(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")),
|
|
||||||
];
|
|
||||||
|
|
||||||
const MUSIC_GAIN: f32 = 0.45; // our replayer is ~4x louder than libopenmpt; leaves headroom for effects
|
const MUSIC_GAIN: f32 = 0.45; // our replayer is ~4x louder than libopenmpt; leaves headroom for effects
|
||||||
const SFX_GAIN: f32 = 0.55;
|
const SFX_GAIN: f32 = 0.55;
|
||||||
@@ -61,7 +59,7 @@ struct State {
|
|||||||
music_vol: f32,
|
music_vol: f32,
|
||||||
sfx_vol: f32,
|
sfx_vol: f32,
|
||||||
muted: bool,
|
muted: bool,
|
||||||
track: Option<Track>,
|
track: Option<usize>,
|
||||||
engine: Option<Engine>,
|
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 });
|
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 {
|
impl Engine {
|
||||||
fn new(rate: f32) -> 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();
|
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() }
|
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);
|
let speed = 1.0 / fade.max(0.01);
|
||||||
for v in self.music.iter_mut() {
|
for v in self.music.iter_mut() {
|
||||||
v.target = 0.0;
|
v.target = 0.0;
|
||||||
v.speed = speed;
|
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;
|
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 });
|
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
|
// 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| {
|
STATE.with(|s| {
|
||||||
let mut s = s.borrow_mut();
|
let mut s = s.borrow_mut();
|
||||||
if s.track == Some(t) {
|
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)
|
STATE.with(|s| s.borrow().track)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,11 +219,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn engine_crossfade_and_sfx() {
|
fn engine_crossfade_and_sfx() {
|
||||||
dc_audio_init(48000.0);
|
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;
|
let mut peak = 0.0f32;
|
||||||
for k in 0..200 {
|
for k in 0..200 {
|
||||||
if k == 50 {
|
if k == 50 {
|
||||||
play_music(Track::Battle1, 2.5);
|
play_music(battle[0], 2.5);
|
||||||
}
|
}
|
||||||
if k % 10 == 0 {
|
if k % 10 == 0 {
|
||||||
play_sfx(k % crate::game::SFX_ALL.len(), 1.0, 0.3);
|
play_sfx(k % crate::game::SFX_ALL.len(), 1.0, 0.3);
|
||||||
@@ -243,6 +240,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert!(peak > 0.05);
|
assert!(peak > 0.05);
|
||||||
assert_eq!(current_track(), Some(Track::Battle1));
|
assert_eq!(current_track(), Some(battle[0]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+61
-25
@@ -11,6 +11,7 @@ mod render3d;
|
|||||||
mod save;
|
mod save;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod sfx;
|
mod sfx;
|
||||||
|
mod tracks;
|
||||||
mod sound;
|
mod sound;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod xm;
|
mod xm;
|
||||||
@@ -73,35 +74,69 @@ fn apply_settings(s: &Settings, r3d: &mut R3d) {
|
|||||||
audio::set_volumes(s.music_volume, s.volume, s.muted);
|
audio::set_volumes(s.music_volume, s.volume, s.muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chooses the music for the current situation; build tracks rotate after two loops
|
// Chooses the music for the current situation. Each category rotates through its tracks;
|
||||||
|
// build and title music switch to the next track after two loops.
|
||||||
struct MusicDirector {
|
struct MusicDirector {
|
||||||
build: usize,
|
next: std::collections::HashMap<u8, usize>, // category -> next index within the category
|
||||||
|
current: Option<audio::Category>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MusicDirector {
|
impl MusicDirector {
|
||||||
fn update(&mut self, g: &Game, title: bool) {
|
fn new() -> MusicDirector {
|
||||||
use audio::Track;
|
MusicDirector { next: std::collections::HashMap::new(), current: None }
|
||||||
const BUILD: [Track; 3] = [Track::Build1, Track::Build2, Track::Build3];
|
|
||||||
let gate_fight = g.time - g.gate_hit_t < 12.0 || g.gate_defenders_alive();
|
|
||||||
let invasion = g.ents.iter().any(|e| e.alive && e.active_hero() && !e.guard);
|
|
||||||
let want = if title {
|
|
||||||
Track::Title
|
|
||||||
} else if g.state == 1 {
|
|
||||||
Track::Victory
|
|
||||||
} else if g.state == 2 {
|
|
||||||
Track::Defeat
|
|
||||||
} else if gate_fight {
|
|
||||||
Track::Battle2
|
|
||||||
} else if invasion {
|
|
||||||
Track::Battle1
|
|
||||||
} else {
|
|
||||||
if BUILD.contains(&audio::current_track().unwrap_or(Track::Title)) && audio::music_loops() >= 2 {
|
|
||||||
self.build = (self.build + 1) % BUILD.len();
|
|
||||||
}
|
}
|
||||||
BUILD[self.build]
|
|
||||||
|
fn pick(&mut self, c: audio::Category) -> usize {
|
||||||
|
let list = audio::tracks_in(c);
|
||||||
|
let k = self.next.entry(c as u8).or_insert(0);
|
||||||
|
let t = list[*k % list.len()];
|
||||||
|
*k += 1;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, g: &Game, title: bool, cam: (f32, f32)) {
|
||||||
|
use audio::Category as C;
|
||||||
|
let alive = |f: &dyn Fn(&game::Ent) -> bool| g.ents.iter().any(|e| e.alive && f(e));
|
||||||
|
let near_room = |k: map::Kind| {
|
||||||
|
let (cx, cz) = (cam.0 as i32, cam.1 as i32);
|
||||||
|
let mut n = 0;
|
||||||
|
for y in cz - 5..=cz + 5 {
|
||||||
|
for x in cx - 5..=cx + 5 {
|
||||||
|
if g.map.get(x, y).map_or(false, |t| t.kind == k) {
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n >= 4
|
||||||
};
|
};
|
||||||
let fade = if matches!(want, Track::Victory | Track::Defeat) { 1.0 } else { 2.5 };
|
let want = if title {
|
||||||
audio::play_music(want, fade);
|
C::Title
|
||||||
|
} else if g.state == 1 {
|
||||||
|
C::Victory
|
||||||
|
} else if g.state == 2 {
|
||||||
|
C::Defeat
|
||||||
|
} else if alive(&|e| e.active_hero() && e.kind == game::EK::Paladin && e.guard) {
|
||||||
|
C::BossWarden
|
||||||
|
} else if alive(&|e| e.active_hero() && e.kind == game::EK::Paladin) {
|
||||||
|
C::BossPaladin
|
||||||
|
} else if g.time - g.gate_hit_t < 12.0 || g.gate_defenders_alive() {
|
||||||
|
C::Assault
|
||||||
|
} else if alive(&|e| e.active_hero() && !e.guard) {
|
||||||
|
C::Battle
|
||||||
|
} else if near_room(map::Kind::Torture) {
|
||||||
|
C::Torture
|
||||||
|
} else if near_room(map::Kind::Temple) {
|
||||||
|
C::Temple
|
||||||
|
} else {
|
||||||
|
C::Build
|
||||||
|
};
|
||||||
|
let rotate = matches!(want, C::Build | C::Title) && self.current == Some(want) && audio::music_loops() >= 2;
|
||||||
|
if self.current != Some(want) || rotate || audio::current_track().is_none() {
|
||||||
|
self.current = Some(want);
|
||||||
|
let t = self.pick(want);
|
||||||
|
let fade = if matches!(want, C::Victory | C::Defeat) { 1.0 } else { 2.5 };
|
||||||
|
audio::play_music(t, fade);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +210,7 @@ async fn main() {
|
|||||||
let gfx = Gfx::new();
|
let gfx = Gfx::new();
|
||||||
let mut r3d = R3d::new();
|
let mut r3d = R3d::new();
|
||||||
let mut sounds = SoundBank::new();
|
let mut sounds = SoundBank::new();
|
||||||
let mut music = MusicDirector { build: 0 };
|
let mut music = MusicDirector::new();
|
||||||
let mut ui = Ui::new();
|
let mut ui = Ui::new();
|
||||||
let mut menu = Menu::new();
|
let mut menu = Menu::new();
|
||||||
|
|
||||||
@@ -542,7 +577,8 @@ async fn main() {
|
|||||||
for ev in app.game.events.drain(..) {
|
for ev in app.game.events.drain(..) {
|
||||||
sounds.play(ev, vec2(app.cam.tx, app.cam.tz), right, hear, now);
|
sounds.play(ev, vec2(app.cam.tx, app.cam.tz), right, hear, now);
|
||||||
}
|
}
|
||||||
music.update(&app.game, menu.page == Some(Page::Title) || matches!(menu.page, Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. })));
|
let on_title = menu.page == Some(Page::Title) || matches!(menu.page, Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. }) | Some(Page::NewGame));
|
||||||
|
music.update(&app.game, on_title, (app.cam.tx, app.cam.tz));
|
||||||
for m in app.game.msgs.drain(..) {
|
for m in app.game.msgs.drain(..) {
|
||||||
ui.log.push((m, now));
|
ui.log.push((m, now));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Generated by tools/compose_music.py - do not edit.
|
||||||
|
// All music modules with their category (see audio::Category).
|
||||||
|
|
||||||
|
use crate::audio::Category;
|
||||||
|
|
||||||
|
pub const TRACKS: [(&str, Category, &[u8]); 35] = [
|
||||||
|
("title", Category::Title, include_bytes!("../assets/music/title.xm")),
|
||||||
|
("title2", Category::Title, include_bytes!("../assets/music/title2.xm")),
|
||||||
|
("build1", Category::Build, include_bytes!("../assets/music/build1.xm")),
|
||||||
|
("build2", Category::Build, include_bytes!("../assets/music/build2.xm")),
|
||||||
|
("build3", Category::Build, include_bytes!("../assets/music/build3.xm")),
|
||||||
|
("build4", Category::Build, include_bytes!("../assets/music/build4.xm")),
|
||||||
|
("build5", Category::Build, include_bytes!("../assets/music/build5.xm")),
|
||||||
|
("build6", Category::Build, include_bytes!("../assets/music/build6.xm")),
|
||||||
|
("build7", Category::Build, include_bytes!("../assets/music/build7.xm")),
|
||||||
|
("build8", Category::Build, include_bytes!("../assets/music/build8.xm")),
|
||||||
|
("build9", Category::Build, include_bytes!("../assets/music/build9.xm")),
|
||||||
|
("build10", Category::Build, include_bytes!("../assets/music/build10.xm")),
|
||||||
|
("build11", Category::Build, include_bytes!("../assets/music/build11.xm")),
|
||||||
|
("build12", Category::Build, include_bytes!("../assets/music/build12.xm")),
|
||||||
|
("build13", Category::Build, include_bytes!("../assets/music/build13.xm")),
|
||||||
|
("build14", Category::Build, include_bytes!("../assets/music/build14.xm")),
|
||||||
|
("build15", Category::Build, include_bytes!("../assets/music/build15.xm")),
|
||||||
|
("battle1", Category::Battle, include_bytes!("../assets/music/battle1.xm")),
|
||||||
|
("battle3", Category::Battle, include_bytes!("../assets/music/battle3.xm")),
|
||||||
|
("battle4", Category::Battle, include_bytes!("../assets/music/battle4.xm")),
|
||||||
|
("battle5", Category::Battle, include_bytes!("../assets/music/battle5.xm")),
|
||||||
|
("battle6", Category::Battle, include_bytes!("../assets/music/battle6.xm")),
|
||||||
|
("battle7", Category::Battle, include_bytes!("../assets/music/battle7.xm")),
|
||||||
|
("battle8", Category::Battle, include_bytes!("../assets/music/battle8.xm")),
|
||||||
|
("battle2", Category::Assault, include_bytes!("../assets/music/battle2.xm")),
|
||||||
|
("assault2", Category::Assault, include_bytes!("../assets/music/assault2.xm")),
|
||||||
|
("assault3", Category::Assault, include_bytes!("../assets/music/assault3.xm")),
|
||||||
|
("assault4", Category::Assault, include_bytes!("../assets/music/assault4.xm")),
|
||||||
|
("boss_paladin", Category::BossPaladin, include_bytes!("../assets/music/boss_paladin.xm")),
|
||||||
|
("boss_warden", Category::BossWarden, include_bytes!("../assets/music/boss_warden.xm")),
|
||||||
|
("mood_torture", Category::Torture, include_bytes!("../assets/music/mood_torture.xm")),
|
||||||
|
("mood_temple", Category::Temple, include_bytes!("../assets/music/mood_temple.xm")),
|
||||||
|
("victory", Category::Victory, include_bytes!("../assets/music/victory.xm")),
|
||||||
|
("victory2", Category::Victory, include_bytes!("../assets/music/victory2.xm")),
|
||||||
|
("defeat", Category::Defeat, include_bytes!("../assets/music/defeat.xm")),
|
||||||
|
];
|
||||||
@@ -1126,23 +1126,11 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
const SONGS: [&[u8]; 8] = [
|
|
||||||
include_bytes!("../assets/music/title.xm"),
|
|
||||||
include_bytes!("../assets/music/build1.xm"),
|
|
||||||
include_bytes!("../assets/music/build2.xm"),
|
|
||||||
include_bytes!("../assets/music/build3.xm"),
|
|
||||||
include_bytes!("../assets/music/battle1.xm"),
|
|
||||||
include_bytes!("../assets/music/battle2.xm"),
|
|
||||||
include_bytes!("../assets/music/victory.xm"),
|
|
||||||
include_bytes!("../assets/music/defeat.xm"),
|
|
||||||
];
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_all_songs() {
|
fn render_all_songs() {
|
||||||
for (k, data) in SONGS.iter().enumerate() {
|
for (k, (name, _, data)) in crate::tracks::TRACKS.iter().enumerate() {
|
||||||
let m = Rc::new(load(data).expect("load"));
|
let m = Rc::new(load(data).expect("load"));
|
||||||
assert_eq!(m.channels, 8);
|
assert_eq!(m.channels, 8, "{}", name);
|
||||||
assert_eq!(m.instruments.len(), 8);
|
|
||||||
let mut p = Player::new(m, 48000.0);
|
let mut p = Player::new(m, 48000.0);
|
||||||
let mut buf = vec![0.0f32; 48000 * 2];
|
let mut buf = vec![0.0f32; 48000 * 2];
|
||||||
let mut peak = 0.0f32;
|
let mut peak = 0.0f32;
|
||||||
@@ -1157,8 +1145,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let rms = (sum / (20.0 * 96000.0)).sqrt();
|
let rms = (sum / (20.0 * 96000.0)).sqrt();
|
||||||
println!("song {}: peak {:.3} rms {:.4} loops {}", k, peak, rms, p.loops);
|
println!("{:2} {:14}: peak {:.3} rms {:.4} loops {}", k, name, peak, rms, p.loops);
|
||||||
assert!(rms > 0.005, "song {} is silent", k);
|
assert!(rms > 0.005, "song {} is silent", name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1173,8 +1161,7 @@ mod render_dump {
|
|||||||
#[test]
|
#[test]
|
||||||
fn dump_renders() {
|
fn dump_renders() {
|
||||||
let Ok(dir) = std::env::var("DC_RENDER_DIR") else { return };
|
let Ok(dir) = std::env::var("DC_RENDER_DIR") else { return };
|
||||||
let names = ["title", "build1", "build2", "build3", "battle1", "battle2", "victory", "defeat"];
|
for (n, _, _) in crate::tracks::TRACKS.iter() {
|
||||||
for n in names {
|
|
||||||
let data = std::fs::read(format!("assets/music/{}.xm", n)).unwrap();
|
let data = std::fs::read(format!("assets/music/{}.xm", n)).unwrap();
|
||||||
let mut p = Player::new(Rc::new(load(&data).unwrap()), 48000.0);
|
let mut p = Player::new(Rc::new(load(&data).unwrap()), 48000.0);
|
||||||
let mut buf = vec![0.0f32; 48000 * 2 * 30];
|
let mut buf = vec![0.0f32; 48000 * 2 * 30];
|
||||||
|
|||||||
Binary file not shown.
+583
-24
@@ -180,6 +180,84 @@ def hihat():
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ---- additional timbres (used by the generated pieces)
|
||||||
|
|
||||||
|
def organ(amp=0.42):
|
||||||
|
harm = [(1, 1.0), (2, 0.6), (3, 0.4), (4, 0.3), (6, 0.2), (8, 0.15)]
|
||||||
|
raw = [sum(a * math.sin(2 * math.pi * h * i / CYCLE) for h, a in harm) for i in range(CYCLE)]
|
||||||
|
m = max(abs(v) for v in raw)
|
||||||
|
return [amp * v / m for v in raw]
|
||||||
|
|
||||||
|
|
||||||
|
def sine_cycle(amp=0.6):
|
||||||
|
return [amp * math.sin(2 * math.pi * i / CYCLE) for i in range(CYCLE)]
|
||||||
|
|
||||||
|
|
||||||
|
def harp_cycle(amp=0.5):
|
||||||
|
raw = [math.sin(2 * math.pi * i / CYCLE) + 0.35 * math.sin(4 * math.pi * i / CYCLE) + 0.15 * math.sin(6 * math.pi * i / CYCLE) for i in range(CYCLE)]
|
||||||
|
m = max(abs(v) for v in raw)
|
||||||
|
return [amp * v / m for v in raw]
|
||||||
|
|
||||||
|
|
||||||
|
def choir_pad(amp=0.36):
|
||||||
|
# vowel-like spectrum ("ah": strong 2nd/3rd and 7th/8th harmonics), two detuned copies -> chorus
|
||||||
|
harm = [(1, 0.5), (2, 1.0), (3, 0.8), (4, 0.3), (5, 0.2), (7, 0.45), (8, 0.4), (9, 0.15)]
|
||||||
|
def wave(ph):
|
||||||
|
return sum(a * math.sin(2 * math.pi * h * ph) for h, a in harm)
|
||||||
|
n = 128 * 127
|
||||||
|
raw = [wave((i % 128) / 128.0) + wave((i % 127) / 127.0) for i in range(n)]
|
||||||
|
m = max(abs(v) for v in raw)
|
||||||
|
return [amp * v / m for v in raw]
|
||||||
|
|
||||||
|
|
||||||
|
def bell():
|
||||||
|
# inharmonic partials, rendered at C4 for 16726 Hz playback (relnote +12)
|
||||||
|
f0 = 261.63
|
||||||
|
n = int(DRUM_RATE * 1.3)
|
||||||
|
parts = [(1.0, 1.0, 2.5), (2.0, 0.5, 3.5), (2.76, 0.4, 5.0), (5.4, 0.25, 8.0), (8.9, 0.12, 12.0)]
|
||||||
|
out = []
|
||||||
|
for i in range(n):
|
||||||
|
t = i / DRUM_RATE
|
||||||
|
out.append(0.5 * sum(a * math.exp(-t * d) * math.sin(2 * math.pi * f0 * r * t) for r, a, d in parts))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def tom():
|
||||||
|
n = int(DRUM_RATE * 0.35)
|
||||||
|
out, ph = [], 0.0
|
||||||
|
for i in range(n):
|
||||||
|
t = i / DRUM_RATE
|
||||||
|
f = 95 + 90 * math.exp(-t * 18)
|
||||||
|
ph += 2 * math.pi * f / DRUM_RATE
|
||||||
|
out.append(0.85 * math.sin(ph) * math.exp(-t * 9))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def crash():
|
||||||
|
rnd = random.Random(11)
|
||||||
|
n = int(DRUM_RATE * 1.0)
|
||||||
|
out, prev = [], 0.0
|
||||||
|
for i in range(n):
|
||||||
|
t = i / DRUM_RATE
|
||||||
|
x = rnd.random() * 2 - 1
|
||||||
|
out.append(0.55 * (x - 0.7 * prev) * math.exp(-t * 3.5))
|
||||||
|
prev = x
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def timpani():
|
||||||
|
rnd = random.Random(5)
|
||||||
|
n = int(DRUM_RATE * 0.9)
|
||||||
|
out, ph = [], 0.0
|
||||||
|
for i in range(n):
|
||||||
|
t = i / DRUM_RATE
|
||||||
|
f = 98 - 8 * math.exp(-t * 6)
|
||||||
|
ph += 2 * math.pi * f / DRUM_RATE
|
||||||
|
hit = 0.3 * (rnd.random() * 2 - 1) * math.exp(-t * 40)
|
||||||
|
out.append(0.8 * math.sin(ph) * math.exp(-t * 3.2) + hit)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def build_instruments():
|
def build_instruments():
|
||||||
return [
|
return [
|
||||||
Instrument("Lead pulse 25%", Sample("pulse25", pulse(0.25), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
|
Instrument("Lead pulse 25%", Sample("pulse25", pulse(0.25), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
|
||||||
@@ -195,10 +273,23 @@ def build_instruments():
|
|||||||
Instrument("Kick", Sample("kick", kick(), relnote=12)),
|
Instrument("Kick", Sample("kick", kick(), relnote=12)),
|
||||||
Instrument("Snare", Sample("snare", snare(), relnote=12)),
|
Instrument("Snare", Sample("snare", snare(), relnote=12)),
|
||||||
Instrument("Hihat", Sample("hihat", hihat(), relnote=12)),
|
Instrument("Hihat", Sample("hihat", hihat(), relnote=12)),
|
||||||
|
Instrument("Organ", Sample("organ", organ(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
|
||||||
|
env=[(0, 0), (2, 60), (40, 56)], sustain=2, fadeout=900),
|
||||||
|
Instrument("Bell", Sample("bell", bell(), relnote=12)),
|
||||||
|
Instrument("Choir", Sample("choir", choir_pad(), loop=(0, 128 * 127), relnote=CYCLE_REL, finetune=CYCLE_FT),
|
||||||
|
env=[(0, 0), (40, 50), (80, 46)], sustain=2, fadeout=450),
|
||||||
|
Instrument("Sub bass", Sample("sine", sine_cycle(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
|
||||||
|
env=[(0, 64), (10, 54), (60, 48)], sustain=2, fadeout=2400),
|
||||||
|
Instrument("Tom", Sample("tom", tom(), relnote=12)),
|
||||||
|
Instrument("Crash", Sample("crash", crash(), relnote=12)),
|
||||||
|
Instrument("Harp", Sample("harp", harp_cycle(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
|
||||||
|
env=[(0, 64), (4, 44), (30, 12), (70, 0)], fadeout=2000),
|
||||||
|
Instrument("Timpani", Sample("timpani", timpani(), relnote=12)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
LEAD, SQUARE, PAD, BASS, PLUCK, KICK, SNARE, HAT = range(1, 9)
|
LEAD, SQUARE, PAD, BASS, PLUCK, KICK, SNARE, HAT = range(1, 9)
|
||||||
|
ORGAN, BELL, CHOIR, SUB, TOM, CRASH, HARP, TIMP = range(9, 17)
|
||||||
# channel layout
|
# channel layout
|
||||||
CH_LEAD, CH_COUNTER, CH_PAD1, CH_PAD2, CH_BASS, CH_ARP, CH_DRUM, CH_HAT = range(8)
|
CH_LEAD, CH_COUNTER, CH_PAD1, CH_PAD2, CH_BASS, CH_ARP, CH_DRUM, CH_HAT = range(8)
|
||||||
CHANNELS = 8
|
CHANNELS = 8
|
||||||
@@ -601,6 +692,10 @@ def song_defeat():
|
|||||||
|
|
||||||
|
|
||||||
def write_xm(path, song, instruments):
|
def write_xm(path, song, instruments):
|
||||||
|
"""Writes an XM file. Only instruments used by the song are stored (renumbered),
|
||||||
|
samples are 8-bit like on the Amiga to keep the files small."""
|
||||||
|
used = sorted({c[1] for pat in song.patterns for row in pat.cells for c in row if c and c[1]})
|
||||||
|
remap = {old: new + 1 for new, old in enumerate(used)}
|
||||||
out = bytearray()
|
out = bytearray()
|
||||||
out += b"Extended Module: "
|
out += b"Extended Module: "
|
||||||
out += song.name.encode("ascii")[:20].ljust(20, b" ")
|
out += song.name.encode("ascii")[:20].ljust(20, b" ")
|
||||||
@@ -609,7 +704,7 @@ def write_xm(path, song, instruments):
|
|||||||
out += struct.pack("<H", 0x0104)
|
out += struct.pack("<H", 0x0104)
|
||||||
order = song.order + [0] * (256 - len(song.order))
|
order = song.order + [0] * (256 - len(song.order))
|
||||||
out += struct.pack("<IHHHHHHHH", 276, len(song.order), song.restart, CHANNELS, len(song.patterns),
|
out += struct.pack("<IHHHHHHHH", 276, len(song.order), song.restart, CHANNELS, len(song.patterns),
|
||||||
len(instruments), 1, song.speed, song.bpm)
|
len(used), 1, song.speed, song.bpm)
|
||||||
out += bytes(order)
|
out += bytes(order)
|
||||||
for pat in song.patterns:
|
for pat in song.patterns:
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
@@ -619,10 +714,11 @@ def write_xm(path, song, instruments):
|
|||||||
data += b"\x80"
|
data += b"\x80"
|
||||||
else:
|
else:
|
||||||
note, inst, vol, fx, param = c
|
note, inst, vol, fx, param = c
|
||||||
data += bytes([note, inst, vol, fx, param])
|
data += bytes([note, remap.get(inst, 0), vol, fx, param])
|
||||||
out += struct.pack("<IBHH", 9, 0, pat.rows, len(data))
|
out += struct.pack("<IBHH", 9, 0, pat.rows, len(data))
|
||||||
out += data
|
out += data
|
||||||
for ins in instruments:
|
for old in used:
|
||||||
|
ins = instruments[old - 1]
|
||||||
smp = ins.sample
|
smp = ins.sample
|
||||||
hdr = bytearray()
|
hdr = bytearray()
|
||||||
hdr += ins.name.encode("ascii")[:22].ljust(22, b"\0")
|
hdr += ins.name.encode("ascii")[:22].ljust(22, b"\0")
|
||||||
@@ -634,8 +730,7 @@ def write_xm(path, song, instruments):
|
|||||||
pts = bytearray()
|
pts = bytearray()
|
||||||
for (x, y) in env[:12]:
|
for (x, y) in env[:12]:
|
||||||
pts += struct.pack("<HH", x, y)
|
pts += struct.pack("<HH", x, y)
|
||||||
pts = pts.ljust(48, b"\0")
|
hdr += pts.ljust(48, b"\0")
|
||||||
hdr += pts
|
|
||||||
hdr += bytes(48) # panning envelope
|
hdr += bytes(48) # panning envelope
|
||||||
vol_type = 0
|
vol_type = 0
|
||||||
if env:
|
if env:
|
||||||
@@ -650,47 +745,511 @@ def write_xm(path, song, instruments):
|
|||||||
hdr += bytes(22) # reserved
|
hdr += bytes(22) # reserved
|
||||||
out += struct.pack("<I", 4 + len(hdr))
|
out += struct.pack("<I", 4 + len(hdr))
|
||||||
out += hdr
|
out += hdr
|
||||||
# sample header (16-bit)
|
|
||||||
n = len(smp.data)
|
n = len(smp.data)
|
||||||
loop_type = 0
|
loop_type = 0
|
||||||
ls, ll = 0, 0
|
ls, ll = 0, 0
|
||||||
if smp.loop:
|
if smp.loop:
|
||||||
loop_type = 2 if smp.ping else 1
|
loop_type = 2 if smp.ping else 1
|
||||||
ls, ll = smp.loop
|
ls, ll = smp.loop
|
||||||
out += struct.pack("<IIIBbBBbB", n * 2, ls * 2, ll * 2, smp.volume, smp.finetune, loop_type | 0x10, smp.pan,
|
out += struct.pack("<IIIBbBBbB", n, ls, ll, smp.volume, smp.finetune, loop_type, smp.pan, smp.relnote, 0)
|
||||||
smp.relnote, 0)
|
|
||||||
out += smp.name.encode("ascii")[:22].ljust(22, b"\0")
|
out += smp.name.encode("ascii")[:22].ljust(22, b"\0")
|
||||||
# delta encoded 16-bit sample data
|
|
||||||
prev = 0
|
prev = 0
|
||||||
for v in smp.data:
|
for v in smp.data:
|
||||||
s16 = int(round(max(-1.0, min(1.0, v)) * 32767))
|
s8 = int(round(max(-1.0, min(1.0, v)) * 127))
|
||||||
d = (s16 - prev) & 0xFFFF
|
out += struct.pack("<B", (s8 - prev) & 0xFF)
|
||||||
out += struct.pack("<H", d)
|
prev = s8
|
||||||
prev = s16
|
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(out)
|
f.write(out)
|
||||||
return len(out)
|
return len(out)
|
||||||
|
|
||||||
|
|
||||||
SONGS = {
|
# ------------------------------------------------------------------ variant generator
|
||||||
"title": song_title,
|
#
|
||||||
"build1": song_build1,
|
# The leitmotif is stored as scale degrees of a minor scale (0 = tonic, 7 = octave; a degree
|
||||||
"build2": song_build2,
|
# marked "lt" is the raised leading tone). It can be realised in any key and mode and is
|
||||||
"build3": song_build3,
|
# transformed (augmented, diminished, inverted, fragmented, sequenced) for every piece.
|
||||||
"battle1": song_battle1,
|
|
||||||
"battle2": song_battle2,
|
MODES = {
|
||||||
"victory": song_victory,
|
"minor": [0, 2, 3, 5, 7, 8, 10],
|
||||||
"defeat": song_defeat,
|
"harmonic": [0, 2, 3, 5, 7, 8, 11],
|
||||||
|
"dorian": [0, 2, 3, 5, 7, 9, 10],
|
||||||
|
"phrygian": [0, 1, 3, 5, 7, 8, 10],
|
||||||
|
"major": [0, 2, 4, 5, 7, 9, 11],
|
||||||
|
"lydian": [0, 2, 4, 6, 7, 9, 11],
|
||||||
|
"mixolydian": [0, 2, 4, 5, 7, 9, 10],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# (degree, rows, leading tone?)
|
||||||
|
MOTIF_DEG_A = [(0, 8), (4, 4), (2, 4), (3, 6), (2, 2), (1, 4), (-1, 4, "lt"), (0, 4), (2, 4), (4, 4), (7, 4), (6, 8), (4, 8)]
|
||||||
|
MOTIF_DEG_B = [(5, 8), (4, 4), (3, 4), (2, 6), (1, 2), (0, 4), (2, 4), (1, 4), (-1, 4, "lt"), (-3, 4), (-1, 4, "lt"), (0, 16)]
|
||||||
|
# harmony of the motif as scale degrees per bar (pairs = half bars)
|
||||||
|
MOTIF_HARM_A = [[0], [3, 4], [0], [2]]
|
||||||
|
MOTIF_HARM_B = [[5], [0], [4], [0]]
|
||||||
|
|
||||||
|
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
||||||
|
|
||||||
|
|
||||||
|
def degree_semis(deg, mode, lt=False):
|
||||||
|
octv, d = divmod(deg, 7)
|
||||||
|
if lt:
|
||||||
|
return 12 * octv + 11
|
||||||
|
return 12 * octv + MODES[mode][d]
|
||||||
|
|
||||||
|
|
||||||
|
def name_of(midi):
|
||||||
|
return "%s%d" % (NOTE_NAMES[midi % 12], midi // 12)
|
||||||
|
|
||||||
|
|
||||||
|
def realize(motif, root, mode, octave):
|
||||||
|
"""motif -> 'X4:8 ...' melody string in the given key"""
|
||||||
|
base = 12 * octave + root
|
||||||
|
out = []
|
||||||
|
for item in motif:
|
||||||
|
deg, l = item[0], item[1]
|
||||||
|
lt = len(item) > 2 and mode in ("minor", "harmonic", "dorian", "phrygian")
|
||||||
|
out.append("%s:%d" % (name_of(base + degree_semis(deg, mode, lt)), l))
|
||||||
|
return " ".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def m_aug(m):
|
||||||
|
return [(i[0], i[1] * 2) + tuple(i[2:]) for i in m]
|
||||||
|
|
||||||
|
|
||||||
|
def m_dim(m):
|
||||||
|
return [(i[0], max(1, i[1] // 2)) + tuple(i[2:]) for i in m]
|
||||||
|
|
||||||
|
|
||||||
|
def m_inv(m):
|
||||||
|
# mirror the melody around the tonic (keeps the rhythm)
|
||||||
|
return [(-i[0], i[1]) for i in m]
|
||||||
|
|
||||||
|
|
||||||
|
def m_shift(m, d):
|
||||||
|
return [(i[0] + d, i[1]) + tuple(i[2:]) for i in m]
|
||||||
|
|
||||||
|
|
||||||
|
def m_take(m, rows):
|
||||||
|
"""first `rows` rows of a motif"""
|
||||||
|
out, r = [], 0
|
||||||
|
for i in m:
|
||||||
|
if r >= rows:
|
||||||
|
break
|
||||||
|
l = min(i[1], rows - r)
|
||||||
|
out.append((i[0], l) + tuple(i[2:]))
|
||||||
|
r += l
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def rows_of(m):
|
||||||
|
return sum(i[1] for i in m)
|
||||||
|
|
||||||
|
|
||||||
|
def chord_tones(deg, mode, root, octave, dominant=False):
|
||||||
|
"""triad on a scale degree -> list of midi notes"""
|
||||||
|
base = 12 * octave + root
|
||||||
|
notes = [base + degree_semis(deg + k, mode) for k in (0, 2, 4)]
|
||||||
|
if dominant and mode in ("minor", "dorian", "phrygian", "harmonic") and deg % 7 == 4:
|
||||||
|
notes[1] = base + degree_semis(deg, mode) + 4 # major third on V
|
||||||
|
return notes
|
||||||
|
|
||||||
|
|
||||||
|
class Gen:
|
||||||
|
"""Builds a song from a spec dict (see SPECS)."""
|
||||||
|
|
||||||
|
def __init__(self, spec):
|
||||||
|
self.sp = spec
|
||||||
|
self.rng = random.Random(spec["seed"])
|
||||||
|
self.root = NOTE_BASE[spec["key"][0]] + (1 if spec["key"].endswith("#") else -1 if spec["key"].endswith("b") and len(spec["key"]) > 1 else 0)
|
||||||
|
self.mode = spec["mode"]
|
||||||
|
|
||||||
|
# ---- accompaniment for a list of chords (degree, rows)
|
||||||
|
def accompany(self, pat, chords):
|
||||||
|
sp = self.sp
|
||||||
|
r = 0
|
||||||
|
for deg, rows in chords:
|
||||||
|
notes = chord_tones(deg, self.mode, self.root, 4, dominant=True)
|
||||||
|
bass = chord_tones(deg, self.mode, self.root, 2, dominant=True)
|
||||||
|
# chord_tones() counts from C-0 = 0, XM notes from 1
|
||||||
|
if sp.get("pad"):
|
||||||
|
pat.set(r, CH_PAD1, notes[1] + 1, sp["pad"], sp.get("pad_vol", 20))
|
||||||
|
pat.set(r, CH_PAD2, notes[2] + 1, sp["pad"], sp.get("pad_vol", 20))
|
||||||
|
bp = sp.get("bass_pat", "R.......F.......")
|
||||||
|
for k in range(rows):
|
||||||
|
c = bp[(r + k) % len(bp)]
|
||||||
|
n = {"R": bass[0], "F": bass[2], "O": bass[0] + 12, "T": bass[1]}.get(c)
|
||||||
|
if n:
|
||||||
|
pat.set(r + k, CH_BASS, n + 1, sp.get("bass", BASS), sp.get("bass_vol", 44))
|
||||||
|
elif c == "-":
|
||||||
|
pat.set(r + k, CH_BASS, KEY_OFF)
|
||||||
|
if sp.get("arp"):
|
||||||
|
step = sp.get("arp_step", 2)
|
||||||
|
order = sp.get("arp_order", (0, 1, 2, 1))
|
||||||
|
an = chord_tones(deg, self.mode, self.root, sp.get("arp_oct", 5), dominant=True)
|
||||||
|
for i, rr in enumerate(range(0, rows, step)):
|
||||||
|
pat.set(r + rr, CH_ARP, an[order[i % len(order)]] + 1, sp["arp"], sp.get("arp_vol", 13))
|
||||||
|
r += rows
|
||||||
|
|
||||||
|
def drums(self, pat, style):
|
||||||
|
styles = {
|
||||||
|
"none": {},
|
||||||
|
"soft": dict(hat="x.......x.......", hv=10),
|
||||||
|
"calm": dict(kick="x...............", hat="x...x...x...x...", hv=10, kv=44),
|
||||||
|
"bouncy": dict(kick="x.......x.......", snare="............x...", hat="..x...x...x...x.", hv=13, sv=30),
|
||||||
|
"rock": dict(kick="x.....x.x.......", snare="....x.......x...", hat="x.x.x.x.x.x.x.x.", hv=15, sv=44),
|
||||||
|
"driving": dict(kick="x...x...x...x...", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", hv=10, sv=46),
|
||||||
|
"march": dict(kick="x.......x.......", snare="....x.x.....x.xx", hat="x...x...x...x...", hv=12, sv=38),
|
||||||
|
"tribal": dict(kick="x..x..x...x..x..", hat="x.x.x.x.x.x.x.x.", hv=11, kv=52),
|
||||||
|
"boss": dict(kick="x.x.x.x.x.x.x.x.", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", hv=10, sv=50),
|
||||||
|
}
|
||||||
|
d = styles[style]
|
||||||
|
pat.drums(kick=d.get("kick", ""), snare=d.get("snare", ""), hat=d.get("hat", ""), kv=d.get("kv", 60), sv=d.get("sv", 44), hv=d.get("hv", 18))
|
||||||
|
if style == "tribal":
|
||||||
|
for bar in range(pat.rows // BAR):
|
||||||
|
for i in (4, 12, 14):
|
||||||
|
pat.set(bar * BAR + i, CH_DRUM, note_num("A3" if i == 14 else "D4"), TOM, 46)
|
||||||
|
if style in ("boss", "driving", "march"):
|
||||||
|
pat.set(0, CH_HAT, note_num("C4"), CRASH, 34)
|
||||||
|
|
||||||
|
# ---- generated melody over chords (strong beats on chord tones)
|
||||||
|
def melody(self, chords, inst, vol, octave, style):
|
||||||
|
rhythms = {
|
||||||
|
"calm": [[8, 8], [12, 4], [4, 4, 8], [16], [6, 2, 8]],
|
||||||
|
"flow": [[4, 4, 4, 4], [6, 2, 4, 4], [8, 4, 4], [4, 4, 8]],
|
||||||
|
"bouncy": [[2, 2, 4, 2, 2, 4], [4, 2, 2, 4, 4], [2, 2, 2, 2, 8], [4, 4, 2, 2, 4]],
|
||||||
|
"driving": [[2, 2, 2, 2, 2, 2, 4], [4, 2, 2, 4, 4], [2, 2, 4, 2, 2, 4], [2] * 8],
|
||||||
|
}[style]
|
||||||
|
scale = MODES[self.mode]
|
||||||
|
base = 12 * octave + self.root
|
||||||
|
pool = [base + 12 * o + iv for o in (-1, 0, 1) for iv in scale]
|
||||||
|
cur = base + scale[0]
|
||||||
|
out = []
|
||||||
|
for deg, rows in chords:
|
||||||
|
ct = [n % 12 for n in chord_tones(deg, self.mode, self.root, 4, dominant=True)]
|
||||||
|
r = 0
|
||||||
|
while r < rows:
|
||||||
|
pat_r = self.rng.choice(rhythms)
|
||||||
|
for k, l in enumerate(pat_r):
|
||||||
|
if r >= rows:
|
||||||
|
break
|
||||||
|
l = min(l, rows - r)
|
||||||
|
strong = k == 0 or (r % 8 == 0)
|
||||||
|
if strong:
|
||||||
|
cands = [n for n in pool if n % 12 in ct and abs(n - cur) <= 7]
|
||||||
|
else:
|
||||||
|
cands = [n for n in pool if 0 < abs(n - cur) <= 3]
|
||||||
|
if not cands:
|
||||||
|
cands = [n for n in pool if n % 12 in ct]
|
||||||
|
# keep the line within about an octave around the centre
|
||||||
|
cands.sort(key=lambda n: abs(n - cur) + 0.4 * abs(n - (base + 7)) + self.rng.random() * 2)
|
||||||
|
cur = cands[0]
|
||||||
|
out.append("%s:%d" % (name_of(cur), l))
|
||||||
|
r += l
|
||||||
|
# occasional breath at the end of a phrase
|
||||||
|
return " ".join(out)
|
||||||
|
|
||||||
|
def harm_rows(self, harm, rows_per_bar=BAR):
|
||||||
|
"""[[0],[3,4],...] -> [(degree, rows)]"""
|
||||||
|
out = []
|
||||||
|
for bar in harm:
|
||||||
|
for d in bar:
|
||||||
|
out.append((d, rows_per_bar // len(bar)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
sp = self.sp
|
||||||
|
song = Song(sp["title"], sp["bpm"])
|
||||||
|
lead, lvol, loct = sp.get("lead", LEAD), sp.get("lead_vol", 40), sp.get("lead_oct", 4)
|
||||||
|
treat = sp.get("motif", "normal")
|
||||||
|
prog = sp["prog"] # list of 4 degrees (one bar each) for free sections
|
||||||
|
# motif treatment
|
||||||
|
mA, mB = MOTIF_DEG_A, MOTIF_DEG_B
|
||||||
|
hA, hB = MOTIF_HARM_A, MOTIF_HARM_B
|
||||||
|
if treat == "inv":
|
||||||
|
mA, mB = m_inv(mA), m_inv(mB)
|
||||||
|
elif treat == "shift":
|
||||||
|
mA, mB = m_shift(mA, 2), m_shift(mB, 2)
|
||||||
|
sections = [] # (melody string, chords) per 4 bars
|
||||||
|
if treat in ("normal", "inv", "shift"):
|
||||||
|
sections.append((realize(mA, self.root, self.mode, loct), self.harm_rows(hA)))
|
||||||
|
sections.append((realize(mB, self.root, self.mode, loct), self.harm_rows(hB)))
|
||||||
|
elif treat == "aug":
|
||||||
|
a = m_aug(m_take(mA, 32))
|
||||||
|
b = m_aug(m_take(mA[6:], 32)) if False else m_aug(m_take(mA, 64)[len(m_take(mA, 32)):])
|
||||||
|
sections.append((realize(a, self.root, self.mode, loct), self.harm_rows([[0], [0], [3], [4]])))
|
||||||
|
sections.append((realize(b, self.root, self.mode, loct), self.harm_rows([[0], [0], [2], [2]])))
|
||||||
|
elif treat == "dim":
|
||||||
|
d = m_dim(mA) + m_dim(mB)
|
||||||
|
sections.append((realize(d, self.root, self.mode, loct + 1), self.harm_rows([[0, 3], [0, 2], [5, 0], [4, 0]])))
|
||||||
|
elif treat == "frag":
|
||||||
|
# the motif's first bar as a rising sequence
|
||||||
|
frag = m_take(mA, 16)
|
||||||
|
seq = frag + m_shift(frag, 1) + m_shift(frag, 2) + m_take(m_shift(mA, 0)[3:], 16)
|
||||||
|
sections.append((realize(seq, self.root, self.mode, loct), self.harm_rows([[0], [1], [2], [4]])))
|
||||||
|
free = self.harm_rows([[d] for d in prog])
|
||||||
|
mel_style = sp.get("mel_style", "flow")
|
||||||
|
sections.append((self.melody(free, lead, lvol, loct, mel_style), free))
|
||||||
|
if sp.get("long"):
|
||||||
|
free2 = self.harm_rows([[d] for d in sp.get("prog2", prog[::-1])])
|
||||||
|
sections.append((self.melody(free2, lead, lvol, loct, mel_style), free2))
|
||||||
|
# intro: accompaniment only
|
||||||
|
pats = []
|
||||||
|
intro = Pattern()
|
||||||
|
self.accompany(intro, free)
|
||||||
|
self.drums(intro, sp.get("intro_drums", "soft" if sp.get("drums", "none") != "none" else "none"))
|
||||||
|
pats.append(intro)
|
||||||
|
for k, (mel, chords) in enumerate(sections):
|
||||||
|
p = Pattern()
|
||||||
|
p.line(CH_LEAD, mel, lead, lvol)
|
||||||
|
if sp.get("echo"):
|
||||||
|
p.echo(CH_LEAD, CH_COUNTER, sp.get("echo_delay", 3), max(12, lvol // 2), sp.get("echo_inst", SQUARE))
|
||||||
|
elif sp.get("counter") and k % 2 == 1:
|
||||||
|
# counter melody: the bass line of the chords an octave up, long notes
|
||||||
|
cm = " ".join("%s:%d" % (name_of(chord_tones(d, self.mode, self.root, loct - 1, True)[1]), rows) for d, rows in chords)
|
||||||
|
p.line(CH_COUNTER, cm, sp["counter"], sp.get("counter_vol", 24))
|
||||||
|
self.accompany(p, chords)
|
||||||
|
self.drums(p, sp.get("drums", "none"))
|
||||||
|
pats.append(p)
|
||||||
|
idx = [song.add(x) for x in pats]
|
||||||
|
song.order = idx + idx[1:]
|
||||||
|
song.restart = 1
|
||||||
|
return song
|
||||||
|
|
||||||
|
|
||||||
|
def spec(title, seed, key, mode, bpm, prog, **kw):
|
||||||
|
d = dict(title=title, seed=seed, key=key, mode=mode, bpm=bpm, prog=prog)
|
||||||
|
d.update(kw)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
# fifteen build tracks in total (three hand-written + twelve generated), calm and varied
|
||||||
|
BUILD_SPECS = [
|
||||||
|
spec("Dripping Stone", 4, "A", "minor", 78, [0, 5, 3, 4], motif="aug", lead=HARP, lead_vol=34, pad=CHOIR, bass=SUB, arp=PLUCK, arp_step=4, drums="soft", echo=True, echo_inst=HARP),
|
||||||
|
spec("Fungus Gardens", 5, "E", "dorian", 96, [0, 3, 0, 6], motif="normal", lead=PLUCK, lead_vol=34, pad=PAD, bass_pat="R.-.R.F.-.R.O.F.", arp=HARP, drums="bouncy", mel_style="bouncy"),
|
||||||
|
spec("Lantern Path", 6, "G", "minor", 88, [0, 2, 6, 4], motif="frag", lead=SQUARE, lead_vol=32, pad=ORGAN, pad_vol=16, arp=PLUCK, drums="calm", echo=True),
|
||||||
|
spec("Deep Forge", 7, "C", "phrygian", 92, [0, 1, 0, 6], motif="normal", lead=LEAD, lead_vol=36, pad=PAD, bass=SUB, bass_pat="R.R.....R.R.....", drums="tribal", counter=ORGAN),
|
||||||
|
spec("Silent Vault", 8, "F", "minor", 72, [0, 5, 2, 4], motif="aug", lead=BELL, lead_vol=40, pad=CHOIR, bass=SUB, bass_pat="R...............", drums="none"),
|
||||||
|
spec("Crystal Hollow", 9, "B", "minor", 84, [0, 5, 6, 4], motif="inv", lead=BELL, lead_vol=36, pad=PAD, arp=HARP, arp_step=2, arp_order=(0, 1, 2, 3 % 3, 2, 1), drums="soft", echo=True, echo_inst=HARP),
|
||||||
|
spec("Whispering Halls", 10, "D", "dorian", 80, [0, 3, 6, 0], motif="shift", lead=HARP, lead_vol=34, pad=CHOIR, bass=BASS, drums="calm", long=True, prog2=[3, 0, 4, 0]),
|
||||||
|
spec("Old Bones", 11, "A", "phrygian", 86, [0, 1, 6, 0], motif="frag", lead=ORGAN, lead_vol=30, pad=PAD, bass=SUB, drums="tribal", arp=PLUCK, arp_step=4),
|
||||||
|
spec("Ember Chamber", 12, "F#", "minor", 94, [0, 3, 4, 0], motif="normal", lead=LEAD, lead_vol=34, pad=ORGAN, pad_vol=15, arp=PLUCK, drums="calm", counter=SQUARE),
|
||||||
|
spec("Underground River", 13, "E", "minor", 100, [0, 6, 5, 6], motif="dim", lead=HARP, lead_vol=34, pad=CHOIR, bass_pat="R.R.F.R.R.R.F.O.", arp=PLUCK, arp_step=1, arp_vol=10, drums="soft"),
|
||||||
|
spec("Moss and Shadow", 14, "G", "dorian", 90, [0, 3, 0, 4], motif="aug", lead=SQUARE, lead_vol=30, pad=PAD, arp=HARP, drums="bouncy", mel_style="bouncy", echo=True),
|
||||||
|
spec("The Long Dig", 15, "D", "minor", 98, [0, 2, 3, 4], motif="normal", lead=PLUCK, lead_vol=36, pad=PAD, bass_pat="R...R.F.R...F.O.", arp=HARP, drums="bouncy", long=True, prog2=[5, 3, 0, 4]),
|
||||||
|
]
|
||||||
|
|
||||||
|
# seven wave battles (one hand-written + six generated)
|
||||||
|
BATTLE_SPECS = [
|
||||||
|
spec("Steel in the Dark", 20, "E", "minor", 144, [0, 5, 6, 4], motif="normal", lead=LEAD, lead_vol=44, pad=PAD, pad_vol=16, bass_pat="R.R.R.R.R.R.R.R.", arp=PLUCK, arp_step=1, arp_vol=10, drums="rock", lead_oct=5, mel_style="driving", counter=SQUARE),
|
||||||
|
spec("Breach!", 21, "C", "harmonic", 152, [0, 5, 3, 4], motif="dim", lead=SQUARE, lead_vol=42, pad=ORGAN, pad_vol=14, bass_pat="RRRRRRRRRRRRRRRR", drums="driving", mel_style="driving"),
|
||||||
|
spec("Shield Wall", 22, "G", "minor", 138, [0, 3, 6, 4], motif="normal", lead=LEAD, lead_vol=44, pad=CHOIR, bass_pat="R.R.R.F.R.R.F.O.", drums="march", lead_oct=5, mel_style="driving", echo=True),
|
||||||
|
spec("Torches and Pitchforks", 23, "A", "dorian", 148, [0, 3, 0, 6], motif="frag", lead=SQUARE, lead_vol=40, pad=PAD, pad_vol=15, bass_pat="R.R.R.R.R.R.R.R.", arp=PLUCK, arp_step=1, drums="rock", lead_oct=5, mel_style="driving"),
|
||||||
|
spec("Blood on the Stones", 24, "D", "phrygian", 142, [0, 1, 0, 6], motif="inv", lead=LEAD, lead_vol=44, pad=ORGAN, pad_vol=14, bass_pat="RRRRRRRRRRRRRRRR", drums="tribal", lead_oct=5, mel_style="driving", counter=SQUARE),
|
||||||
|
spec("No Retreat", 25, "B", "minor", 156, [0, 5, 3, 4], motif="normal", lead=LEAD, lead_vol=44, pad=PAD, pad_vol=15, bass_pat="R.R.R.R.R.R.R.R.", arp=PLUCK, arp_step=1, arp_vol=10, drums="driving", lead_oct=5, mel_style="driving", long=True, prog2=[5, 6, 0, 4]),
|
||||||
|
]
|
||||||
|
|
||||||
|
# four gate assaults (one hand-written + three generated)
|
||||||
|
ASSAULT_SPECS = [
|
||||||
|
spec("Siege Engine", 30, "D", "harmonic", 160, [0, 5, 1, 4], motif="dim", lead=LEAD, lead_vol=46, pad=ORGAN, pad_vol=15, bass_pat="RRRRRRRRRRRRRRRR", arp=PLUCK, arp_step=1, arp_vol=10, drums="boss", mel_style="driving"),
|
||||||
|
spec("The Gate Trembles", 31, "F", "minor", 164, [0, 6, 5, 4], motif="frag", lead=SQUARE, lead_vol=44, pad=CHOIR, bass_pat="RRRRRRRRRRRRRRRR", drums="driving", lead_oct=5, mel_style="driving", echo=True, echo_delay=2),
|
||||||
|
spec("Last Charge", 32, "C", "minor", 168, [0, 3, 4, 0], motif="normal", lead=LEAD, lead_vol=46, pad=PAD, pad_vol=16, bass_pat="R.R.R.R.R.R.R.R.", arp=PLUCK, arp_step=1, arp_vol=10, drums="boss", lead_oct=5, mel_style="driving", counter=ORGAN),
|
||||||
|
]
|
||||||
|
|
||||||
|
MOOD_SPECS = {
|
||||||
|
"torture": spec("The Rack", 40, "C#", "phrygian", 66, [0, 1, 0, 1], motif="aug", lead=BELL, lead_vol=34, pad=CHOIR, pad_vol=16, bass=SUB, bass_pat="R...............", drums="none", intro_drums="none", arp=PLUCK, arp_step=8, arp_order=(0, 1), arp_vol=12),
|
||||||
|
"temple": spec("Ember Shrine", 41, "D", "lydian", 74, [0, 1, 4, 0], motif="aug", lead=ORGAN, lead_vol=32, pad=CHOIR, pad_vol=20, bass=SUB, bass_pat="R...............", drums="none", intro_drums="none", arp=BELL, arp_step=8, arp_order=(0, 2), arp_vol=18, echo=True, echo_inst=HARP),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ hand-written additions
|
||||||
|
|
||||||
|
def song_title2():
|
||||||
|
"""Throne of Ash: second title theme, the motif slow and majestic on organ and choir."""
|
||||||
|
s = Song("DungeonCraft - Throne of Ash", 92)
|
||||||
|
ch_a = bars(("Dm", 1), ("Bb", 1), ("Gm", 1), ("A", 1))
|
||||||
|
p0 = Pattern()
|
||||||
|
p0.set(0, CH_DRUM, note_num("C4"), TIMP, 50)
|
||||||
|
for c, (n, r) in enumerate([("D3", 0), ("D3", 32)]):
|
||||||
|
p0.set(r, CH_DRUM, note_num("C4"), TIMP, 46)
|
||||||
|
p0.pads(ch_a, vol=20)
|
||||||
|
for k in range(4):
|
||||||
|
p0.set(k * BAR, CH_PAD1, chord_notes(ch_a[k][0], 4)[1], CHOIR, 22)
|
||||||
|
p0.set(k * BAR, CH_PAD2, chord_notes(ch_a[k][0], 4)[2], CHOIR, 22)
|
||||||
|
p0.bass(ch_a, "R...............", inst=SUB, vol=46)
|
||||||
|
p1 = Pattern()
|
||||||
|
p1.line(CH_LEAD, MOTIF_A, ORGAN, 40)
|
||||||
|
p1.line(CH_COUNTER, "D3:16 | G2:8 A2:8 | D3:16 | F2:16", BASS, 40)
|
||||||
|
for k, (c, rr) in enumerate(CHORDS_A):
|
||||||
|
pass
|
||||||
|
p1.pads(CHORDS_A, vol=18)
|
||||||
|
p1.drums(kick="x...............", kv=50)
|
||||||
|
p1.set(0, CH_HAT, note_num("C4"), CRASH, 30)
|
||||||
|
p2 = Pattern()
|
||||||
|
p2.line(CH_LEAD, MOTIF_B, ORGAN, 40)
|
||||||
|
p2.line(CH_COUNTER, "Bb2:16 | D3:16 | A2:16 | D3:16", BASS, 40)
|
||||||
|
p2.pads(CHORDS_B, vol=18)
|
||||||
|
p2.drums(kick="x.......x.......", kv=48)
|
||||||
|
p3 = Pattern()
|
||||||
|
p3.line(CH_LEAD, transpose(MOTIF_A, 12), BELL, 40)
|
||||||
|
p3.echo(CH_LEAD, CH_COUNTER, 4, 20, HARP)
|
||||||
|
p3.pads(CHORDS_A, vol=20)
|
||||||
|
p3.bass(CHORDS_A, "R.......F.......", inst=SUB, vol=44)
|
||||||
|
p3.arp(CHORDS_A, step=2, vol=12, inst=HARP)
|
||||||
|
p3.drums(kick="x.......x.......", hat="x...x...x...x...", hv=10)
|
||||||
|
p = [s.add(x) for x in (p0, p1, p2, p3)]
|
||||||
|
s.order = [p[0], p[1], p[2], p[3], p[2]]
|
||||||
|
s.restart = 1
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def song_boss_paladin():
|
||||||
|
"""The Paladin of Light: the heroes' fanfare (major) clashes with the leitmotif (minor)."""
|
||||||
|
s = Song("DungeonCraft - Paladin of Light", 150)
|
||||||
|
fan_ch = bars(("D", 1), ("G", 1), ("A", 1), ("D", 1))
|
||||||
|
p0 = Pattern()
|
||||||
|
p0.line(CH_LEAD, "D5:2 D5:2 D5:2 A5:10 | G5:2 F#5:2 E5:2 D5:10 | E5:4 F#5:4 G5:4 A5:4 | D6:16", SQUARE, 44)
|
||||||
|
p0.line(CH_COUNTER, "F#4:16 | B4:16 | C#5:16 | F#5:16", SQUARE, 28)
|
||||||
|
p0.pads(fan_ch, vol=18)
|
||||||
|
p0.bass(fan_ch, "R...R...R...R...", vol=46)
|
||||||
|
p0.drums(kick="x.......x.......", snare="....x.xx....xxxx", hat="x.x.x.x.x.x.x.x.", sv=44, hv=12)
|
||||||
|
p0.set(0, CH_HAT, note_num("C4"), CRASH, 36)
|
||||||
|
p1 = Pattern()
|
||||||
|
p1.line(CH_LEAD, transpose(MOTIF_A, 12), LEAD, 46)
|
||||||
|
p1.line(CH_COUNTER, "D4:2 D4:2 D4:2 A4:10 | G4:8 A4:8 | D4:16 | F4:16", SQUARE, 26)
|
||||||
|
p1.pads(CHORDS_A, vol=16)
|
||||||
|
p1.bass(CHORDS_A, "RRRRRRRRRRRRRRRR", vol=44)
|
||||||
|
p1.drums(kick="x.x.x.x.x.x.x.x.", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", sv=48, hv=10)
|
||||||
|
p1.set(0, CH_HAT, note_num("C4"), CRASH, 34)
|
||||||
|
p2 = Pattern()
|
||||||
|
p2.line(CH_LEAD, transpose(MOTIF_B, 12), LEAD, 46)
|
||||||
|
p2.pads(CHORDS_B, vol=16)
|
||||||
|
p2.bass(CHORDS_B, "RRRRRRRRRRRRRRRR", vol=44)
|
||||||
|
p2.arp(CHORDS_B, step=1, vol=10)
|
||||||
|
p2.drums(kick="x.x.x.x.x.x.x.x.", snare="....x.......x.xx", hat="xxxxxxxxxxxxxxxx", sv=48, hv=10)
|
||||||
|
p3 = Pattern()
|
||||||
|
ch3 = bars(("Bb", 1), ("C", 1), ("Dm", 1), ("A", 1))
|
||||||
|
p3.line(CH_LEAD, "D6:4 C6:4 Bb5:4 A5:4 | G5:4 A5:4 Bb5:4 C6:4 | D6:4 A5:4 F5:4 D5:4 | C#5:8 E5:8", LEAD, 46)
|
||||||
|
p3.line(CH_COUNTER, "D5:2 D5:2 D5:2 F5:10 | E5:2 E5:2 E5:2 G5:10 | F5:16 | E5:16", SQUARE, 26)
|
||||||
|
p3.pads(ch3, vol=16)
|
||||||
|
p3.bass(ch3, "RRRRRRRRRRRRRRRR", vol=44)
|
||||||
|
p3.drums(kick="x.x.x.x.x.x.x.x.", snare="....x.......x...", hat="xxxxxxxxxxxxxxxx", sv=50, hv=10)
|
||||||
|
for k in range(4):
|
||||||
|
p3.set(k * BAR + 14, CH_DRUM, note_num("D4"), TIMP, 50)
|
||||||
|
p = [s.add(x) for x in (p0, p1, p2, p3)]
|
||||||
|
s.order = [p[0], p[1], p[2], p[3], p[1], p[2], p[3]]
|
||||||
|
s.restart = 1
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def song_boss_warden():
|
||||||
|
"""The Gate Warden: heavy, slow-fast, organ and timpani; the motif inverted."""
|
||||||
|
s = Song("DungeonCraft - Gate Warden", 132)
|
||||||
|
ch = bars(("Dm", 1), ("Bb", 1), ("Gm", .5), ("A", .5), ("Dm", 1))
|
||||||
|
inv = realize(m_inv(MOTIF_DEG_A), NOTE_BASE["D"], "harmonic", 5)
|
||||||
|
p0 = Pattern()
|
||||||
|
for k in range(4):
|
||||||
|
p0.set(k * BAR, CH_DRUM, note_num("C4"), TIMP, 56)
|
||||||
|
p0.set(k * BAR + 8, CH_DRUM, note_num("C4"), TIMP, 46)
|
||||||
|
p0.line(CH_PAD1, "D4:32 | C#4:32", ORGAN, 24)
|
||||||
|
p0.line(CH_PAD2, "A4:32 | E4:32", ORGAN, 24)
|
||||||
|
p0.bass(ch, "R...............", inst=SUB, vol=50)
|
||||||
|
p1 = Pattern()
|
||||||
|
p1.line(CH_LEAD, inv, LEAD, 46)
|
||||||
|
p1.pads(ch, vol=18)
|
||||||
|
p1.bass(ch, "R.R.R.R.R.R.R.R.", vol=46)
|
||||||
|
p1.drums(kick="x..x..x.x..x..x.", snare="....x.......x...", hat="x.x.x.x.x.x.x.x.", sv=48, hv=12)
|
||||||
|
p1.set(0, CH_HAT, note_num("C4"), CRASH, 34)
|
||||||
|
p2 = Pattern()
|
||||||
|
p2.line(CH_LEAD, transpose(MOTIF_A, 12), ORGAN, 42)
|
||||||
|
p2.echo(CH_LEAD, CH_COUNTER, 2, 22, SQUARE)
|
||||||
|
p2.pads(CHORDS_A, vol=18)
|
||||||
|
p2.bass(CHORDS_A, "RRRRRRRRRRRRRRRR", vol=46)
|
||||||
|
p2.drums(kick="x.x.x.x.x.x.x.x.", snare="....x.......x.xx", hat="xxxxxxxxxxxxxxxx", sv=50, hv=10)
|
||||||
|
p = [s.add(x) for x in (p0, p1, p2)]
|
||||||
|
s.order = [p[0], p[1], p[2], p[1], p[2]]
|
||||||
|
s.restart = 1
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def song_victory2():
|
||||||
|
"""Crown of the Deep: a slower, triumphant victory jingle with bells."""
|
||||||
|
s = Song("DungeonCraft - Crown of the Deep", 100)
|
||||||
|
p0 = Pattern()
|
||||||
|
ch = bars(("D", 1), ("Bb", .5), ("C", .5), ("D", 2))
|
||||||
|
p0.line(CH_LEAD, "A4:4 D5:4 F#5:4 A5:4 | Bb5:6 A5:2 G5:4 E5:4 | D5:32", BELL, 48)
|
||||||
|
p0.line(CH_COUNTER, "D4:16 | D4:8 E4:8 | F#4:32", ORGAN, 30)
|
||||||
|
p0.pads(ch, vol=22)
|
||||||
|
p0.bass(ch, "R.......R.......", inst=SUB, vol=46)
|
||||||
|
for k in (0, 32, 40, 44):
|
||||||
|
p0.set(k, CH_DRUM, note_num("C4"), TIMP, 52)
|
||||||
|
p0.set(32, CH_HAT, note_num("C4"), CRASH, 36)
|
||||||
|
for c in range(CHANNELS):
|
||||||
|
p0.set(63, c, KEY_OFF)
|
||||||
|
silence = Pattern(1)
|
||||||
|
p = [s.add(x) for x in (p0, silence)]
|
||||||
|
s.order = [p[0], p[1]]
|
||||||
|
s.restart = 1
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def generated(spec_):
|
||||||
|
return lambda: Gen(spec_).build()
|
||||||
|
|
||||||
|
|
||||||
|
# name -> (category, song function); the order within a category is the play order
|
||||||
|
SONGS = {}
|
||||||
|
SONGS["title"] = ("title", song_title)
|
||||||
|
SONGS["title2"] = ("title", song_title2)
|
||||||
|
SONGS["build1"] = ("build", song_build1)
|
||||||
|
SONGS["build2"] = ("build", song_build2)
|
||||||
|
SONGS["build3"] = ("build", song_build3)
|
||||||
|
for k, sp_ in enumerate(BUILD_SPECS):
|
||||||
|
SONGS["build%d" % (k + 4)] = ("build", generated(sp_))
|
||||||
|
SONGS["battle1"] = ("battle", song_battle1)
|
||||||
|
for k, sp_ in enumerate(BATTLE_SPECS):
|
||||||
|
SONGS["battle%d" % (k + 3)] = ("battle", generated(sp_))
|
||||||
|
SONGS["battle2"] = ("assault", song_battle2)
|
||||||
|
for k, sp_ in enumerate(ASSAULT_SPECS):
|
||||||
|
SONGS["assault%d" % (k + 2)] = ("assault", generated(sp_))
|
||||||
|
SONGS["boss_paladin"] = ("boss_paladin", song_boss_paladin)
|
||||||
|
SONGS["boss_warden"] = ("boss_warden", song_boss_warden)
|
||||||
|
SONGS["mood_torture"] = ("torture", generated(MOOD_SPECS["torture"]))
|
||||||
|
SONGS["mood_temple"] = ("temple", generated(MOOD_SPECS["temple"]))
|
||||||
|
SONGS["victory"] = ("victory", song_victory)
|
||||||
|
SONGS["victory2"] = ("victory", song_victory2)
|
||||||
|
SONGS["defeat"] = ("defeat", song_defeat)
|
||||||
|
|
||||||
|
|
||||||
|
def write_rust_table(names):
|
||||||
|
"""src/tracks.rs: embedded music with categories (generated, do not edit)"""
|
||||||
|
lines = [
|
||||||
|
"// Generated by tools/compose_music.py - do not edit.",
|
||||||
|
"// All music modules with their category (see audio::Category).",
|
||||||
|
"",
|
||||||
|
"use crate::audio::Category;",
|
||||||
|
"",
|
||||||
|
"pub const TRACKS: [(&str, Category, &[u8]); %d] = [" % len(names),
|
||||||
|
]
|
||||||
|
cat = {"title": "Title", "build": "Build", "battle": "Battle", "assault": "Assault", "boss_paladin": "BossPaladin",
|
||||||
|
"boss_warden": "BossWarden", "torture": "Torture", "temple": "Temple", "victory": "Victory", "defeat": "Defeat"}
|
||||||
|
for n in names:
|
||||||
|
lines.append(' ("%s", Category::%s, include_bytes!("../assets/music/%s.xm")),' % (n, cat[SONGS[n][0]], n))
|
||||||
|
lines.append("];")
|
||||||
|
path = os.path.join(os.path.dirname(__file__), "..", "src", "tracks.rs")
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write("\n".join(lines) + "\n")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.makedirs(OUT_DIR, exist_ok=True)
|
os.makedirs(OUT_DIR, exist_ok=True)
|
||||||
instruments = build_instruments()
|
instruments = build_instruments()
|
||||||
for name, fn in SONGS.items():
|
total = 0
|
||||||
|
for name, (cat, fn) in SONGS.items():
|
||||||
song = fn()
|
song = fn()
|
||||||
size = write_xm(os.path.join(OUT_DIR, name + ".xm"), song, instruments)
|
size = write_xm(os.path.join(OUT_DIR, name + ".xm"), song, instruments)
|
||||||
print("%-8s %3d BPM %2d patterns %6d bytes" % (name, song.bpm, len(song.patterns), size))
|
total += size
|
||||||
|
print("%-13s %-12s %3d BPM %2d patterns %6d bytes %s" % (name, cat, song.bpm, len(song.patterns), size, song.name))
|
||||||
|
write_rust_table(list(SONGS.keys()))
|
||||||
|
print("%d songs, %d KB" % (len(SONGS), total // 1024))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in new issue
Block a user