// 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 { 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 { 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 { notes.iter().map(|&f| seg(w, f, f, each, vol, 6.0)).collect() } fn def(s: Sfx, r: f32) -> Vec { 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> { SFX_ALL.iter().map(|&s| def(s, rate)).collect() }