From 32a62cfcb925225359a5c90761b795f2f737ebd2 Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Tue, 18 Aug 2026 00:08:28 -0400 Subject: Re-populate games --- 2026/games/asrael_io/source/src/sfx.rs | 302 +++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 2026/games/asrael_io/source/src/sfx.rs (limited to '2026/games/asrael_io/source/src/sfx.rs') diff --git a/2026/games/asrael_io/source/src/sfx.rs b/2026/games/asrael_io/source/src/sfx.rs new file mode 100644 index 0000000..a1367df --- /dev/null +++ b/2026/games/asrael_io/source/src/sfx.rs @@ -0,0 +1,302 @@ +use std::sync::mpsc::{Receiver, Sender, channel}; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{Device, OutputCallbackInfo, Stream, SupportedStreamConfig}; +use resid::{ChipModel, PAL_CLOCK, SamplingMethod, Sid}; + +const GATE: u8 = 0x01; +const TRIANGLE: u8 = 0x10; +const SAWTOOTH: u8 = 0x20; +const PULSE: u8 = 0x40; +const NOISE: u8 = 0x80; + +enum Sound { + EnemyShoot, + Explosion, + Hit, + Lose, + Shoot, + Win, +} + +struct Pending { + config: SupportedStreamConfig, + device: Device, + rx: Receiver, + sid: Sid, +} + +pub struct Sfx { + pending: Option, + stream: Option, + tx: Sender, +} + +impl Sfx { + pub fn new() -> Self { + let device = cpal::default_host() + .default_output_device() + .expect("no audio output device"); + + let config = device + .default_output_config() + .expect("no default audio config"); + + let mut sid = Sid::new(ChipModel::Mos6581); + let (tx, rx) = channel::(); + + sid.set_sampling_parameters(SamplingMethod::ResampleFast, PAL_CLOCK, config.sample_rate()); + Self::set_volume(&mut sid, 0x0F); + Self::set_envelope(&mut sid, 0, 0x06, 0x00); + Self::set_envelope(&mut sid, 1, 0x06, 0x00); + Self::set_envelope(&mut sid, 2, 0x08, 0xA9); + + Self { + pending: Some(Pending { + config, + device, + rx, + sid, + }), + stream: None, + tx, + } + } + + pub fn resume(&mut self) { + if let Some(stream) = &self.stream { + let _ = stream.play(); + return; + } + + let Some(Pending { + config, + device, + rx, + mut sid, + }) = self.pending.take() + else { + return; + }; + + let channels = config.channels() as usize; + let sample_rate = config.sample_rate(); + let mut scratch: Vec = Vec::new(); + + let mut enemy_shoot_hz: f32 = 0.0; + let mut explosion_hz: f32 = 0.0; + let mut hit_t: u32 = 0; + let mut lose_hz: f32 = 0.0; + let mut lose_t: u32 = 0; + let mut lose_wob: f32 = 0.0; + let mut shoot_hz: f32 = 0.0; + let mut win_t: u32 = 0; + + let stream = device + .build_output_stream( + config.into(), + move |data: &mut [f32], _: &OutputCallbackInfo| { + while let Ok(sound) = rx.try_recv() { + match sound { + Sound::EnemyShoot => { + enemy_shoot_hz = 520.0; + Self::set_freq(&mut sid, 1, enemy_shoot_hz); + Self::gate_on(&mut sid, 1, TRIANGLE); + } + + Sound::Explosion => { + hit_t = 0; + Self::set_envelope(&mut sid, 2, 0x08, 0xA9); + + explosion_hz = 2500.0; + Self::set_freq(&mut sid, 2, explosion_hz); + Self::gate_on(&mut sid, 2, NOISE); + } + + Sound::Hit => { + Self::set_envelope(&mut sid, 2, 0x05, 0x00); + + hit_t = 45; + Self::set_freq(&mut sid, 2, 2500.0); + Self::gate_on(&mut sid, 2, NOISE); + } + + Sound::Lose => { + enemy_shoot_hz = 0.0; + Self::set_envelope(&mut sid, 1, 0x00, 0xC6); + Self::set_pulse_width(&mut sid, 1, 0x0800); + + lose_hz = 392.0; + lose_t = 300; + lose_wob = 0.0; + Self::set_freq(&mut sid, 1, lose_hz); + Self::gate_on(&mut sid, 1, PULSE); + } + + Sound::Shoot => { + shoot_hz = 1760.0; + Self::set_freq(&mut sid, 0, shoot_hz); + Self::gate_on(&mut sid, 0, SAWTOOTH); + } + + Sound::Win => { + shoot_hz = 0.0; + Self::set_envelope(&mut sid, 0, 0x00, 0xA9); + Self::set_pulse_width(&mut sid, 0, 0x0800); + + win_t = 300; + Self::set_freq(&mut sid, 0, 987.77); + Self::gate_on(&mut sid, 0, PULSE); + } + } + } + + let frames = data.len() / channels; + if scratch.len() < frames { + scratch.resize(frames, 0); + } + let out = &mut scratch[..frames]; + + for block in out.chunks_mut(64) { + Self::sweep(&mut sid, &mut shoot_hz, 0.975, 220.0, 0, SAWTOOTH); + Self::sweep(&mut sid, &mut enemy_shoot_hz, 0.97, 200.0, 1, TRIANGLE); + Self::sweep(&mut sid, &mut explosion_hz, 0.985, 150.0, 2, NOISE); + + if hit_t > 0 { + hit_t -= 1; + if hit_t == 0 { + Self::gate_off(&mut sid, 2, NOISE); + } + } + + if win_t > 0 { + win_t -= 1; + + if win_t == 245 || win_t == 190 { + let hz = if win_t == 245 { 1318.5 } else { 1661.2 }; + Self::set_freq(&mut sid, 0, hz); + Self::gate_on(&mut sid, 0, PULSE); + } + + if win_t == 0 { + Self::gate_off(&mut sid, 0, PULSE); + } + } + + if lose_t > 0 { + lose_t -= 1; + lose_hz *= 0.995; + lose_wob += 0.35; + + let hz = lose_hz * (1.0 + 0.05 * lose_wob.sin()); + Self::set_freq(&mut sid, 1, hz); + + if lose_t == 0 { + Self::gate_off(&mut sid, 1, PULSE); + } + } + + Self::fill(&mut sid, block, sample_rate); + } + + for (frame, &s) in data.chunks_mut(channels).zip(out.iter()) { + frame.fill(s as f32 / 32768.0); + } + }, + move |err| log::error!("audio stream error: {err}"), + None, + ) + .expect("failed to build audio stream"); + + stream.play().expect("failed to start audio stream"); + self.stream = Some(stream); + } + + pub fn enemy_shoot(&self) { + let _ = self.tx.send(Sound::EnemyShoot); + } + + pub fn explode(&self) { + let _ = self.tx.send(Sound::Explosion); + } + + pub fn hit(&self) { + let _ = self.tx.send(Sound::Hit); + } + + pub fn lose(&self) { + let _ = self.tx.send(Sound::Lose); + } + + pub fn shoot(&self) { + let _ = self.tx.send(Sound::Shoot); + } + + pub fn win(&self) { + let _ = self.tx.send(Sound::Win); + } + + fn fill(sid: &mut Sid, out: &mut [i16], sample_rate: u32) { + let budget = PAL_CLOCK / sample_rate + 1; + let mut i = 0; + + while i < out.len() { + let target = (out.len() - i) as u32; + let (n, _) = sid.sample(target * budget, &mut out[i..], 1); + + if n == 0 { + break; + } + + i += n; + } + } + + fn gate_off(sid: &mut Sid, voice: u8, wave: u8) { + sid.write(voice * 7 + 4, wave); + } + + fn gate_on(sid: &mut Sid, voice: u8, wave: u8) { + sid.write(voice * 7 + 4, wave); + sid.write(voice * 7 + 4, wave | GATE); + } + + fn set_envelope(sid: &mut Sid, voice: u8, attack_decay: u8, sustain_release: u8) { + let base = voice * 7; + + sid.write(base + 5, attack_decay); + sid.write(base + 6, sustain_release); + } + + fn set_freq(sid: &mut Sid, voice: u8, hz: f32) { + let base = voice * 7; + let freq = (hz * 16_777_216.0 / PAL_CLOCK as f32) as u16; + + sid.write(base, freq as u8); + sid.write(base + 1, (freq >> 8) as u8); + } + + fn set_pulse_width(sid: &mut Sid, voice: u8, width: u16) { + let base = voice * 7; + + sid.write(base + 2, width as u8); + sid.write(base + 3, (width >> 8) as u8); + } + + fn set_volume(sid: &mut Sid, volume: u8) { + sid.write(0x18, volume); + } + + fn sweep(sid: &mut Sid, hz: &mut f32, rate: f32, floor: f32, voice: u8, wave: u8) { + if *hz > 0.0 { + *hz *= rate; + + if *hz < floor { + *hz = 0.0; + Self::gate_off(sid, voice, wave); + } else { + Self::set_freq(sid, voice, *hz); + } + } + } +} -- cgit v1.3.1