From 70373647fbddf55bb70d85c402c7b82776cb9b1a Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 4 Nov 2024 09:59:58 -0500 Subject: [PATCH] Add audio, fix timing --- Cargo.lock | 378 ++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 6 + src/audio.rs | 101 +++++++++++ src/emulator.rs | 55 +++++- src/main.rs | 8 +- src/shrooms_vb_core.rs | 66 ++++++- 6 files changed, 597 insertions(+), 17 deletions(-) create mode 100644 src/audio.rs diff --git a/Cargo.lock b/Cargo.lock index 7a98704..2edeed5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,12 +31,43 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.6.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -51,7 +82,7 @@ dependencies = [ "jni-sys", "libc", "log", - "ndk", + "ndk 0.9.0", "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", @@ -167,6 +198,24 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.87", +] + [[package]] name = "bit-set" version = "0.6.0" @@ -284,6 +333,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -308,6 +366,17 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e10e7569f6ca78ef7664d7d651115172d4875c4410c050306bccde856a99a49" +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.20" @@ -454,6 +523,49 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -477,6 +589,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "dispatch" version = "0.2.0" @@ -513,6 +631,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "equivalent" version = "1.0.1" @@ -588,6 +712,12 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "glow" version = "0.13.1" @@ -638,7 +768,7 @@ dependencies = [ "presser", "thiserror", "winapi", - "windows", + "windows 0.52.0", ] [[package]] @@ -773,6 +903,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "jni" version = "0.21.1" @@ -885,6 +1024,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -924,6 +1072,12 @@ dependencies = [ "paste", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mint" version = "0.5.9" @@ -951,6 +1105,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror", +] + [[package]] name = "ndk" version = "0.9.0" @@ -990,6 +1158,54 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_enum" version = "0.7.3" @@ -1223,6 +1439,29 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -1341,6 +1580,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -1395,6 +1643,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "realfft" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390252372b7f2aac8360fc5e72eba10136b166d6faeed97e6d0c8324eb99b2b1" +dependencies = [ + "rustfft", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1413,18 +1670,80 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "renderdoc-sys" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rtrb" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f94e84c073f3b85d4012b44722fa8842b9986d741590d4f2636ad0a5b14143" + +[[package]] +name = "rubato" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd96992d7e24b3d7f35fdfe02af037a356ac90d41b466945cf3333525a86eea" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustfft" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + [[package]] name = "rustix" version = "0.38.38" @@ -1506,10 +1825,16 @@ dependencies = [ "bytemuck", "cc", "clap", + "cpal", "imgui", "imgui-wgpu", "imgui-winit-support", + "itertools", + "num-derive", + "num-traits", "pollster", + "rtrb", + "rubato", "wgpu", "winit", ] @@ -1587,6 +1912,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + [[package]] name = "strict-num" version = "0.1.1" @@ -1708,6 +2039,16 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "ttf-parser" version = "0.25.0" @@ -2111,7 +2452,17 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-core", + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", "windows-targets 0.52.6", ] @@ -2124,6 +2475,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -2351,7 +2721,7 @@ dependencies = [ "js-sys", "libc", "memmap2", - "ndk", + "ndk 0.9.0", "objc2", "objc2-app-kit", "objc2-foundation", diff --git a/Cargo.toml b/Cargo.toml index 8820305..51aceec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,16 @@ edition = "2021" anyhow = "1" bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } +cpal = "0.15" imgui = "0.12" imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } imgui-winit-support = "0.13" +itertools = "0.13" +num-derive = "0.4" +num-traits = "0.2" pollster = "0.4" +rtrb = "0.3" +rubato = "0.16" wgpu = "22.1" winit = "0.30" diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..2486830 --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,101 @@ +use anyhow::{bail, Result}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use itertools::Itertools; +use rubato::{FftFixedInOut, Resampler}; + +pub struct Audio { + #[allow(unused)] + stream: cpal::Stream, + sampler: FftFixedInOut, + input_buffer: Vec>, + output_buffer: Vec>, + sample_sink: rtrb::Producer, +} + +impl Audio { + pub fn init() -> Result { + let host = cpal::default_host(); + let Some(device) = host.default_output_device() else { + bail!("No output device available"); + }; + let Some(config) = device + .supported_output_configs()? + .find(|c| c.channels() == 2 && c.sample_format().is_float()) + else { + bail!("No suitable output config available"); + }; + let mut config = config.with_max_sample_rate().config(); + let sampler = FftFixedInOut::new(41700, config.sample_rate.0 as usize, 834, 2)?; + config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32); + + let input_buffer = sampler.input_buffer_allocate(true); + let output_buffer = sampler.output_buffer_allocate(true); + let (sample_sink, mut sample_source) = + rtrb::RingBuffer::new(sampler.output_frames_max() * 4); + + let stream = device.build_output_stream( + &config, + move |data: &mut [f32], _| { + let requested = data.len(); + let chunk = match sample_source.read_chunk(data.len()) { + Ok(c) => c, + Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { + sample_source.read_chunk(n).unwrap() + } + }; + let len = chunk.len(); + let (first, second) = chunk.as_slices(); + data[0..first.len()].copy_from_slice(first); + data[first.len()..len].copy_from_slice(second); + for rest in &mut data[len..requested] { + *rest = 0.0; + } + chunk.commit_all(); + }, + move |err| eprintln!("stream error: {err}"), + None, + )?; + stream.play()?; + Ok(Self { + stream, + sampler, + input_buffer, + output_buffer, + sample_sink, + }) + } + + pub fn update(&mut self, samples: &[f32]) { + for sample in samples.chunks_exact(2) { + for (channel, value) in self.input_buffer.iter_mut().zip(sample) { + channel.push(*value); + } + if self.input_buffer[0].len() >= self.sampler.input_frames_next() { + let (_, output_samples) = self + .sampler + .process_into_buffer(&self.input_buffer, &mut self.output_buffer, None) + .unwrap(); + + let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) { + Ok(c) => c, + Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { + self.sample_sink.write_chunk_uninit(n).unwrap() + } + }; + let interleaved = self.output_buffer[0] + .iter() + .interleave(self.output_buffer[1].iter()) + .cloned(); + chunk.fill_from_iter(interleaved); + + for channel in &mut self.input_buffer { + channel.clear(); + } + } + } + + while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 { + std::hint::spin_loop(); + } + } +} diff --git a/src/emulator.rs b/src/emulator.rs index 18100fd..e1a4836 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1,29 +1,60 @@ use std::{ fs, - path::Path, + path::{Path, PathBuf}, sync::mpsc::{self, TryRecvError}, }; use anyhow::Result; -use crate::{renderer::GameRenderer, shrooms_vb_core::CoreVB}; +use crate::{audio::Audio, renderer::GameRenderer, shrooms_vb_core::CoreVB}; + +pub struct EmulatorBuilder { + rom: Option, + commands: mpsc::Receiver, +} + +impl EmulatorBuilder { + pub fn new() -> (Self, EmulatorClient) { + let (queue, commands) = mpsc::channel(); + let builder = Self { + rom: None, + commands, + }; + let client = EmulatorClient { queue }; + (builder, client) + } + + pub fn with_rom(self, path: &Path) -> Self { + Self { + rom: Some(path.into()), + ..self + } + } + + pub fn build(self) -> Result { + let mut emulator = Emulator::new(self.commands)?; + if let Some(path) = self.rom { + emulator.load_rom(&path)?; + } + Ok(emulator) + } +} pub struct Emulator { sim: CoreVB, + audio: Audio, commands: mpsc::Receiver, renderer: Option, } impl Emulator { - pub fn new() -> (Self, EmulatorClient) { - let (sink, source) = mpsc::channel(); - let emu = Emulator { + fn new(commands: mpsc::Receiver) -> Result { + Ok(Self { sim: CoreVB::new(), - commands: source, + audio: Audio::init()?, + commands, renderer: None, - }; - let queue = EmulatorClient { queue: sink }; - (emu, queue) + }) } pub fn load_rom(&mut self, path: &Path) -> Result<()> { @@ -34,6 +65,7 @@ impl Emulator { pub fn run(&mut self) { let mut eye_contents = vec![0u8; 384 * 224 * 2]; + let mut audio_samples = vec![]; loop { self.sim.emulate_frame(); if let Some(renderer) = &mut self.renderer { @@ -41,6 +73,11 @@ impl Emulator { renderer.render(&eye_contents); } } + self.sim.read_samples(&mut audio_samples); + if !audio_samples.is_empty() { + self.audio.update(&audio_samples); + audio_samples.clear(); + } loop { match self.commands.try_recv() { Ok(command) => self.handle_command(command), diff --git a/src/main.rs b/src/main.rs index 8f625d1..b066de0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,11 @@ use std::{path::PathBuf, thread}; use anyhow::Result; use app::App; use clap::Parser; -use emulator::Emulator; +use emulator::EmulatorBuilder; use winit::event_loop::{ControlFlow, EventLoop}; mod app; +mod audio; mod emulator; mod renderer; mod shrooms_vb_core; @@ -19,9 +20,10 @@ struct Args { fn main() -> Result<()> { let args = Args::parse(); - let (mut emulator, client) = Emulator::new(); - emulator.load_rom(&args.rom)?; + let (builder, client) = EmulatorBuilder::new(); + let builder = builder.with_rom(&args.rom); thread::spawn(move || { + let mut emulator = builder.build().unwrap(); emulator.run(); }); diff --git a/src/shrooms_vb_core.rs b/src/shrooms_vb_core.rs index a28f386..90acc21 100644 --- a/src/shrooms_vb_core.rs +++ b/src/shrooms_vb_core.rs @@ -1,6 +1,7 @@ -use std::{ffi::c_void, ptr}; +use std::{ffi::c_void, ptr, slice}; use anyhow::{anyhow, Result}; +use num_derive::{FromPrimitive, ToPrimitive}; #[repr(C)] struct VB { @@ -9,6 +10,20 @@ struct VB { #[allow(non_camel_case_types)] type c_int = i32; +#[allow(non_camel_case_types)] +type c_uint = u32; + +#[repr(u32)] +#[derive(FromPrimitive, ToPrimitive)] +enum VBDataType { + S8 = 0, + U8 = 1, + S16 = 2, + U16 = 3, + S32 = 4, + F32 = 5, +} + type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; #[link(name = "vb")] @@ -27,6 +42,13 @@ extern "C" { right_stride_x: c_int, right_stride_y: c_int, ); + #[link_name = "vbGetSamples"] + fn vb_get_samples( + sim: *mut VB, + typ_: *mut VBDataType, + capacity: *mut c_uint, + position: *mut c_uint, + ) -> *mut c_void; #[link_name = "vbGetUserData"] fn vb_get_user_data(sim: *mut VB) -> *mut c_void; #[link_name = "vbInit"] @@ -39,6 +61,13 @@ extern "C" { fn vb_set_keys(sim: *mut VB, keys: u16) -> u16; #[link_name = "vbSetFrameCallback"] fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame); + #[link_name = "vbSetSamples"] + fn vb_set_samples( + sim: *mut VB, + samples: *mut c_void, + typ_: VBDataType, + capacity: c_uint, + ) -> c_int; #[link_name = "vbSetUserData"] fn vb_set_user_data(sim: *mut VB, tag: *mut c_void); #[link_name = "vbSizeOf"] @@ -53,6 +82,9 @@ extern "C" fn on_frame(sim: *mut VB) -> i32 { 1 } +const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4; +const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2; + struct VBState { frame_seen: bool, } @@ -79,6 +111,11 @@ impl CoreVB { unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) }; unsafe { vb_set_frame_callback(sim, on_frame) }; + // set up audio buffer + let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS]; + let samples: *mut c_void = Box::into_raw(audio_buffer.into_boxed_slice()).cast(); + unsafe { vb_set_samples(sim, samples, VBDataType::F32, AUDIO_CAPACITY_SAMPLES as u32) }; + CoreVB { sim } } @@ -138,6 +175,25 @@ impl CoreVB { true } + pub fn read_samples(&mut self, samples: &mut Vec) { + let mut position = 0; + let ptr = + unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), &mut position) }; + // SAFETY: position is an offset in a buffer of (f32, f32). so, position * 2 is an offset in a buffer of f32. + let read_samples: &mut [f32] = + unsafe { slice::from_raw_parts_mut(ptr.cast(), position as usize * 2) }; + samples.extend_from_slice(read_samples); + + unsafe { + vb_set_samples( + self.sim, + ptr, + VBDataType::F32, + AUDIO_CAPACITY_SAMPLES as u32, + ) + }; + } + pub fn set_keys(&mut self, keys: u16) { unsafe { vb_set_keys(self.sim, keys) }; } @@ -145,6 +201,14 @@ impl CoreVB { impl Drop for CoreVB { fn drop(&mut self) { + let ptr = + unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) }; + // SAFETY: the audio buffer originally came from a Vec + let floats: Vec = unsafe { + Vec::from_raw_parts(ptr.cast(), AUDIO_CAPACITY_FLOATS, AUDIO_CAPACITY_FLOATS) + }; + drop(floats); + // SAFETY: the *mut VB owns its userdata. // There is no way for the userdata to be null or otherwise invalid. let ptr: *mut VBState = unsafe { vb_get_user_data(self.sim).cast() };