From 2dee1b30a4f1c8a0bae9e3e43ccc446b71fa4c87 Mon Sep 17 00:00:00 2001 From: Dennis Ranke Date: Tue, 26 Apr 2022 00:02:34 +0200 Subject: [PATCH] add support for non 44.1kHz audio configs (resampling) --- Cargo.lock | 80 ++++++++++ Cargo.toml | 7 +- src/main.rs | 12 +- src/run_native.rs | 363 +++++++++++++++++++++++++++++----------------- 4 files changed, 323 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3388101..3a2d0bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1578,6 +1578,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -1589,6 +1598,16 @@ dependencies = [ "syn", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -1795,6 +1814,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "primal-check" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01419cee72c1a1ca944554e23d83e483e1bccf378753344e881de28b5487511d" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro-crate" version = "1.1.2" @@ -1928,6 +1956,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "realfft" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a83b876fe55da7e1bf5deeacb93d6411edf81eba0e1a497e79c067734729053a" +dependencies = [ + "rustfft", +] + [[package]] name = "redox_syscall" version = "0.2.10" @@ -1996,6 +2033,18 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rubato" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ceb88cff5e2414abd59f1470673e1bba0627118dfc13ce33cebf67ea4a8acb0" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -2008,6 +2057,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustfft" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d089e5c57521629a59f5f39bca7434849ff89bd6873b521afe389c1c602543" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + [[package]] name = "rustix" version = "0.33.3" @@ -2218,6 +2281,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" +[[package]] +name = "strength_reduce" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ff2f71c82567c565ba4b3009a9350a96a7269eaa4001ebedae926230bc2254" + [[package]] name = "strsim" version = "0.8.0" @@ -2434,6 +2503,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "transpose" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f9c900aa98b6ea43aee227fd680550cdec726526aab8ac801549eadb25e39f" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -2567,6 +2646,7 @@ dependencies = [ "minifb", "notify", "pico-args", + "rubato", "same-file", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index eef549b..d5a1e67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,13 +7,13 @@ edition = "2021" [features] default = ["native", "browser"] -native = ["wasmtime"] +native = ["wasmtime", "minifb", "cpal", "rubato"] browser = ["warp", "tokio", "tokio-stream", "webbrowser"] [dependencies] wasmtime = { version = "0.35.3", optional = true } anyhow = "1" -minifb = { version = "0.22", default-features = false, features = ["x11"] } +minifb = { version = "0.22", default-features = false, features = ["x11"], optional = true } notify = "4" pico-args = "0.4" curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "aac7bbd" } @@ -25,4 +25,5 @@ tokio = { version = "1.17.0", features = ["sync", "rt"], optional = true } tokio-stream = { version = "0.1.8", features = ["sync"], optional = true } webbrowser = { version = "0.6.0", optional = true } ansi_term = "0.12.1" -cpal = "0.13.5" \ No newline at end of file +cpal = { version = "0.13.5", optional = true } +rubato = { version = "0.11.0", optional = true } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a820f36..70ae779 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ fn main() -> Result<()> { let mut args = Arguments::from_env(); // try to enable ansi support in win10 cmd shell - #[cfg(target_os="windows")] + #[cfg(target_os = "windows")] let _ = ansi_term::enable_ansi_support(); match args.subcommand()?.as_deref() { @@ -79,6 +79,8 @@ fn run(mut args: Arguments) -> Result<()> { #[cfg(not(feature = "native"))] let run_browser = args.contains(["-b", "--browser"]) || true; + let disable_audio = args.contains(["-m", "--disable-audio"]); + let filename = args.free_from_os_str::(|s| Ok(s.into()))?; let mut watcher = uw8::FileWatcher::new()?; @@ -89,7 +91,13 @@ fn run(mut args: Arguments) -> Result<()> { #[cfg(not(feature = "native"))] unimplemented!(); #[cfg(feature = "native")] - Box::new(MicroW8::new()?) + { + let mut microw8 = MicroW8::new()?; + if disable_audio { + microw8.disable_audio(); + } + Box::new(microw8) + } } else { #[cfg(not(feature = "browser"))] unimplemented!(); diff --git a/src/run_native.rs b/src/run_native.rs index 208a3ad..0180858 100644 --- a/src/run_native.rs +++ b/src/run_native.rs @@ -5,6 +5,7 @@ use std::{thread, time::Instant}; use anyhow::{anyhow, Result}; use cpal::traits::*; use minifb::{Key, Window, WindowOptions}; +use rubato::Resampler; use wasmtime::{ Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType, }; @@ -27,6 +28,7 @@ pub struct MicroW8 { window_buffer: Vec, instance: Option, timeout: u32, + disable_audio: bool, } struct UW8Instance { @@ -37,7 +39,7 @@ struct UW8Instance { start_time: Instant, module: Vec, watchdog: Arc>, - sound: Uw8Sound, + sound: Option, } impl Drop for UW8Instance { @@ -77,6 +79,7 @@ impl MicroW8 { window_buffer: vec![0u32; 320 * 240], instance: None, timeout: 30, + disable_audio: false, }) } @@ -86,11 +89,10 @@ impl MicroW8 { *v = 0; } } -} -struct Uw8Sound { - stream: cpal::Stream, - tx: mpsc::SyncSender<[u8; 32]>, + pub fn disable_audio(&mut self) { + self.disable_audio = true; + } } impl super::Runtime for MicroW8 { @@ -126,60 +128,8 @@ impl super::Runtime for MicroW8 { let module_length = load_uw8.call(&mut store, module_data.len() as i32)? as u32 as usize; let module = wasmtime::Module::new(&self.engine, &memory.data(&store)[..module_length])?; - fn add_native_functions( - linker: &mut wasmtime::Linker<()>, - store: &mut wasmtime::Store<()>, - ) -> Result<()> { - linker.func_wrap("env", "acos", |v: f32| v.acos())?; - linker.func_wrap("env", "asin", |v: f32| v.asin())?; - linker.func_wrap("env", "atan", |v: f32| v.atan())?; - linker.func_wrap("env", "atan2", |x: f32, y: f32| x.atan2(y))?; - linker.func_wrap("env", "cos", |v: f32| v.cos())?; - linker.func_wrap("env", "exp", |v: f32| v.exp())?; - linker.func_wrap("env", "log", |v: f32| v.ln())?; - linker.func_wrap("env", "sin", |v: f32| v.sin())?; - linker.func_wrap("env", "tan", |v: f32| v.tan())?; - linker.func_wrap("env", "pow", |a: f32, b: f32| a.powf(b))?; - for i in 10..64 { - linker.func_wrap("env", &format!("reserved{}", i), || {})?; - } - for i in 0..16 { - linker.define( - "env", - &format!("g_reserved{}", i), - wasmtime::Global::new( - &mut *store, - GlobalType::new(ValType::I32, Mutability::Const), - 0.into(), - )?, - )?; - } - - Ok(()) - } - add_native_functions(&mut linker, &mut store)?; - fn instantiate_platform( - linker: &mut wasmtime::Linker<()>, - store: &mut wasmtime::Store<()>, - platform_module: &wasmtime::Module, - ) -> Result { - let platform_instance = linker.instantiate(&mut *store, &platform_module)?; - - for export in platform_instance.exports(&mut *store) { - linker.define( - "env", - export.name(), - export - .into_func() - .expect("platform surely only exports functions"), - )?; - } - - Ok(platform_instance) - } - let platform_instance = instantiate_platform(&mut linker, &mut store, &platform_module)?; let watchdog = Arc::new(Mutex::new(UW8WatchDog { @@ -215,85 +165,21 @@ impl super::Runtime for MicroW8 { let end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?; let update = instance.get_typed_func::<(), (), _>(&mut store, "upd").ok(); - let sound = { - let mut store = wasmtime::Store::new(&self.engine, ()); - - let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?; - - let mut linker = wasmtime::Linker::new(&self.engine); - linker.define("env", "memory", memory)?; - add_native_functions(&mut linker, &mut store)?; - - let platform_instance = - instantiate_platform(&mut linker, &mut store, &platform_module)?; - let instance = linker.instantiate(&mut store, &module)?; - - let snd = instance - .get_typed_func::<(i32,), f32, _>(&mut store, "snd") - .or_else(|_| { - platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "gesSnd") - })?; - - let host = cpal::default_host(); - let device = host - .default_output_device() - .ok_or_else(|| anyhow!("No audio output device available"))?; - let config = device - .supported_output_configs()? - .find(|config| { - config.min_sample_rate().0 <= 44100 - && config.max_sample_rate().0 >= 44100 - && config.channels() == 2 - && config.sample_format() == cpal::SampleFormat::F32 - }) - .ok_or_else(|| anyhow!("Could not find 44.1kHz float config"))?; - let config = config.with_sample_rate(cpal::SampleRate(44100)); - let buffer_size = match *config.buffer_size() { - cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default, - cpal::SupportedBufferSize::Range { min, max } => { - cpal::BufferSize::Fixed(256.max(min).min(max)) + let sound = if self.disable_audio { + None + } else { + match init_sound(&self.engine, &platform_module, &module) { + Ok(sound) => { + sound.stream.play()?; + Some(sound) } - }; - let config = cpal::StreamConfig { - buffer_size, - ..config.config() - }; - - let (tx, rx) = mpsc::sync_channel::<[u8; 32]>(1); - - let start_time = Instant::now(); - - let mut sample_index = 0; - let stream = { - device.build_output_stream( - &config, - move |buffer: &mut [f32], _| { - if let Ok(regs) = rx.try_recv() { - memory.write(&mut store, 80, ®s).unwrap(); - } - - { - let time = start_time.elapsed().as_millis() as i32; - let mem = memory.data_mut(&mut store); - mem[64..68].copy_from_slice(&time.to_le_bytes()); - } - - for v in buffer { - *v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0); - sample_index = sample_index.wrapping_add(1); - } - }, - move |err| { - dbg!(err); - }, - )? - }; - - Uw8Sound { stream, tx } + Err(err) => { + eprintln!("Failed to init sound: {}", err); + None + } + } }; - sound.stream.play()?; - self.instance = Some(UW8Instance { store, memory, @@ -345,7 +231,9 @@ impl super::Runtime for MicroW8 { let mut sound_regs = [0u8; 32]; sound_regs.copy_from_slice(&memory[80..112]); - instance.sound.tx.send(sound_regs)?; + if let Some(ref sound) = instance.sound { + sound.tx.send(sound_regs)?; + } let framebuffer = &memory[120..(120 + 320 * 240)]; let palette = &memory[0x13000..]; @@ -371,3 +259,210 @@ impl super::Runtime for MicroW8 { Ok(()) } } + +fn add_native_functions( + linker: &mut wasmtime::Linker<()>, + store: &mut wasmtime::Store<()>, +) -> Result<()> { + linker.func_wrap("env", "acos", |v: f32| v.acos())?; + linker.func_wrap("env", "asin", |v: f32| v.asin())?; + linker.func_wrap("env", "atan", |v: f32| v.atan())?; + linker.func_wrap("env", "atan2", |x: f32, y: f32| x.atan2(y))?; + linker.func_wrap("env", "cos", |v: f32| v.cos())?; + linker.func_wrap("env", "exp", |v: f32| v.exp())?; + linker.func_wrap("env", "log", |v: f32| v.ln())?; + linker.func_wrap("env", "sin", |v: f32| v.sin())?; + linker.func_wrap("env", "tan", |v: f32| v.tan())?; + linker.func_wrap("env", "pow", |a: f32, b: f32| a.powf(b))?; + for i in 10..64 { + linker.func_wrap("env", &format!("reserved{}", i), || {})?; + } + for i in 0..16 { + linker.define( + "env", + &format!("g_reserved{}", i), + wasmtime::Global::new( + &mut *store, + GlobalType::new(ValType::I32, Mutability::Const), + 0.into(), + )?, + )?; + } + + Ok(()) +} + +fn instantiate_platform( + linker: &mut wasmtime::Linker<()>, + store: &mut wasmtime::Store<()>, + platform_module: &wasmtime::Module, +) -> Result { + let platform_instance = linker.instantiate(&mut *store, &platform_module)?; + + for export in platform_instance.exports(&mut *store) { + linker.define( + "env", + export.name(), + export + .into_func() + .expect("platform surely only exports functions"), + )?; + } + + Ok(platform_instance) +} + +struct Uw8Sound { + stream: cpal::Stream, + tx: mpsc::SyncSender<[u8; 32]>, +} + +fn init_sound( + engine: &wasmtime::Engine, + platform_module: &wasmtime::Module, + module: &wasmtime::Module, +) -> Result { + let mut store = wasmtime::Store::new(engine, ()); + + let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?; + + let mut linker = wasmtime::Linker::new(engine); + linker.define("env", "memory", memory)?; + add_native_functions(&mut linker, &mut store)?; + + let platform_instance = instantiate_platform(&mut linker, &mut store, platform_module)?; + let instance = linker.instantiate(&mut store, module)?; + + let snd = instance + .get_typed_func::<(i32,), f32, _>(&mut store, "snd") + .or_else(|_| platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "gesSnd"))?; + + let host = cpal::default_host(); + let device = host + .default_output_device() + .ok_or_else(|| anyhow!("No audio output device available"))?; + let mut configs: Vec<_> = device + .supported_output_configs()? + .filter(|config| { + config.channels() == 2 && config.sample_format() == cpal::SampleFormat::F32 + }) + .collect(); + configs.sort_by_key(|config| { + let rate = 44100 + .max(config.min_sample_rate().0) + .min(config.max_sample_rate().0); + if rate >= 44100 { + rate - 44100 + } else { + (44100 - rate) * 1000 + } + }); + let config = configs + .into_iter() + .next() + .ok_or_else(|| anyhow!("Could not find float output config"))?; + let sample_rate = cpal::SampleRate(44100) + .max(config.min_sample_rate()) + .max(config.max_sample_rate()); + let config = config.with_sample_rate(sample_rate); + let buffer_size = match *config.buffer_size() { + cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default, + cpal::SupportedBufferSize::Range { min, max } => { + cpal::BufferSize::Fixed(256.max(min).min(max)) + } + }; + let config = cpal::StreamConfig { + buffer_size, + ..config.config() + }; + + let sample_rate = config.sample_rate.0 as usize; + + let (tx, rx) = mpsc::sync_channel::<[u8; 32]>(1); + + let start_time = Instant::now(); + + struct Resampler { + resampler: rubato::FftFixedIn, + input_buffers: Vec>, + output_buffers: Vec>, + output_index: usize, + } + let mut resampler: Option = if sample_rate == 44100 { + None + } else { + let rs = rubato::FftFixedIn::new(44100, sample_rate, 128, 1, 2)?; + let input_buffers = rs.input_buffer_allocate(); + let output_buffers = rs.output_buffer_allocate(); + Some(Resampler { + resampler: rs, + input_buffers, + output_buffers, + output_index: usize::MAX, + }) + }; + + let mut sample_index = 0; + let stream = device.build_output_stream( + &config, + move |mut buffer: &mut [f32], _| { + if let Ok(regs) = rx.try_recv() { + memory.write(&mut store, 80, ®s).unwrap(); + } + + { + let time = start_time.elapsed().as_millis() as i32; + let mem = memory.data_mut(&mut store); + mem[64..68].copy_from_slice(&time.to_le_bytes()); + } + + if let Some(ref mut resampler) = resampler { + while !buffer.is_empty() { + let copy_size = resampler.output_buffers[0] + .len() + .saturating_sub(resampler.output_index) + .min(buffer.len() / 2); + if copy_size == 0 { + resampler.input_buffers[0].clear(); + resampler.input_buffers[1].clear(); + for _ in 0..resampler.resampler.input_frames_next() { + resampler.input_buffers[0] + .push(snd.call(&mut store, (sample_index,)).unwrap_or(0.0)); + resampler.input_buffers[1] + .push(snd.call(&mut store, (sample_index + 1,)).unwrap_or(0.0)); + sample_index = sample_index.wrapping_add(2); + } + + resampler + .resampler + .process_into_buffer( + &resampler.input_buffers, + &mut resampler.output_buffers, + None, + ) + .unwrap(); + resampler.output_index = 0; + } else { + for i in 0..copy_size { + buffer[i * 2] = resampler.output_buffers[0][resampler.output_index + i]; + buffer[i * 2 + 1] = + resampler.output_buffers[1][resampler.output_index + i]; + } + resampler.output_index += copy_size; + buffer = &mut buffer[copy_size * 2..]; + } + } + } else { + for v in buffer { + *v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0); + sample_index = sample_index.wrapping_add(1); + } + } + }, + move |err| { + dbg!(err); + }, + )?; + + Ok(Uw8Sound { stream, tx }) +}