174 lines
8.8 KiB
Rust
174 lines
8.8 KiB
Rust
// Chip-style sound effects, synthesised once when audio starts.
|
|
// Each effect is a list of layers; a layer is a sequence of segments
|
|
// (waveform, start/end frequency, duration, volume, decay).
|
|
|
|
use crate::game::{Sfx, SFX_ALL};
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum W {
|
|
Square(f32), // duty cycle
|
|
Tri,
|
|
Saw,
|
|
Noise,
|
|
Sine,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct Seg {
|
|
w: W,
|
|
f0: f32,
|
|
f1: f32,
|
|
dur: f32,
|
|
vol: f32,
|
|
decay: f32, // exponential decay rate per second (0 = flat)
|
|
vib: f32, // vibrato depth (fraction of frequency)
|
|
}
|
|
|
|
fn seg(w: W, f0: f32, f1: f32, dur: f32, vol: f32, decay: f32) -> Seg {
|
|
Seg { w, f0, f1, dur, vol, decay, vib: 0.0 }
|
|
}
|
|
|
|
fn vib(mut s: Seg, v: f32) -> Seg {
|
|
s.vib = v;
|
|
s
|
|
}
|
|
|
|
fn render_layer(rate: f32, segs: &[Seg], seed: u32) -> Vec<f32> {
|
|
let mut out = Vec::new();
|
|
let mut ph = 0.0f32;
|
|
let mut rng = seed | 1;
|
|
let mut noise = 0.0f32;
|
|
let mut noise_ph = 0.0f32;
|
|
for s in segs {
|
|
let n = (s.dur * rate) as usize;
|
|
let attack = (rate * 0.003) as usize;
|
|
let release = (rate * 0.008) as usize;
|
|
for i in 0..n {
|
|
let t = i as f32 / rate;
|
|
let k = i as f32 / n.max(1) as f32;
|
|
let mut f = s.f0 * (s.f1 / s.f0).powf(k); // exponential sweep
|
|
if s.vib > 0.0 {
|
|
f *= 1.0 + s.vib * (t * 2.0 * std::f32::consts::PI * 7.0).sin();
|
|
}
|
|
ph = (ph + f / rate).fract();
|
|
let v = match s.w {
|
|
W::Square(d) => {
|
|
if ph < d {
|
|
1.0
|
|
} else {
|
|
-1.0
|
|
}
|
|
}
|
|
W::Tri => 4.0 * (ph - 0.5).abs() - 1.0,
|
|
W::Saw => 2.0 * ph - 1.0,
|
|
W::Sine => (ph * 2.0 * std::f32::consts::PI).sin(),
|
|
W::Noise => {
|
|
// sample-and-hold noise; frequency controls the "pitch" of the noise
|
|
noise_ph += f / rate;
|
|
if noise_ph >= 1.0 {
|
|
noise_ph -= 1.0;
|
|
rng ^= rng << 13;
|
|
rng ^= rng >> 17;
|
|
rng ^= rng << 5;
|
|
noise = (rng as f32 / u32::MAX as f32) * 2.0 - 1.0;
|
|
}
|
|
noise
|
|
}
|
|
};
|
|
let mut env = (-s.decay * t).exp();
|
|
if i < attack {
|
|
env *= i as f32 / attack as f32;
|
|
}
|
|
if n - i < release {
|
|
env *= (n - i) as f32 / release as f32;
|
|
}
|
|
out.push(v * s.vol * env);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn mix(layers: Vec<Vec<f32>>) -> Vec<f32> {
|
|
let n = layers.iter().map(|l| l.len()).max().unwrap_or(0);
|
|
let mut out = vec![0.0; n];
|
|
for l in layers {
|
|
for (o, v) in out.iter_mut().zip(l) {
|
|
*o += v;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn arp(w: W, notes: &[f32], each: f32, vol: f32) -> Vec<Seg> {
|
|
notes.iter().map(|&f| seg(w, f, f, each, vol, 6.0)).collect()
|
|
}
|
|
|
|
fn def(s: Sfx, r: f32) -> Vec<f32> {
|
|
use W::*;
|
|
let l = |segs: &[Seg], seed: u32| render_layer(r, segs, seed);
|
|
match s {
|
|
Sfx::Dig => mix(vec![l(&[seg(Noise, 3000.0, 1500.0, 0.06, 0.35, 40.0)], 1), l(&[seg(Tri, 140.0, 70.0, 0.08, 0.5, 30.0)], 2)]),
|
|
Sfx::GoldDig => mix(vec![l(&[seg(Square(0.25), 1568.0, 2093.0, 0.07, 0.22, 25.0)], 3), l(&[seg(Noise, 6000.0, 6000.0, 0.02, 0.2, 80.0)], 4)]),
|
|
Sfx::DigDone => mix(vec![l(&[seg(Noise, 2500.0, 400.0, 0.3, 0.4, 9.0)], 5), l(&[seg(Tri, 110.0, 45.0, 0.25, 0.6, 12.0)], 6)]),
|
|
Sfx::Claim => l(&[seg(Square(0.5), 440.0, 660.0, 0.07, 0.12, 20.0)], 7),
|
|
Sfx::Deposit => l(&[seg(Square(0.25), 988.0, 988.0, 0.05, 0.22, 8.0), seg(Square(0.25), 1319.0, 1319.0, 0.14, 0.22, 14.0)], 8),
|
|
Sfx::Build => mix(vec![l(&[seg(Tri, 220.0, 80.0, 0.14, 0.6, 15.0)], 9), l(&[seg(Noise, 1800.0, 900.0, 0.08, 0.25, 30.0)], 10)]),
|
|
Sfx::Sell => l(&[seg(Square(0.25), 1319.0, 1319.0, 0.05, 0.2, 8.0), seg(Square(0.25), 988.0, 988.0, 0.12, 0.2, 14.0)], 11),
|
|
Sfx::MeleeHit => mix(vec![l(&[seg(Noise, 4000.0, 2000.0, 0.07, 0.4, 35.0)], 12), l(&[seg(Tri, 200.0, 90.0, 0.08, 0.5, 30.0)], 13)]),
|
|
Sfx::Shoot => l(&[seg(Square(0.125), 900.0, 300.0, 0.13, 0.25, 12.0)], 14),
|
|
Sfx::Death => l(&[vib(seg(Square(0.5), 420.0, 70.0, 0.4, 0.3, 5.0), 0.04)], 15),
|
|
Sfx::HeroDeath => mix(vec![l(&[seg(Noise, 2000.0, 300.0, 0.3, 0.3, 8.0)], 16), l(&[seg(Tri, 330.0, 60.0, 0.35, 0.5, 6.0)], 17)]),
|
|
Sfx::Lightning => mix(vec![l(&[seg(Noise, 9000.0, 2000.0, 0.55, 0.6, 5.0)], 18), l(&[seg(Tri, 80.0, 40.0, 0.6, 0.6, 4.0)], 19)]),
|
|
Sfx::Heal => l(&arp(Sine, &[523.0, 659.0, 784.0, 1047.0], 0.07, 0.45), 20),
|
|
Sfx::Summon => l(&[vib(seg(Square(0.25), 300.0, 900.0, 0.45, 0.22, 2.0), 0.06)], 21),
|
|
Sfx::PortalArrive => l(&[vib(seg(Saw, 900.0, 260.0, 0.45, 0.2, 3.0), 0.08)], 22),
|
|
Sfx::WaveAlarm => l(&[
|
|
seg(Square(0.5), 660.0, 660.0, 0.13, 0.2, 0.0),
|
|
seg(Square(0.5), 880.0, 880.0, 0.13, 0.2, 0.0),
|
|
seg(Square(0.5), 660.0, 660.0, 0.13, 0.2, 0.0),
|
|
seg(Square(0.5), 880.0, 880.0, 0.13, 0.2, 0.0),
|
|
], 23),
|
|
Sfx::WaveStart => mix(vec![l(&[seg(Saw, 220.0, 220.0, 0.7, 0.22, 2.0)], 24), l(&[seg(Saw, 330.0, 330.0, 0.7, 0.16, 2.0)], 25)]),
|
|
Sfx::CoreHit => mix(vec![l(&[seg(Tri, 95.0, 40.0, 0.22, 0.7, 10.0)], 26), l(&[seg(Noise, 1200.0, 600.0, 0.12, 0.25, 25.0)], 27)]),
|
|
Sfx::GateHit => mix(vec![l(&[seg(Square(0.5), 310.0, 290.0, 0.1, 0.15, 25.0)], 28), l(&[seg(Square(0.5), 465.0, 440.0, 0.1, 0.12, 25.0)], 29), l(&[seg(Noise, 5000.0, 3000.0, 0.05, 0.2, 50.0)], 30)]),
|
|
Sfx::GateDefenders => l(&[seg(Saw, 440.0, 440.0, 0.25, 0.22, 1.0), seg(Saw, 330.0, 330.0, 0.5, 0.22, 2.0)], 31),
|
|
Sfx::LevelUp => l(&arp(Square(0.25), &[523.0, 659.0, 784.0, 1047.0, 1319.0], 0.05, 0.2), 32),
|
|
Sfx::Pickup => l(&[seg(Tri, 300.0, 600.0, 0.06, 0.4, 10.0)], 33),
|
|
Sfx::Drop => l(&[seg(Tri, 600.0, 250.0, 0.08, 0.4, 10.0)], 34),
|
|
Sfx::Payday => l(&[
|
|
seg(Square(0.25), 988.0, 988.0, 0.05, 0.18, 8.0),
|
|
seg(Square(0.25), 1319.0, 1319.0, 0.05, 0.18, 8.0),
|
|
seg(Square(0.25), 988.0, 988.0, 0.05, 0.18, 8.0),
|
|
seg(Square(0.25), 1568.0, 1568.0, 0.15, 0.18, 10.0),
|
|
], 35),
|
|
Sfx::FlagSet => l(&[seg(Square(0.5), 392.0, 392.0, 0.08, 0.2, 6.0), seg(Square(0.5), 523.0, 523.0, 0.16, 0.2, 6.0)], 36),
|
|
Sfx::Victory => l(&arp(Square(0.25), &[587.0, 740.0, 880.0, 1175.0], 0.08, 0.2), 37),
|
|
Sfx::Defeat => l(&[seg(Square(0.5), 220.0, 110.0, 0.6, 0.22, 2.0)], 38),
|
|
Sfx::Click => l(&[seg(Square(0.5), 1000.0, 1000.0, 0.02, 0.12, 50.0)], 39),
|
|
Sfx::Error => l(&[seg(Square(0.5), 110.0, 105.0, 0.16, 0.2, 4.0)], 40),
|
|
Sfx::Reinforce => l(&[seg(Tri, 320.0, 200.0, 0.07, 0.5, 20.0), seg(Tri, 260.0, 170.0, 0.09, 0.5, 20.0)], 41),
|
|
Sfx::Cannon => mix(vec![l(&[seg(Noise, 1500.0, 150.0, 0.5, 0.6, 6.0)], 42), l(&[seg(Tri, 75.0, 32.0, 0.45, 0.9, 5.0)], 43)]),
|
|
Sfx::Magic => mix(vec![l(&[seg(Sine, 880.0, 1760.0, 0.16, 0.35, 8.0)], 44), l(&[vib(seg(Square(0.125), 1320.0, 1320.0, 0.16, 0.08, 10.0), 0.05)], 45)]),
|
|
Sfx::Capture => l(&[
|
|
seg(Square(0.25), 1400.0, 1400.0, 0.04, 0.16, 20.0),
|
|
seg(Square(0.25), 1100.0, 1100.0, 0.04, 0.16, 20.0),
|
|
seg(Square(0.25), 900.0, 900.0, 0.08, 0.16, 15.0),
|
|
], 46),
|
|
Sfx::Convert => l(&arp(Saw, &[220.0, 262.0, 330.0, 440.0], 0.1, 0.22), 47),
|
|
Sfx::Slap => mix(vec![l(&[seg(Noise, 5000.0, 2500.0, 0.06, 0.5, 40.0)], 48), l(&[seg(Square(0.5), 900.0, 500.0, 0.05, 0.12, 30.0)], 49)]),
|
|
Sfx::Punch => mix(vec![l(&[seg(Tri, 160.0, 60.0, 0.15, 0.8, 18.0)], 50), l(&[seg(Noise, 3000.0, 800.0, 0.08, 0.4, 30.0)], 51)]),
|
|
// Crookfinger's syllables: a nasal squeak gliding up or down, some with a hissy consonant
|
|
Sfx::Voice1 => mix(vec![l(&[vib(seg(Square(0.2), 620.0, 760.0, 0.08, 0.16, 6.0), 0.04)], 60), l(&[seg(Noise, 6000.0, 4000.0, 0.02, 0.05, 40.0)], 61)]),
|
|
Sfx::Voice2 => l(&[vib(seg(Square(0.18), 780.0, 600.0, 0.09, 0.16, 6.0), 0.05)], 62),
|
|
Sfx::Voice3 => mix(vec![l(&[vib(seg(Square(0.22), 520.0, 640.0, 0.07, 0.15, 7.0), 0.03)], 63), l(&[seg(Tri, 260.0, 300.0, 0.07, 0.12, 8.0)], 64)]),
|
|
Sfx::Voice4 => l(&[seg(Square(0.15), 900.0, 980.0, 0.05, 0.13, 10.0), vib(seg(Square(0.2), 700.0, 560.0, 0.06, 0.14, 8.0), 0.04)], 65),
|
|
Sfx::Voice5 => mix(vec![l(&[vib(seg(Square(0.25), 470.0, 560.0, 0.1, 0.15, 5.0), 0.06)], 66), l(&[seg(Noise, 3000.0, 2000.0, 0.025, 0.06, 30.0)], 67)]),
|
|
Sfx::Voice6 => l(&[vib(seg(Square(0.2), 680.0, 820.0, 0.06, 0.15, 9.0), 0.05), seg(Square(0.2), 820.0, 700.0, 0.04, 0.12, 12.0)], 68),
|
|
}
|
|
}
|
|
|
|
// One sample buffer per effect, in SFX_ALL order
|
|
pub fn generate_all(rate: f32) -> Vec<Vec<f32>> {
|
|
SFX_ALL.iter().map(|&s| def(s, rate)).collect()
|
|
}
|