Compare commits
2
Commits
65ed76865d
...
d7b6d6de7e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7b6d6de7e | ||
|
|
9800ce7156 |
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
+66
-1
@@ -24,6 +24,10 @@ pub enum Category {
|
||||
Defeat,
|
||||
Surface, // above ground: flute, harp, birdsong
|
||||
SurfaceBattle, // fighting above ground
|
||||
// the holidays' pieces (see holiday_category)
|
||||
Christmas,
|
||||
Easter,
|
||||
Halloween,
|
||||
}
|
||||
|
||||
// indices of all tracks of a category, in play order
|
||||
@@ -34,7 +38,27 @@ pub fn tracks_in(c: Category) -> Vec<usize> {
|
||||
// the calm tracks the music player offers (no battle, victory or defeat music)
|
||||
pub fn player_tracks() -> Vec<usize> {
|
||||
use Category as C;
|
||||
TRACKS.iter().enumerate().filter(|(_, t)| matches!(t.1, C::Title | C::Build | C::Surface | C::Torture | C::Temple)).map(|(i, _)| i).collect()
|
||||
TRACKS.iter().enumerate().filter(|(_, t)| matches!(t.1, C::Title | C::Build | C::Surface | C::Torture | C::Temple | C::Christmas | C::Easter | C::Halloween)).map(|(i, _)| i).collect()
|
||||
}
|
||||
|
||||
// the music of a holiday (holiday::current()), if it has one
|
||||
pub fn holiday_music(holiday: u8) -> Option<Category> {
|
||||
match holiday {
|
||||
crate::holiday::CHRISTMAS => Some(Category::Christmas),
|
||||
crate::holiday::EASTER => Some(Category::Easter),
|
||||
crate::holiday::HALLOWEEN => Some(Category::Halloween),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// the category actually played for `want` during a holiday: the title music always becomes the
|
||||
// holiday's piece, the build music every fourth time (`build_picks`: build tracks picked so far)
|
||||
pub fn holiday_category(want: Category, holiday: u8, build_picks: usize) -> Category {
|
||||
match (want, holiday_music(holiday)) {
|
||||
(Category::Title, Some(h)) => h,
|
||||
(Category::Build, Some(h)) if build_picks % 4 == 3 => h,
|
||||
_ => want,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_id(t: usize) -> &'static str {
|
||||
@@ -366,6 +390,47 @@ mod tests {
|
||||
assert_eq!(track_title(track_by_id("build13").unwrap()), "Underground River");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_holiday_has_its_piece_for_the_title_and_now_and_then_the_build_music() {
|
||||
use crate::holiday::{CHRISTMAS, EASTER, HALLOWEEN, NONE};
|
||||
for (h, c, id, title) in [
|
||||
(CHRISTMAS, Category::Christmas, "holiday_christmas", "Frostbound Carol"),
|
||||
(EASTER, Category::Easter, "holiday_easter", "Eggs in the Moss"),
|
||||
(HALLOWEEN, Category::Halloween, "holiday_halloween", "Crypt of Lanterns"),
|
||||
] {
|
||||
let t = track_by_id(id).unwrap();
|
||||
assert_eq!(tracks_in(c), vec![t]);
|
||||
assert_eq!(track_title(t), title);
|
||||
assert!(player_tracks().contains(&t));
|
||||
assert_eq!(holiday_category(Category::Title, h, 0), c);
|
||||
assert_eq!(holiday_category(Category::Title, h, 5), c);
|
||||
let builds: Vec<Category> = (0..8).map(|k| holiday_category(Category::Build, h, k)).collect();
|
||||
assert_eq!(builds.iter().filter(|&&b| b == c).count(), 2, "now and then, not always");
|
||||
assert!(builds.iter().all(|&b| b == c || b == Category::Build));
|
||||
for other in [Category::Battle, Category::Surface, Category::Victory, Category::Torture] {
|
||||
assert_eq!(holiday_category(other, h, 3), other);
|
||||
}
|
||||
// the piece plays: audible, finite and not clipping
|
||||
let m = std::rc::Rc::new(xm::load(TRACKS[t].2).unwrap());
|
||||
let mut p = Player::new(m, 22050.0);
|
||||
let mut buf = vec![0.0f32; 2 * 22050];
|
||||
let mut peak = 0.0f32;
|
||||
for _ in 0..20 {
|
||||
buf.iter_mut().for_each(|v| *v = 0.0);
|
||||
p.render(&mut buf, MUSIC_GAIN);
|
||||
for &v in &buf {
|
||||
assert!(v.is_finite());
|
||||
peak = peak.max(v.abs());
|
||||
}
|
||||
}
|
||||
assert!(peak > 0.05 && peak < 1.0, "{} peak {}", id, peak);
|
||||
}
|
||||
for k in 0..8 {
|
||||
assert_eq!(holiday_category(Category::Title, NONE, k), Category::Title);
|
||||
assert_eq!(holiday_category(Category::Build, NONE, k), Category::Build);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_crossfade_and_sfx() {
|
||||
dc_audio_init(48000.0);
|
||||
|
||||
+8
-1
@@ -70,11 +70,17 @@ pub enum Sfx {
|
||||
Rain,
|
||||
Wind,
|
||||
Spores,
|
||||
// hearts (played by the sound module where the camera looks): the soul core when hurt, the
|
||||
// other masters' hearts (Mossgrim's tree, Lady Nightwell's lily, Saffrax's sun)
|
||||
Heartbeat,
|
||||
HeartMoss,
|
||||
HeartLily,
|
||||
HeartSun,
|
||||
}
|
||||
|
||||
pub const VOICE_SYLLABLES: [Sfx; 6] = [Sfx::Voice1, Sfx::Voice2, Sfx::Voice3, Sfx::Voice4, Sfx::Voice5, Sfx::Voice6];
|
||||
|
||||
pub const SFX_ALL: [Sfx; 49] = [
|
||||
pub const SFX_ALL: [Sfx; 53] = [
|
||||
Sfx::Dig, Sfx::GoldDig, Sfx::DigDone, Sfx::Claim, Sfx::Deposit, Sfx::Build, Sfx::Sell, Sfx::MeleeHit,
|
||||
Sfx::Shoot, Sfx::Death, Sfx::HeroDeath, Sfx::Lightning, Sfx::Heal, Sfx::Summon, Sfx::PortalArrive,
|
||||
Sfx::WaveAlarm, Sfx::WaveStart, Sfx::CoreHit, Sfx::GateHit, Sfx::GateDefenders, Sfx::LevelUp,
|
||||
@@ -82,6 +88,7 @@ pub const SFX_ALL: [Sfx; 49] = [
|
||||
Sfx::Reinforce, Sfx::Cannon, Sfx::Magic, Sfx::Capture, Sfx::Convert, Sfx::Slap, Sfx::Punch,
|
||||
Sfx::Voice1, Sfx::Voice2, Sfx::Voice3, Sfx::Voice4, Sfx::Voice5, Sfx::Voice6,
|
||||
Sfx::Thunder, Sfx::Steal, Sfx::Snare, Sfx::Sermon, Sfx::Rain, Sfx::Wind, Sfx::Spores,
|
||||
Sfx::Heartbeat, Sfx::HeartMoss, Sfx::HeartLily, Sfx::HeartSun,
|
||||
];
|
||||
|
||||
impl Sfx {
|
||||
|
||||
+11
-3
@@ -492,11 +492,12 @@ fn apply_settings(s: &Settings, r3d: &mut R3d) {
|
||||
struct MusicDirector {
|
||||
next: std::collections::HashMap<u8, usize>, // category -> next index within the category
|
||||
current: Option<audio::Category>,
|
||||
build_picks: usize, // build tracks picked so far (during a holiday every fourth is its piece)
|
||||
}
|
||||
|
||||
impl MusicDirector {
|
||||
fn new() -> MusicDirector {
|
||||
MusicDirector { next: std::collections::HashMap::new(), current: None }
|
||||
MusicDirector { next: std::collections::HashMap::new(), current: None, build_picks: 0 }
|
||||
}
|
||||
|
||||
fn pick(&mut self, c: audio::Category) -> usize {
|
||||
@@ -573,11 +574,17 @@ impl MusicDirector {
|
||||
}
|
||||
let rotate = matches!(want, C::Build | C::Title | C::Surface) && self.current == Some(want) && audio::music_loops() >= 2;
|
||||
// after the chosen track was let go, the director picks again
|
||||
let foreign = chosen.is_none() && audio::current_track().map_or(false, |t| crate::tracks::TRACKS.get(t).map_or(false, |x| x.1 != want)) && calm && self.current == Some(want);
|
||||
// (a holiday's piece belongs to the title and build music)
|
||||
let holiday = holiday::current();
|
||||
let fits = |c: C| c == want || (matches!(want, C::Title | C::Build) && audio::holiday_music(holiday) == Some(c));
|
||||
let foreign = chosen.is_none() && audio::current_track().map_or(false, |t| crate::tracks::TRACKS.get(t).map_or(false, |x| !fits(x.1))) && calm && self.current == Some(want);
|
||||
if self.current != Some(want) || rotate || foreign || audio::current_track().is_none() {
|
||||
let before = self.current;
|
||||
self.current = Some(want);
|
||||
let t = self.pick(want);
|
||||
let t = self.pick(audio::holiday_category(want, holiday, self.build_picks));
|
||||
if want == C::Build {
|
||||
self.build_picks += 1;
|
||||
}
|
||||
// between the dungeon and the surface the music changes slowly
|
||||
let fade = if matches!(want, C::Victory | C::Defeat) {
|
||||
1.0
|
||||
@@ -2037,6 +2044,7 @@ app.pending_briefing = briefing_of(&campaign, i);
|
||||
sounds.play(ev, vec2(app.cam.rx, app.cam.rz), right, hear, now);
|
||||
}
|
||||
sounds.ambience(&app.game, (app.cam.rx, app.cam.rz), now);
|
||||
sounds.hearts(&app.game, vec2(app.cam.rx, app.cam.rz), right, now);
|
||||
// a won mission goes into the campaign progress (with the carry-over); a co-op mission of an
|
||||
// editor campaign keeps no progress (its index is not a built-in mission)
|
||||
if app.coop_editor {
|
||||
|
||||
+38
@@ -189,6 +189,44 @@ fn def(s: Sfx, r: f32) -> Vec<f32> {
|
||||
Sfx::Wind => l(&[seg(Noise, 300.0, 700.0, 0.6, 0.03, -3.0), seg(Noise, 700.0, 500.0, 1.4, 0.18, 0.2), seg(Noise, 500.0, 250.0, 0.6, 0.15, 3.0)], 82),
|
||||
// the spores of a grove: a faint chime
|
||||
Sfx::Spores => l(&arp(Sine, &[2093.0, 2637.0, 3136.0, 2637.0], 0.09, 0.07), 83),
|
||||
// the soul core: a soft, low lub-dub (a quiet click on top so small speakers carry it)
|
||||
Sfx::Heartbeat => mix(vec![
|
||||
l(&[seg(Tri, 78.0, 50.0, 0.11, 0.55, 16.0), seg(Tri, 50.0, 50.0, 0.06, 0.0, 0.0), seg(Tri, 70.0, 46.0, 0.12, 0.38, 18.0)], 84),
|
||||
l(&[seg(Noise, 600.0, 250.0, 0.04, 0.07, 45.0), seg(Noise, 250.0, 250.0, 0.13, 0.0, 0.0), seg(Noise, 500.0, 220.0, 0.04, 0.05, 45.0)], 85),
|
||||
]),
|
||||
// Mossgrim's tree: old wood creaking under the beat, the moss and twigs rustling
|
||||
Sfx::HeartMoss => mix(vec![
|
||||
l(&[seg(Tri, 90.0, 60.0, 0.12, 0.4, 14.0)], 86),
|
||||
l(&[seg(Square(0.5), 40.0, 40.0, 0.08, 0.0, 0.0), vib(seg(Square(0.3), 36.0, 54.0, 0.45, 0.07, 3.0), 0.12), seg(Square(0.3), 54.0, 44.0, 0.2, 0.05, 6.0)], 87),
|
||||
l(&[seg(Noise, 2600.0, 2600.0, 0.25, 0.0, 0.0), seg(Noise, 2600.0, 3400.0, 0.2, 0.05, -4.0), seg(Noise, 3400.0, 1800.0, 0.5, 0.06, 5.0)], 88),
|
||||
]),
|
||||
// Lady Nightwell's lily: a soft beat and a glassy chime (inharmonic partials ringing out)
|
||||
Sfx::HeartLily => mix(vec![
|
||||
l(&[seg(Sine, 110.0, 70.0, 0.12, 0.35, 14.0)], 89),
|
||||
l(&[seg(Sine, 1568.0, 1568.0, 1.3, 0.09, 3.0)], 90),
|
||||
l(&[seg(Sine, 2470.0, 2470.0, 1.0, 0.06, 4.5)], 91),
|
||||
l(&[seg(Sine, 3729.0, 3729.0, 0.7, 0.04, 6.0)], 92),
|
||||
l(&[seg(Sine, 2093.0, 2093.0, 0.18, 0.0, 0.0), seg(Sine, 2093.0, 2093.0, 0.9, 0.05, 4.0)], 93),
|
||||
]),
|
||||
// Saffrax's sun: a warm beat, then bubbling and a faint fizz like a boiling crucible
|
||||
Sfx::HeartSun => mix(vec![
|
||||
l(&[seg(Tri, 100.0, 64.0, 0.12, 0.42, 14.0)], 94),
|
||||
l(&[
|
||||
seg(Sine, 400.0, 400.0, 0.1, 0.0, 0.0),
|
||||
seg(Sine, 380.0, 720.0, 0.035, 0.1, 25.0),
|
||||
seg(Sine, 380.0, 380.0, 0.05, 0.0, 0.0),
|
||||
seg(Sine, 520.0, 950.0, 0.03, 0.08, 25.0),
|
||||
seg(Sine, 380.0, 380.0, 0.03, 0.0, 0.0),
|
||||
seg(Sine, 300.0, 600.0, 0.04, 0.1, 25.0),
|
||||
seg(Sine, 380.0, 380.0, 0.07, 0.0, 0.0),
|
||||
seg(Sine, 610.0, 1100.0, 0.03, 0.07, 25.0),
|
||||
seg(Sine, 380.0, 380.0, 0.04, 0.0, 0.0),
|
||||
seg(Sine, 450.0, 820.0, 0.035, 0.08, 25.0),
|
||||
seg(Sine, 380.0, 380.0, 0.06, 0.0, 0.0),
|
||||
seg(Sine, 700.0, 1250.0, 0.03, 0.06, 25.0),
|
||||
], 95),
|
||||
l(&[seg(Noise, 9000.0, 9000.0, 0.1, 0.0, 0.0), seg(Noise, 9000.0, 11000.0, 0.25, 0.025, -3.0), seg(Noise, 11000.0, 8000.0, 0.35, 0.03, 6.0)], 96),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+137
-1
@@ -11,11 +11,90 @@ pub struct SoundBank {
|
||||
next_rain: f64,
|
||||
next_wind: f64,
|
||||
next_spores: f64,
|
||||
core_beat: i64, // the soul core's last heard beat
|
||||
rival_beats: Vec<(u8, i64, f64)>, // per rival side: last beat seen, next time its sound may play
|
||||
}
|
||||
|
||||
// how far (tiles) a heart is heard from the camera's target: full up to NEAR, silent beyond FAR
|
||||
const HEART_NEAR: f32 = 3.0;
|
||||
const HEART_FAR: f32 = 10.0;
|
||||
// the soul core beats audibly once it has lost this much of its life
|
||||
const CORE_HURT: f32 = 0.85;
|
||||
// at least this many seconds between two sounds of one rival heart
|
||||
const RIVAL_HEART_GAP: f64 = 5.0;
|
||||
|
||||
// a heart's beats per second by its life (0..1), as render3d's draw_heart shows them
|
||||
fn heart_rate(hp: f32) -> f64 {
|
||||
0.9 + (1.0 - hp.clamp(0.0, 1.0) as f64) * 1.6
|
||||
}
|
||||
|
||||
impl SoundBank {
|
||||
pub fn new() -> SoundBank {
|
||||
SoundBank { last: vec![-1.0; SFX_ALL.len()], next_rain: 0.0, next_wind: 0.0, next_spores: 0.0 }
|
||||
SoundBank { last: vec![-1.0; SFX_ALL.len()], next_rain: 0.0, next_wind: 0.0, next_spores: 0.0, core_beat: i64::MIN, rival_beats: Vec::new() }
|
||||
}
|
||||
|
||||
// the hearts near the camera, in time with their drawn beat (`now`: the clock the renderer
|
||||
// animates with): the soul core thumps softly once it is hurt (louder the worse), the other
|
||||
// masters' hearts give their own sound every few beats. Returns what was played.
|
||||
pub fn hearts(&mut self, g: &crate::game::Game, listener: Vec2, right: Vec2, now: f64) -> Vec<(Sfx, f32)> {
|
||||
let mut out = Vec::new();
|
||||
if g.state != 0 {
|
||||
return out;
|
||||
}
|
||||
let place = |x: f32, z: f32| {
|
||||
let d = vec2(x, z) - listener;
|
||||
let f = 1.0 - ((d.length() - HEART_NEAR) / (HEART_FAR - HEART_NEAR)).clamp(0.0, 1.0);
|
||||
(f, (d.dot(right) / HEART_FAR).clamp(-1.0, 1.0) * 0.6)
|
||||
};
|
||||
// the soul core
|
||||
let hp = (g.core_hp / crate::game::CORE_HP).clamp(0.0, 1.0);
|
||||
let beat = (now * heart_rate(hp)).floor() as i64;
|
||||
if beat != self.core_beat {
|
||||
let first = self.core_beat == i64::MIN;
|
||||
self.core_beat = beat;
|
||||
let (f, pan) = place(g.map.core.0 as f32 + 1.5, g.map.core.1 as f32 + 1.5);
|
||||
if !first && hp < CORE_HURT && g.core_hp > 0.0 && f > 0.05 {
|
||||
let vol = f * (0.25 + 0.45 * (CORE_HURT - hp) / CORE_HURT);
|
||||
audio::play_sfx(Sfx::Heartbeat.index(), vol, pan);
|
||||
out.push((Sfx::Heartbeat, vol));
|
||||
}
|
||||
}
|
||||
// the other masters' hearts (drawn with their clock shifted by side * 0.37)
|
||||
for rv in g.rivals.iter().filter(|r| r.alive) {
|
||||
let sfx = match rv.side {
|
||||
2 => Sfx::HeartMoss,
|
||||
3 => Sfx::HeartLily,
|
||||
4 => Sfx::HeartSun,
|
||||
_ => continue,
|
||||
};
|
||||
let hp = (rv.hp / crate::rival::RIVAL_CORE_HP).clamp(0.0, 1.0);
|
||||
let beat = ((now + rv.side as f64 * 0.37) * heart_rate(hp)).floor() as i64;
|
||||
let k = match self.rival_beats.iter().position(|b| b.0 == rv.side) {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
// the first sound only after a moment (no burst when the camera arrives)
|
||||
self.rival_beats.push((rv.side, beat, now + 1.5));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let (_, last, next) = self.rival_beats[k];
|
||||
if beat == last {
|
||||
continue;
|
||||
}
|
||||
self.rival_beats[k].1 = beat;
|
||||
let (cx, cz) = rv.centre();
|
||||
let shown = g.map.shown(crate::map::idx(rv.core.0 + 1, rv.core.1 + 1));
|
||||
let (f, pan) = place(cx, cz);
|
||||
if now < next || !shown || f <= 0.05 {
|
||||
continue;
|
||||
}
|
||||
// every few seconds, a little irregular so it never sounds like a metronome
|
||||
self.rival_beats[k].2 = now + RIVAL_HEART_GAP + ((beat as f64) * 1.7).sin().abs() * 2.5;
|
||||
let vol = 0.5 * f;
|
||||
audio::play_sfx(sfx.index(), vol, pan);
|
||||
out.push((sfx, vol));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// background sounds where the camera looks: rain and wind over the land above ground (the
|
||||
@@ -87,3 +166,60 @@ impl SoundBank {
|
||||
audio::play_sfx(i, vol, pan);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::game::{Game, CORE_HP};
|
||||
|
||||
// plays `secs` seconds at 60 frames per second with the camera at `cam`
|
||||
fn run(b: &mut SoundBank, g: &Game, cam: Vec2, from: f64, secs: f64) -> Vec<(Sfx, f32)> {
|
||||
let mut out = Vec::new();
|
||||
let mut t = from;
|
||||
while t < from + secs {
|
||||
out.extend(b.hearts(g, cam, vec2(1.0, 0.0), t));
|
||||
t += 1.0 / 60.0;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hurt_soul_core_beats_and_the_rival_hearts_sound_now_and_then() {
|
||||
let mut g = Game::new(7, 64, 64);
|
||||
let core = vec2(g.map.core.0 as f32 + 1.5, g.map.core.1 as f32 + 1.5);
|
||||
let mut b = SoundBank::new();
|
||||
// a whole heart is silent
|
||||
assert!(run(&mut b, &g, core, 0.0, 6.0).is_empty());
|
||||
// a hurt one thumps with its drawn beat, louder the worse, and only near the camera
|
||||
g.core_hp = CORE_HP * 0.5;
|
||||
let half = run(&mut b, &g, core, 10.0, 10.0);
|
||||
let rate = heart_rate(0.5);
|
||||
assert!(half.iter().all(|s| s.0 == Sfx::Heartbeat));
|
||||
assert!((half.len() as f64 - rate * 10.0).abs() <= 1.5, "{} beats", half.len());
|
||||
g.core_hp = CORE_HP * 0.1;
|
||||
let worse = run(&mut b, &g, core, 30.0, 4.0);
|
||||
assert!(worse[0].1 > half[0].1 && worse.iter().all(|s| s.1 <= 0.75));
|
||||
assert!(run(&mut b, &g, core + vec2(30.0, 0.0), 40.0, 5.0).is_empty());
|
||||
g.core_hp = CORE_HP;
|
||||
// the rivals' hearts: each its own sound, not more often than every RIVAL_HEART_GAP seconds
|
||||
g.add_rival(2, 50, 50, 1);
|
||||
g.add_rival(3, 50, 14, 1);
|
||||
g.add_rival(4, 14, 50, 1);
|
||||
g.map.explored.clear(); // no fog: every heart in view
|
||||
for (side, sfx) in [(2, Sfx::HeartMoss), (3, Sfx::HeartLily), (4, Sfx::HeartSun)] {
|
||||
let rv = g.rivals.iter().find(|r| r.side == side).unwrap();
|
||||
let (x, z) = rv.centre();
|
||||
let mut b = SoundBank::new();
|
||||
let played = run(&mut b, &g, vec2(x, z), 100.0, 30.0);
|
||||
assert!(played.len() >= 3 && played.len() <= 30 / RIVAL_HEART_GAP as usize + 1, "{:?}: {}", sfx, played.len());
|
||||
assert!(played.iter().all(|s| s.0 == sfx && s.1 <= 0.5));
|
||||
// far away or hidden in the fog: silence
|
||||
assert!(run(&mut b, &g, vec2(x + 40.0, z), 200.0, 20.0).iter().all(|s| s.0 != sfx));
|
||||
}
|
||||
g.init_fog();
|
||||
let rv = &g.rivals[0];
|
||||
let mut b = SoundBank::new();
|
||||
assert!(!g.map.shown(crate::map::idx(rv.core.0 + 1, rv.core.1 + 1)));
|
||||
assert!(run(&mut b, &g, vec2(rv.centre().0, rv.centre().1), 300.0, 20.0).is_empty());
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::audio::Category;
|
||||
|
||||
pub const TRACKS: [(&str, Category, &[u8]); 47] = [
|
||||
pub const TRACKS: [(&str, Category, &[u8]); 50] = [
|
||||
("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")),
|
||||
@@ -51,4 +51,7 @@ pub const TRACKS: [(&str, Category, &[u8]); 47] = [
|
||||
("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")),
|
||||
("holiday_christmas", Category::Christmas, include_bytes!("../assets/music/holiday_christmas.xm")),
|
||||
("holiday_easter", Category::Easter, include_bytes!("../assets/music/holiday_easter.xm")),
|
||||
("holiday_halloween", Category::Halloween, include_bytes!("../assets/music/holiday_halloween.xm")),
|
||||
];
|
||||
@@ -1130,7 +1130,8 @@ mod tests {
|
||||
fn render_all_songs() {
|
||||
for (k, (name, _, data)) in crate::tracks::TRACKS.iter().enumerate() {
|
||||
let m = Rc::new(load(data).expect("load"));
|
||||
assert!(m.channels == 8 || (m.channels == 10 && name.starts_with("surface")), "{} {}", name, m.channels);
|
||||
// birds and wind above ground and in the spring
|
||||
assert!(m.channels == 8 || (m.channels == 10 && (name.starts_with("surface") || *name == "holiday_easter")), "{} {}", name, m.channels);
|
||||
let mut p = Player::new(m, 48000.0);
|
||||
let mut buf = vec![0.0f32; 48000 * 2];
|
||||
let mut peak = 0.0f32;
|
||||
|
||||
+237
-2
@@ -75,6 +75,10 @@ CHORDS = {
|
||||
"C": ["C", "E", "G"],
|
||||
"Am": ["A", "C", "E"],
|
||||
"C7": ["C", "E", "Bb"],
|
||||
"E": ["E", "G#", "B"],
|
||||
"Em": ["E", "G", "B"],
|
||||
"Bm": ["B", "D", "F#"],
|
||||
"Eb": ["Eb", "G", "Bb"],
|
||||
}
|
||||
|
||||
|
||||
@@ -304,6 +308,15 @@ def wind():
|
||||
return out[: n - k]
|
||||
|
||||
|
||||
def harpsichord_cycle(amp=0.4):
|
||||
# bright and nasal: a narrow pulse with a saw on top (many strong harmonics, like a plucked quill)
|
||||
raw = [(1.0 if i < CYCLE * 0.18 else -0.22) + 0.6 * (2.0 * i / CYCLE - 1.0) for i in range(CYCLE)]
|
||||
mean = sum(raw) / CYCLE
|
||||
raw = [v - mean for v in raw]
|
||||
m = max(abs(v) for v in raw)
|
||||
return [amp * v / m for v in raw]
|
||||
|
||||
|
||||
def build_instruments():
|
||||
return [
|
||||
Instrument("Lead pulse 25%", Sample("pulse25", pulse(0.25), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
|
||||
@@ -336,12 +349,14 @@ def build_instruments():
|
||||
Instrument("Bird", Sample("bird", bird(), relnote=12)),
|
||||
Instrument("Wind", Sample("wind", wind(), loop=(0, len(wind())), relnote=12),
|
||||
env=[(0, 0), (60, 48), (120, 40)], sustain=2, fadeout=200),
|
||||
Instrument("Harpsichord", Sample("harpsichord", harpsichord_cycle(), loop=(0, CYCLE), relnote=CYCLE_REL, finetune=CYCLE_FT),
|
||||
env=[(0, 64), (2, 46), (14, 22), (40, 8), (70, 0)], fadeout=2400),
|
||||
]
|
||||
|
||||
|
||||
LEAD, SQUARE, PAD, BASS, PLUCK, KICK, SNARE, HAT = range(1, 9)
|
||||
ORGAN, BELL, CHOIR, SUB, TOM, CRASH, HARP, TIMP = range(9, 17)
|
||||
FLUTE, BIRD, WIND = range(17, 20)
|
||||
FLUTE, BIRD, WIND, HARPSI = range(17, 21)
|
||||
# channel layout (the last two only for songs above ground: birds and wind)
|
||||
CH_LEAD, CH_COUNTER, CH_PAD1, CH_PAD2, CH_BASS, CH_ARP, CH_DRUM, CH_HAT, CH_BIRD, CH_WIND = range(10)
|
||||
CHANNELS = 10
|
||||
@@ -1283,6 +1298,222 @@ def song_victory2():
|
||||
return s
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ holiday pieces
|
||||
# During a holiday the title music (and now and then the build music) plays the holiday's piece
|
||||
# (see audio::holiday_category). Each one quotes the leitmotif in its own colours.
|
||||
|
||||
|
||||
def sleigh(pat, vol=8):
|
||||
"""Sleigh bells: the hihat pitched up, every eighth, louder on the beats."""
|
||||
for r in range(0, pat.rows, 2):
|
||||
pat.set(r, CH_HAT, note_num("C6"), HAT, vol + (6 if r % 4 == 0 else 0))
|
||||
|
||||
|
||||
def organ_pads(pat, chords, vol=18, octave=4):
|
||||
r = 0
|
||||
for name, rows in chords:
|
||||
notes = chord_notes(name, octave)
|
||||
pat.set(r, CH_PAD1, notes[1], ORGAN, vol)
|
||||
pat.set(r, CH_PAD2, notes[2], ORGAN, vol)
|
||||
r += rows
|
||||
|
||||
|
||||
def choir_pads(pat, chords, vol=18, octave=4):
|
||||
r = 0
|
||||
for name, rows in chords:
|
||||
notes = chord_notes(name, octave)
|
||||
pat.set(r, CH_PAD1, notes[1], CHOIR, vol)
|
||||
pat.set(r, CH_PAD2, notes[2], CHOIR, vol)
|
||||
r += rows
|
||||
|
||||
|
||||
def song_christmas():
|
||||
"""Frostbound Carol: a dark carol in D minor on bells and choir, sleigh bells, a turn to
|
||||
D major in the middle that falls back into the minor; the leitmotif rung on the bells."""
|
||||
s = Song("Frostbound Carol", 100)
|
||||
mel_a = "A4:4 A4:4 Bb4:4 A4:4 | G4:4 F4:4 E4:8 | F4:4 G4:4 A4:4 D5:4 | C#5:12 -:4"
|
||||
mel_b = "D5:4 C5:4 Bb4:4 A4:4 | Bb4:4 A4:4 G4:8 | F4:4 E4:4 D4:4 F4:4 | E4:6 C#4:2 D4:8"
|
||||
mel_c = "F#4:4 A4:4 D5:8 | B4:4 A4:4 G4:8 | F#4:4 G4:4 A4:4 B4:4 | A4:12 -:4"
|
||||
ch_a = bars(("Dm", 1), ("Gm", .5), ("A", .5), ("Dm", .5), ("F", .5), ("A", 1))
|
||||
ch_b = bars(("Bb", 1), ("Gm", 1), ("Dm", 1), ("A", .5), ("Dm", .5))
|
||||
ch_c = bars(("D", 1), ("G", 1), ("D", .5), ("G", .5), ("A", 1))
|
||||
ch_i = bars(("Dm", 1), ("Bb", 1), ("Gm", 1), ("A", 1))
|
||||
# intro: the bells toll over the choir, timpani like distant steps in the snow
|
||||
p0 = Pattern()
|
||||
p0.line(CH_LEAD, "D5:16 | D5:16 | D5:16 | C#5:16", BELL, 36)
|
||||
choir_pads(p0, ch_i, vol=20)
|
||||
p0.bass(ch_i, "R...............", inst=SUB, vol=42)
|
||||
for k in (0, 32):
|
||||
p0.set(k, CH_DRUM, note_num("C4"), TIMP, 44)
|
||||
p1 = Pattern()
|
||||
p1.line(CH_LEAD, transpose(mel_a, 12), BELL, 44)
|
||||
choir_pads(p1, ch_a, vol=18)
|
||||
p1.bass(ch_a, "R.......F.......", vol=40)
|
||||
sleigh(p1, 6)
|
||||
p2 = Pattern()
|
||||
p2.line(CH_LEAD, transpose(mel_b, 12), BELL, 44)
|
||||
choir_pads(p2, ch_b, vol=18)
|
||||
p2.bass(ch_b, "R.......F.......", vol=40)
|
||||
sleigh(p2, 6)
|
||||
# second verse: the organ sings, the harp ripples, a soft kick
|
||||
p3 = Pattern()
|
||||
p3.line(CH_LEAD, mel_a, ORGAN, 38)
|
||||
p3.line(CH_COUNTER, "D4:16 | Bb3:8 C#4:8 | D4:8 C4:8 | C#4:16", CHOIR, 22)
|
||||
p3.pads(ch_a, vol=14)
|
||||
p3.bass(ch_a, "R...R.F.R...F.O.", vol=40)
|
||||
p3.arp(ch_a, step=2, vol=11, inst=HARP)
|
||||
p3.drums(kick="x.......x.......", kv=44)
|
||||
sleigh(p3, 8)
|
||||
p4 = Pattern()
|
||||
p4.line(CH_LEAD, mel_b, ORGAN, 38)
|
||||
p4.line(CH_COUNTER, "F4:16 | D4:16 | A3:16 | G3:8 F3:8", CHOIR, 22)
|
||||
p4.pads(ch_b, vol=14)
|
||||
p4.bass(ch_b, "R...R.F.R...F.O.", vol=40)
|
||||
p4.arp(ch_b, step=2, vol=11, inst=HARP)
|
||||
p4.drums(kick="x.......x.......", kv=44)
|
||||
sleigh(p4, 8)
|
||||
# the turn to major: candle light for a moment
|
||||
p5 = Pattern()
|
||||
p5.line(CH_LEAD, transpose(mel_c, 12), BELL, 46)
|
||||
p5.line(CH_COUNTER, "D4:16 | D4:16 | D4:8 B3:8 | C#4:16", ORGAN, 26)
|
||||
choir_pads(p5, ch_c, vol=18)
|
||||
p5.bass(ch_c, "R.......F.......", vol=42)
|
||||
p5.arp(ch_c, step=4, vol=10, inst=HARP)
|
||||
sleigh(p5, 8)
|
||||
p5.set(48, CH_DRUM, note_num("C4"), TIMP, 40)
|
||||
# the leitmotif on the bells, echoed by the harp
|
||||
p6 = Pattern()
|
||||
p6.line(CH_LEAD, transpose(MOTIF_A, 12), BELL, 42)
|
||||
p6.echo(CH_LEAD, CH_COUNTER, 4, 18, HARP)
|
||||
choir_pads(p6, CHORDS_A, vol=18)
|
||||
p6.bass(CHORDS_A, "R.......F.......", inst=SUB, vol=42)
|
||||
sleigh(p6, 6)
|
||||
p7 = Pattern()
|
||||
p7.line(CH_LEAD, transpose(MOTIF_B, 12), BELL, 42)
|
||||
p7.echo(CH_LEAD, CH_COUNTER, 4, 18, HARP)
|
||||
choir_pads(p7, CHORDS_B, vol=18)
|
||||
p7.bass(CHORDS_B, "R.......F.......", inst=SUB, vol=42)
|
||||
sleigh(p7, 6)
|
||||
p7.set(48, CH_DRUM, note_num("C4"), TIMP, 42)
|
||||
p = [s.add(x) for x in (p0, p1, p2, p3, p4, p5, p6, p7)]
|
||||
s.order = [p[0], p[1], p[2], p[3], p[4], p[5], p[2], p[6], p[7], p[1], p[2]]
|
||||
s.restart = 1
|
||||
return s
|
||||
|
||||
|
||||
def song_easter():
|
||||
"""Eggs in the Moss: light and playful in G major, flute and harp, birdsong, a bouncing
|
||||
beat; the leitmotif turned into a spring tune."""
|
||||
s = Song("Eggs in the Moss", 120)
|
||||
rng = random.Random(61)
|
||||
mel_a = "G4:2 B4:2 D5:4 B4:2 D5:2 G5:4 | E5:2 D5:2 C5:2 B4:2 A4:8 | F#4:2 A4:2 D5:4 C5:2 B4:2 A4:4 | B4:4 G4:4 G4:8"
|
||||
mel_b = "E5:4 D5:2 C5:2 B4:4 A4:4 | D5:4 C5:2 B4:2 A4:8 | G4:2 A4:2 B4:2 C5:2 D5:4 E5:4 | D5:8 -:8"
|
||||
ch_a = bars(("G", 1), ("C", .5), ("D", .5), ("D", 1), ("G", 1))
|
||||
ch_b = bars(("Em", 1), ("D", 1), ("C", .5), ("G", .5), ("D", 1))
|
||||
mot_a = realize(MOTIF_DEG_A, NOTE_BASE["G"], "major", 4)
|
||||
mot_b = realize(MOTIF_DEG_B, NOTE_BASE["G"], "major", 4)
|
||||
ch_ma = bars(("G", 1), ("C", .5), ("D", .5), ("G", 1), ("Bm", 1))
|
||||
ch_mb = bars(("Em", 1), ("G", 1), ("D", 1), ("G", 1))
|
||||
bouncy = dict(kick="x.......x.......", snare="............x...", hat="..x...x...x...x.", hv=12, sv=28)
|
||||
|
||||
def birds(pat):
|
||||
for bar in range(pat.rows // BAR):
|
||||
if rng.random() < 0.6:
|
||||
row = bar * BAR + rng.choice([2, 5, 7, 10, 13])
|
||||
pat.set(row, CH_BIRD, note_num(rng.choice(["C4", "D4", "E4", "G4", "A4", "C5"])), BIRD, 12 + rng.randrange(10))
|
||||
|
||||
def verse(mel, ch, lead, lvol, echo=None):
|
||||
p = Pattern()
|
||||
p.line(CH_LEAD, mel, lead, lvol)
|
||||
if echo:
|
||||
p.echo(CH_LEAD, CH_COUNTER, 2, 16, echo)
|
||||
p.pads(ch, vol=13)
|
||||
p.bass(ch, "R.-.R.F.-.R.O.F.", vol=40)
|
||||
p.arp(ch, step=2, vol=11, inst=HARP, order=(0, 1, 2, 1))
|
||||
p.drums(**bouncy)
|
||||
birds(p)
|
||||
return p
|
||||
|
||||
p0 = Pattern()
|
||||
choir_pads(p0, ch_a, vol=12)
|
||||
p0.bass(ch_a, "R.......F.......", inst=SUB, vol=38)
|
||||
p0.arp(ch_a, step=2, vol=11, inst=HARP, order=(0, 1, 2, 1))
|
||||
p0.drums(hat="..x...x...x...x.", hv=10)
|
||||
birds(p0)
|
||||
p0.set(0, CH_WIND, note_num("C4"), WIND, 10)
|
||||
p1 = verse(mel_a, ch_a, FLUTE, 40)
|
||||
p2 = verse(mel_b, ch_b, FLUTE, 40)
|
||||
p3 = verse(transpose(mel_a, 12), ch_a, PLUCK, 34, HARP)
|
||||
p4 = verse(transpose(mel_b, 12), ch_b, PLUCK, 34, HARP)
|
||||
p5 = verse(mot_a, ch_ma, FLUTE, 40, HARP)
|
||||
p6 = verse(mot_b, ch_mb, FLUTE, 40, HARP)
|
||||
p = [s.add(x) for x in (p0, p1, p2, p3, p4, p5, p6)]
|
||||
s.order = [p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[3], p[4], p[1], p[2]]
|
||||
s.restart = 1
|
||||
return s
|
||||
|
||||
|
||||
def song_halloween():
|
||||
"""Crypt of Lanterns: a spooky dance in D harmonic minor, harpsichord and organ, midnight
|
||||
bells, toms like knocking on coffin lids; the leitmotif on the harpsichord."""
|
||||
s = Song("Crypt of Lanterns", 120)
|
||||
mel_a = "D4:4 F4:4 G#4:4 A4:4 | Bb4:6 A4:2 G#4:8 | A4:4 C5:4 Bb4:4 G4:4 | A4:16"
|
||||
mel_b = "D5:2 Eb5:2 D5:4 A4:4 F4:4 | G4:2 F4:2 E4:4 C#4:8 | D4:2 E4:2 F4:2 G4:2 A4:2 Bb4:2 C#5:4 | D5:16"
|
||||
ch_a = bars(("Dm", 1), ("E", 1), ("Gm", 1), ("A", 1))
|
||||
ch_b = bars(("Dm", 1), ("A", 1), ("Gm", .5), ("A", .5), ("Dm", 1))
|
||||
|
||||
def knock(pat):
|
||||
for bar in range(pat.rows // BAR):
|
||||
pat.set(bar * BAR + 14, CH_DRUM, note_num("A3"), TOM, 36)
|
||||
|
||||
# midnight: twelve... well, four bells over an organ drone
|
||||
p0 = Pattern()
|
||||
p0.line(CH_LEAD, "D4:16 | D4:16 | D4:16 | C#4:16", BELL, 36)
|
||||
p0.line(CH_PAD1, "D3:64", ORGAN, 20)
|
||||
p0.line(CH_PAD2, "A3:32 | G#3:32", ORGAN, 18)
|
||||
p0.bass(ch_a, "R...............", inst=SUB, vol=44)
|
||||
p0.arp(ch_a, step=4, vol=9, octave=4, inst=HARPSI)
|
||||
|
||||
def verse(mel, ch, lead, lvol, heavy=False):
|
||||
p = Pattern()
|
||||
p.line(CH_LEAD, mel, lead, lvol)
|
||||
organ_pads(p, ch, vol=16)
|
||||
p.bass(ch, "R.......R...F..." if not heavy else "R.R.....R.R.F...", inst=SUB, vol=44)
|
||||
p.arp(ch, step=1, vol=10, octave=4, order=(0, 1, 2, 1), inst=HARPSI)
|
||||
if heavy:
|
||||
p.drums(kick="x.....x.x.......", snare="....x.......x...", hat="..x...x...x...x.", kv=52, sv=34, hv=10)
|
||||
else:
|
||||
p.drums(kick="x.......x.......", hat="..x...x...x...x.", kv=48, hv=9)
|
||||
knock(p)
|
||||
return p
|
||||
|
||||
p1 = verse(transpose(mel_a, 12), ch_a, HARPSI, 40)
|
||||
p2 = verse(transpose(mel_b, 12), ch_b, HARPSI, 40)
|
||||
p3 = verse(mel_a, ch_a, ORGAN, 36, heavy=True)
|
||||
p3.line(CH_COUNTER, "A3:16 | G#3:16 | Bb3:16 | C#4:16", HARPSI, 24)
|
||||
p4 = verse(mel_b, ch_b, ORGAN, 36, heavy=True)
|
||||
p4.line(CH_COUNTER, "F3:16 | E3:16 | D3:8 E3:8 | F3:16", HARPSI, 24)
|
||||
# the leitmotif on the harpsichord with its own ghostly echo
|
||||
p5 = verse(transpose(MOTIF_A, 12), CHORDS_A, HARPSI, 40)
|
||||
p5.echo(CH_LEAD, CH_COUNTER, 3, 16, HARPSI)
|
||||
p6 = verse(transpose(MOTIF_B, 12), CHORDS_B, HARPSI, 40)
|
||||
p6.echo(CH_LEAD, CH_COUNTER, 3, 16, HARPSI)
|
||||
# creeping down: a chromatic organ line over a pedal, the bells again
|
||||
p7 = Pattern()
|
||||
p7.line(CH_LEAD, "A4:8 G#4:8 | G4:8 F#4:8 | F4:8 E4:8 | Eb4:8 D4:8", ORGAN, 34)
|
||||
p7.line(CH_PAD1, "D3:64", ORGAN, 16)
|
||||
p7.line(CH_PAD2, "A3:64", ORGAN, 14)
|
||||
p7.bass(ch_a, "R...............", inst=SUB, vol=44)
|
||||
for k in (0, 32):
|
||||
p7.set(k, CH_COUNTER, note_num("D5"), BELL, 30)
|
||||
p7.set(k, CH_DRUM, note_num("C4"), TIMP, 46)
|
||||
p7.arp(ch_a, step=2, vol=9, octave=4, inst=HARPSI, order=(0, 2))
|
||||
p = [s.add(x) for x in (p0, p1, p2, p3, p4, p5, p6, p7)]
|
||||
s.order = [p[0], p[1], p[2], p[3], p[4], p[7], p[5], p[6], p[1], p[2], p[3], p[4]]
|
||||
s.restart = 1
|
||||
return s
|
||||
|
||||
|
||||
def generated(spec_):
|
||||
return lambda: Gen(spec_).build()
|
||||
|
||||
@@ -1317,6 +1548,9 @@ SONGS["mood_temple"] = ("temple", generated(MOOD_SPECS["temple"]))
|
||||
SONGS["victory"] = ("victory", song_victory)
|
||||
SONGS["victory2"] = ("victory", song_victory2)
|
||||
SONGS["defeat"] = ("defeat", song_defeat)
|
||||
SONGS["holiday_christmas"] = ("christmas", song_christmas)
|
||||
SONGS["holiday_easter"] = ("easter", song_easter)
|
||||
SONGS["holiday_halloween"] = ("halloween", song_halloween)
|
||||
|
||||
|
||||
def write_rust_table(names):
|
||||
@@ -1331,7 +1565,8 @@ def write_rust_table(names):
|
||||
]
|
||||
cat = {"title": "Title", "build": "Build", "battle": "Battle", "assault": "Assault", "boss_paladin": "BossPaladin",
|
||||
"boss_warden": "BossWarden", "torture": "Torture", "temple": "Temple", "victory": "Victory", "defeat": "Defeat",
|
||||
"surface": "Surface", "surface_battle": "SurfaceBattle"}
|
||||
"surface": "Surface", "surface_battle": "SurfaceBattle", "christmas": "Christmas", "easter": "Easter",
|
||||
"halloween": "Halloween"}
|
||||
for n in names:
|
||||
lines.append(' ("%s", Category::%s, include_bytes!("../assets/music/%s.xm")),' % (n, cat[SONGS[n][0]], n))
|
||||
lines.append("];")
|
||||
|
||||
Reference in new issue
Block a user