use std::fs;
use std::path::Path;
use anyhow::{Context, ensure};
use crate::rom::Rom;
const UPLOAD: usize = 0x28000;
const DIR: usize = 0x3C00;
const DIR_ENTRIES: usize = 28;
const RATE: u32 = 32000;
const SFX: std::ops::RangeInclusive<u8> = 0x20..=0x68;
const SFX_SECONDS: u32 = 5;
const STARTED: [(u16, u8); 38] = [
(0x0012, 0x72),
(0x0014, 0x88),
(0x0015, 0x3C),
(0x0018, 0xA4),
(0x0019, 0x54),
(0x0020, 0x3B),
(0x0043, 0xC0),
(0x0047, 0x80),
(0x0048, 0x20),
(0x004C, 0x01),
(0x004D, 0x01),
(0x0051, 0xE0),
(0x0053, 0x10),
(0x005E, 0x80),
(0x00E3, 0x01),
(0x00F1, 0x01),
(0x00F2, 0x2C),
(0x00FA, 0x10),
(0x022E, 0x70),
(0x022F, 0x05),
(0x0361, 0x01),
(0x0363, 0x01),
(0x0365, 0x01),
(0x0367, 0x01),
(0x0369, 0x01),
(0x036B, 0x01),
(0x036D, 0x01),
(0x036F, 0x01),
(0x0390, 0x03),
(0x0391, 0x03),
(0x03B2, 0x47),
(0x03B3, 0x47),
(0x03B4, 0x0E),
(0x03C0, 0x03),
(0x03D4, 0x11),
(0x03D5, 0x11),
(0x03D6, 0x22),
(0x03D7, 0x22),
];
const STARTED_CPU: [u8; 7] = [0xAD, 0x08, 0x38, 0x61, 0x00, 0x03, 0xCF];
const STARTED_DSP: [(usize, u8); 9] = [
(0x0C, 0x60),
(0x1C, 0x60),
(0x5D, 0x3C),
(0x6C, 0x20),
(0x6D, 0xF7),
(0x75, 0xFF),
(0x76, 0x80),
(0x77, 0xB8),
(0x7D, 0x01),
];
pub fn samples(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let dir = folder.join("audio/samples");
fs::create_dir_all(&dir)?;
let ram = aram(rom)?;
for (index, entry) in ram[DIR..DIR + DIR_ENTRIES * 4].chunks(4).enumerate() {
let start = usize::from(u16::from_le_bytes([entry[0], entry[1]]));
let restart = usize::from(u16::from_le_bytes([entry[2], entry[3]]));
if start == 0xFFFF {
continue;
}
let (samples, restart) = brr(&ram, start, restart)?;
fs::write(dir.join(format!("{index:02}.wav")), wav(&samples, restart))?;
}
Ok(())
}
pub fn sfx(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let dir = folder.join("audio/sfx");
fs::create_dir_all(&dir)?;
let ram = aram(rom)?;
for id in SFX.filter(|id| ![0x5E, 0x60, 0x63].contains(id)) {
fs::write(dir.join(format!("{id:02x}.spc")), spc(&ram, id))?;
}
Ok(())
}
fn spc(ram: &[u8], id: u8) -> Vec<u8> {
let mut file = vec![0; 0x10200];
file[..35].copy_from_slice(b"SNES-SPC700 Sound File Data v0.30\x1A\x1A");
file[0x23] = 26;
file[0x24] = 30;
file[0x25..0x2C].copy_from_slice(&STARTED_CPU);
let text = |file: &mut Vec<u8>, at: usize, text: &str| {
file[at..at + text.len()].copy_from_slice(text.as_bytes())
};
text(&mut file, 0x2E, &format!("Sound effect {id:02X}"));
text(&mut file, 0x4E, "Super Mario Kart");
text(&mut file, 0x6E, "desmack");
text(&mut file, 0xA9, &SFX_SECONDS.to_string());
text(&mut file, 0xAC, "0");
let aram = &mut file[0x100..0x10100];
aram.copy_from_slice(ram);
for (address, value) in STARTED {
aram[usize::from(address)] = value;
}
aram[0x00] = id;
aram[0xF4..0xF8].fill(0);
for (register, value) in STARTED_DSP {
file[0x10100 + register] = value;
}
file
}
fn aram(rom: &Rom) -> anyhow::Result<Vec<u8>> {
let stream: Vec<u8> = (UPLOAD..rom.data.len())
.filter(|p| p & 0x8000 != 0)
.map(|p| rom.data[p])
.collect();
let mut ram = vec![0; 0x10000];
let mut at = 0;
loop {
let header = stream.get(at..at + 4).context("upload runs past the ROM")?;
let length = usize::from(u16::from_le_bytes([header[0], header[1]]));
let address = usize::from(u16::from_le_bytes([header[2], header[3]]));
if length == 0 {
return Ok(ram);
}
let data = stream
.get(at + 4..at + 4 + length)
.context("upload runs past the ROM")?;
ram.get_mut(address..address + length)
.context("upload runs past ARAM")?
.copy_from_slice(data);
at += 4 + length;
}
}
fn brr(ram: &[u8], start: usize, restart: usize) -> anyhow::Result<(Vec<i16>, Option<usize>)> {
let (mut old, mut older) = (0i32, 0i32);
let mut samples = Vec::new();
let mut at = start;
loop {
let block = ram.get(at..at + 9).context("sample runs past ARAM")?;
let (shift, filter) = (block[0] >> 4, block[0] >> 2 & 3);
for &byte in &block[1..] {
for nibble in [byte >> 4, byte & 15] {
let n = i32::from((nibble << 4) as i8 >> 4);
let s = match shift {
0..=12 => n << shift >> 1,
_ if n < 0 => -2048,
_ => 0,
};
let s = match filter {
0 => s,
1 => s + old + (-old >> 4),
2 => s + 2 * old + ((-3 * old) >> 5) - older + (older >> 4),
_ => s + 2 * old + ((-13 * old) >> 6) - older + ((3 * older) >> 4),
};
let s = i32::from((s.clamp(-0x8000, 0x7FFF) as i16).wrapping_mul(2) >> 1);
(older, old) = (old, s);
samples.push((s * 2) as i16);
}
}
if block[0] & 1 != 0 {
if block[0] & 2 == 0 {
return Ok((samples, None));
}
ensure!(
restart >= start && restart <= at && (restart - start).is_multiple_of(9),
"loop at {restart:04X} is not a block of the sample at {start:04X}"
);
return Ok((samples, Some((restart - start) / 9 * 16)));
}
at += 9;
}
}
fn wav(samples: &[i16], restart: Option<usize>) -> Vec<u8> {
let words = |words: &[u32]| -> Vec<u8> { words.iter().flat_map(|w| w.to_le_bytes()).collect() };
let mut chunks = chunk(b"fmt ", &words(&[0x0001_0001, RATE, RATE * 2, 0x0010_0002]));
chunks.extend(chunk(
b"data",
&samples
.iter()
.flat_map(|s| s.to_le_bytes())
.collect::<Vec<_>>(),
));
if let Some(restart) = restart {
let end = samples.len() as u32 - 1;
chunks.extend(chunk(
b"smpl",
&words(&[
0,
0,
1_000_000_000 / RATE,
60,
0,
0,
0,
1,
0,
0,
0,
restart as u32,
end,
0,
0,
]),
));
}
let mut file = b"RIFF".to_vec();
file.extend((chunks.len() as u32 + 4).to_le_bytes());
file.extend(b"WAVE");
file.extend(chunks);
file
}
fn chunk(id: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut chunk = id.to_vec();
chunk.extend((body.len() as u32).to_le_bytes());
chunk.extend(body);
chunk
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn brr_shifts_filters_and_loops() {
let mut ram = vec![0; 18];
ram[0] = 0xC0;
ram[1] = 0x1F;
ram[8] = 0x0F;
ram[9] = 0x07;
let (samples, restart) = brr(&ram, 0, 9).unwrap();
assert_eq!(samples.len(), 32);
assert_eq!(&samples[..2], [4096, -4096]);
assert_eq!(samples[16], -3840);
assert_eq!(restart, Some(16));
ram[9] = 0x01;
assert_eq!(brr(&ram, 0, 9).unwrap().1, None);
assert!(brr(&ram[..9], 0, 0).is_err());
}
#[test]
fn wav_has_a_loop_only_when_asked() {
let plain = wav(&[1, -1], None);
assert_eq!(&plain[..4], b"RIFF");
assert_eq!(plain.len(), 12 + 24 + 12);
assert_eq!(u32::from_le_bytes(plain[4..8].try_into().unwrap()), 40);
assert_eq!(&plain[44..], [1, 0, 0xFF, 0xFF]);
let looped = wav(&[1, -1], Some(1));
assert_eq!(looped.len(), plain.len() + 8 + 60);
assert_eq!(&looped[48..52], b"smpl");
}
#[test]
fn rom_samples() {
let Ok(path) = std::env::var("SMK_ROM") else {
return;
};
let rom = Rom::parse(fs::read(path).unwrap()).unwrap();
let folder = std::env::temp_dir().join(format!("desmack-audio-{}", std::process::id()));
samples(&rom, &folder).unwrap();
let dir = folder.join("audio/samples");
assert_eq!(fs::read_dir(&dir).unwrap().count(), 26);
let first = fs::read(dir.join("00.wav")).unwrap();
assert_eq!(
u32::from_le_bytes(first[40..44].try_into().unwrap()),
2640 * 2
);
assert_eq!(&first[44 + 2640 * 2..][..4], b"smpl");
sfx(&rom, &folder).unwrap();
assert_eq!(fs::read_dir(folder.join("audio/sfx")).unwrap().count(), 70);
fs::remove_dir_all(folder).unwrap();
}
#[test]
fn spc_starts_after_the_driver_start_up() {
let file = spc(&[0xFF; 0x10000], 0x20);
assert_eq!(file.len(), 0x10200);
assert_eq!(&file[..33], b"SNES-SPC700 Sound File Data v0.30");
assert_eq!(&file[0x25..0x27], [0xAD, 0x08]);
assert_eq!(&file[0x2E..0x3D], b"Sound effect 20");
assert_eq!(&file[0xA9..0xAC], b"5\0\0");
let aram = &file[0x100..0x10100];
assert_eq!(aram[0x00], 0x20);
assert_eq!(aram[0xF4..0xF8], [0; 4]);
assert_eq!(aram[0x0012], 0x72);
assert_eq!(aram[0x0013], 0xFF);
assert_eq!(file[0x10100 + 0x5D], 0x3C);
}
}