summaryrefslogtreecommitdiff
path: root/2026/games/asrael_io/source/src/sfx.rs
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-08-18 00:08:28 -0400
committerLLLL Colonq <llll@colonq>2026-08-18 00:08:28 -0400
commit32a62cfcb925225359a5c90761b795f2f737ebd2 (patch)
tree95c2261964803b19f2d39cb35d8d514ed6e5a6f4 /2026/games/asrael_io/source/src/sfx.rs
parentde99c5f7636a952025ea13de02bafd25bcb6bdec (diff)
Re-populate games
Diffstat (limited to '2026/games/asrael_io/source/src/sfx.rs')
-rw-r--r--2026/games/asrael_io/source/src/sfx.rs302
1 files changed, 302 insertions, 0 deletions
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<Sound>,
+ sid: Sid,
+}
+
+pub struct Sfx {
+ pending: Option<Pending>,
+ stream: Option<Stream>,
+ tx: Sender<Sound>,
+}
+
+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::<Sound>();
+
+ 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<i16> = 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);
+ }
+ }
+ }
+}