use wasm_bindgen::prelude::*;
use dotzuki_audio::apu::Apu;
use dotzuki_audio::format::TrackDef;
use dotzuki_audio::sequencer::Sequencer;
use dotzuki_audio::CPU_CLOCK_HZ;
pub const OUTPUT_SAMPLE_RATE: u32 = 48_000;
#[wasm_bindgen]
pub fn audio_sample_rate() -> u32 {
OUTPUT_SAMPLE_RATE
}
#[wasm_bindgen]
pub fn render_audio_pcm(track_json: &str, max_seconds: f32) -> Vec<u8> {
let track: TrackDef = match serde_json::from_str(track_json) {
Ok(t) => t,
Err(e) => {
crate::log_error(&format!("render_audio_pcm: bad track JSON: {e}"));
return Vec::new();
}
};
let samples = render_track_samples(&track, max_seconds);
let mut bytes = Vec::with_capacity(samples.len() * 4);
for s in samples {
bytes.extend_from_slice(&s.to_le_bytes());
}
bytes
}
fn render_track_samples(track: &TrackDef, max_seconds: f32) -> Vec<f32> {
let mut seq = Sequencer::new();
let mut apu = Apu::new();
apu.write_register(0xFF26, 0x80);
track.play_on(&mut seq);
let cycles_per_sample = CPU_CLOCK_HZ / OUTPUT_SAMPLE_RATE;
let samples_per_frame = (OUTPUT_SAMPLE_RATE / 60) as usize;
let max_frames = (max_seconds.max(0.0) * 60.0).ceil() as usize;
let mut out = Vec::with_capacity(samples_per_frame * max_frames * 2);
let mut frame = 0;
while seq.is_playing() && frame < max_frames {
seq.update_frame(&mut apu);
for _ in 0..samples_per_frame {
apu.tick_n(cycles_per_sample);
let (l, r) = apu.mix_sample();
out.push(l as f32 / 480.0);
out.push(r as f32 / 480.0);
}
frame += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use dotzuki_audio::format::{AudioCommand, ChannelDef, TrackDef, TrackKind};
use dotzuki_audio::HwChannel;
fn demo_track() -> TrackDef {
TrackDef {
id: "Demo".into(),
kind: TrackKind::Music,
name: None,
tempo: 0x0100,
channels: vec![ChannelDef {
hw: HwChannel::Pulse1,
commands: vec![
AudioCommand::DutyCycle { value: 2 },
AudioCommand::NoteType { speed: 8, param: 0xF0 }, AudioCommand::Octave { value: 4 },
AudioCommand::Note { pitch: 0, length: 16 },
],
}],
}
}
#[test]
fn renders_non_silent_pcm() {
let samples = render_track_samples(&demo_track(), 0.25);
assert!(!samples.is_empty(), "expected some samples");
assert_eq!(samples.len() % 2, 0);
assert!(
samples.iter().any(|&s| s.abs() > 0.0),
"rendered audio was completely silent"
);
}
#[test]
fn pcm_byte_export_is_f32_sized() {
let json = serde_json::to_string(&demo_track()).unwrap();
let bytes = render_audio_pcm(&json, 0.1);
assert!(!bytes.is_empty());
assert_eq!(bytes.len() % 4, 0, "f32 byte buffer must be 4-aligned");
}
#[test]
fn bad_json_returns_empty() {
assert!(render_audio_pcm("not json", 1.0).is_empty());
}
}