9 Commits

15 changed files with 764 additions and 351 deletions

View File

@@ -27,7 +27,7 @@ jobs:
steps: steps:
- name: Install dependencies - name: Install dependencies
run: sudo apt-get install -y libxkbcommon-dev run: sudo apt-get install -y libxkbcommon-dev libasound2-dev
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2

554
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,19 @@
[package] [package]
name = "uw8" name = "uw8"
version = "0.1.2" version = "0.2.0-rc2"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["native", "browser"] default = ["native", "browser"]
native = ["wasmtime"] native = ["wasmtime", "minifb", "cpal", "rubato"]
browser = ["warp", "tokio", "tokio-stream", "webbrowser"] browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
[dependencies] [dependencies]
wasmtime = { version = "0.35.3", optional = true } wasmtime = { version = "0.35.3", optional = true }
anyhow = "1" anyhow = "1"
minifb = { version = "0.22", default-features = false, features = ["x11"] } minifb = { version = "0.22", default-features = false, features = ["x11"], optional = true }
notify = "4" notify = "4"
pico-args = "0.4" pico-args = "0.4"
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "aac7bbd" } 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 } tokio-stream = { version = "0.1.8", features = ["sync"], optional = true }
webbrowser = { version = "0.6.0", optional = true } webbrowser = { version = "0.6.0", optional = true }
ansi_term = "0.12.1" ansi_term = "0.12.1"
cpal = "0.13.5" cpal = { version = "0.13.5", optional = true }
rubato = { version = "0.11.0", optional = true }

View File

@@ -0,0 +1,55 @@
// port of cracklebass by pestis (originally on TIC-80)
include "../include/microw8-api.cwa"
const MUSIC_DATA = 0x20000;
export fn upd() {
let inline t = 32!32 * 6 / 100;
let inline p = t / 1024;
let channel:i32;
loop channels {
let inline e = t * channel?MUSIC_DATA / 8;
let lazy pattern = (8 * channel + p)?(MUSIC_DATA + 56);
let lazy n = !!pattern * (8 * pattern + e / 16 % 8)?MUSIC_DATA;
let inline prev_ctrl = (channel * 6)?80;
(channel * 6)?80 = if n {
let inline base_note = 12 + 12 * channel?(MUSIC_DATA + 4) + n;
let inline pitch_drop = e % 16 * channel?(MUSIC_DATA + 94);
let inline key_pattern = p?(MUSIC_DATA + 8*4 + 56);
let inline key = select(key_pattern, (8 * key_pattern + t / 128 % 8)?MUSIC_DATA, 1);
(channel * 6)?83 = base_note - pitch_drop / 4 + key;
prev_ctrl & 0xfc | (e / 8 & 2) | 1
} else {
prev_ctrl & 0xfe
};
branch_if (channel := channel + 1) < 4: channels;
}
}
data 80 {
i8(
0x44, 0, 0, 0, 0x50, 0x40,
0x4, 0x50, 0, 0, 0x80, 0x80,
0x40, 0x80, 0, 0, 0x40, 0x40,
0, 0, 0, 0, 0x50, 0x50
)
}
data MUSIC_DATA {
i8(
16, 2, 8, 8, 1, 2, 2, 3, 1, 0,
1,13,16, 0, 1, 8, 1, 0, 1,13,
16, 1, 1, 8, 1, 0, 8,13,13, 0,
16,13, 1, 0, 1, 0, 1, 0, 1, 1,
1, 0, 0, 0, 1, 0,13, 1, 1, 1,
6, 8, 1, 1, 6, 8, 1, 1, 2, 1,
2, 1, 2, 0, 0, 0, 0, 3, 3, 3,
5, 0, 0, 2, 1, 2, 1, 2, 1, 2,
0, 4, 4, 0, 4, 4, 4, 4, 0, 0,
0, 0, 6, 6, 0, 0, 0, 8
)
}

View File

@@ -29,6 +29,33 @@ Examplers for older versions:
## Versions ## Versions
### v0.2.0-rc2
* [Web runtime](v0.2.0-rc2)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-windows.zip)
Changes:
* fix timing issues of sound playback, especially on systems with large sound buffers
### v0.2.0-rc1
* [Web runtime](v0.2.0-rc1)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-windows.zip)
Changes:
* [add sound support](docs#sound)
* "integer constant cast to float" literal syntax in CurlyWas (ex. `1_f` is equivalent to `1 as f32`)
Known issues:
* timing accuracy/update frequency of sound support currently depends on sound buffer size
### v0.1.2 ### v0.1.2
* [Web runtime](v0.1.2) * [Web runtime](v0.1.2)

View File

@@ -286,6 +286,12 @@ As the only means of communication, 32 bytes starting at address 0x00050 are cop
By default, the `sndGes` function generates sound based on the 32 bytes at 0x00050. This means that in the default configuration those 32 bytes act By default, the `sndGes` function generates sound based on the 32 bytes at 0x00050. This means that in the default configuration those 32 bytes act
as sound registers. See the `sndGes` function for the meaning of those registers. as sound registers. See the `sndGes` function for the meaning of those registers.
### export fn snd(sampleIndex: i32) -> f32
If the module exports a `snd` function, it is called 88200 times per second to provide PCM sample data for playback (44.1kHz stereo).
The `sampleIndex` will start at 0 and increments by 1 for each call. On even indices the function is expected to return a sample value for
the left channel, on odd indices for the right channel.
### fn playNote(channel: i32, note: i32) ### fn playNote(channel: i32, note: i32)
Triggers a note (1-127) on the given channel (0-3). Notes are semitones with 69 being A4 (same as MIDI). A note value of 0 stops the Triggers a note (1-127) on the given channel (0-3). Notes are semitones with 69 being A4 (same as MIDI). A note value of 0 stops the

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@ fn main() -> Result<()> {
let mut args = Arguments::from_env(); let mut args = Arguments::from_env();
// try to enable ansi support in win10 cmd shell // try to enable ansi support in win10 cmd shell
#[cfg(target_os="windows")] #[cfg(target_os = "windows")]
let _ = ansi_term::enable_ansi_support(); let _ = ansi_term::enable_ansi_support();
match args.subcommand()?.as_deref() { match args.subcommand()?.as_deref() {
@@ -79,6 +79,8 @@ fn run(mut args: Arguments) -> Result<()> {
#[cfg(not(feature = "native"))] #[cfg(not(feature = "native"))]
let run_browser = args.contains(["-b", "--browser"]) || true; let run_browser = args.contains(["-b", "--browser"]) || true;
let disable_audio = args.contains(["-m", "--disable-audio"]);
let filename = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?; let filename = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let mut watcher = uw8::FileWatcher::new()?; let mut watcher = uw8::FileWatcher::new()?;
@@ -89,7 +91,13 @@ fn run(mut args: Arguments) -> Result<()> {
#[cfg(not(feature = "native"))] #[cfg(not(feature = "native"))]
unimplemented!(); unimplemented!();
#[cfg(feature = "native")] #[cfg(feature = "native")]
Box::new(MicroW8::new()?) {
let mut microw8 = MicroW8::new()?;
if disable_audio {
microw8.disable_audio();
}
Box::new(microw8)
}
} else { } else {
#[cfg(not(feature = "browser"))] #[cfg(not(feature = "browser"))]
unimplemented!(); unimplemented!();

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@ use std::{thread, time::Instant};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use cpal::traits::*; use cpal::traits::*;
use minifb::{Key, Window, WindowOptions}; use minifb::{Key, Window, WindowOptions};
use rubato::Resampler;
use wasmtime::{ use wasmtime::{
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType, Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
}; };
@@ -27,6 +28,7 @@ pub struct MicroW8 {
window_buffer: Vec<u32>, window_buffer: Vec<u32>,
instance: Option<UW8Instance>, instance: Option<UW8Instance>,
timeout: u32, timeout: u32,
disable_audio: bool,
} }
struct UW8Instance { struct UW8Instance {
@@ -37,7 +39,7 @@ struct UW8Instance {
start_time: Instant, start_time: Instant,
module: Vec<u8>, module: Vec<u8>,
watchdog: Arc<Mutex<UW8WatchDog>>, watchdog: Arc<Mutex<UW8WatchDog>>,
sound: Uw8Sound, sound: Option<Uw8Sound>,
} }
impl Drop for UW8Instance { impl Drop for UW8Instance {
@@ -77,6 +79,7 @@ impl MicroW8 {
window_buffer: vec![0u32; 320 * 240], window_buffer: vec![0u32; 320 * 240],
instance: None, instance: None,
timeout: 30, timeout: 30,
disable_audio: false,
}) })
} }
@@ -86,11 +89,10 @@ impl MicroW8 {
*v = 0; *v = 0;
} }
} }
}
struct Uw8Sound { pub fn disable_audio(&mut self) {
stream: cpal::Stream, self.disable_audio = true;
tx: mpsc::SyncSender<[u8; 32]>, }
} }
impl super::Runtime for MicroW8 { 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_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])?; 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)?; add_native_functions(&mut linker, &mut store)?;
fn instantiate_platform(
linker: &mut wasmtime::Linker<()>,
store: &mut wasmtime::Store<()>,
platform_module: &wasmtime::Module,
) -> Result<wasmtime::Instance> {
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 platform_instance = instantiate_platform(&mut linker, &mut store, &platform_module)?;
let watchdog = Arc::new(Mutex::new(UW8WatchDog { let watchdog = Arc::new(Mutex::new(UW8WatchDog {
@@ -215,84 +165,20 @@ impl super::Runtime for MicroW8 {
let end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?; let end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?;
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd").ok(); let update = instance.get_typed_func::<(), (), _>(&mut store, "upd").ok();
let sound = { let sound = if self.disable_audio {
let mut store = wasmtime::Store::new(&self.engine, ()); None
} else {
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?; match init_sound(&self.engine, &platform_module, &module) {
Ok(sound) => {
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 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, &regs).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 }
};
sound.stream.play()?; sound.stream.play()?;
Some(sound)
}
Err(err) => {
eprintln!("Failed to init sound: {}", err);
None
}
}
};
self.instance = Some(UW8Instance { self.instance = Some(UW8Instance {
store, store,
@@ -311,8 +197,8 @@ impl super::Runtime for MicroW8 {
fn run_frame(&mut self) -> Result<()> { fn run_frame(&mut self) -> Result<()> {
let mut result = Ok(()); let mut result = Ok(());
if let Some(mut instance) = self.instance.take() { if let Some(mut instance) = self.instance.take() {
{
let time = instance.start_time.elapsed().as_millis() as i32; let time = instance.start_time.elapsed().as_millis() as i32;
{
let mut gamepad: u32 = 0; let mut gamepad: u32 = 0;
for key in self.window.get_keys() { for key in self.window.get_keys() {
if let Some(index) = GAMEPAD_KEYS if let Some(index) = GAMEPAD_KEYS
@@ -345,7 +231,12 @@ impl super::Runtime for MicroW8 {
let mut sound_regs = [0u8; 32]; let mut sound_regs = [0u8; 32];
sound_regs.copy_from_slice(&memory[80..112]); sound_regs.copy_from_slice(&memory[80..112]);
instance.sound.tx.send(sound_regs)?; if let Some(ref sound) = instance.sound {
sound.tx.send(RegisterUpdate {
time,
data: sound_regs,
})?;
}
let framebuffer = &memory[120..(120 + 320 * 240)]; let framebuffer = &memory[120..(120 + 320 * 240)];
let palette = &memory[0x13000..]; let palette = &memory[0x13000..];
@@ -371,3 +262,244 @@ impl super::Runtime for MicroW8 {
Ok(()) 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<wasmtime::Instance> {
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 RegisterUpdate {
time: i32,
data: [u8; 32],
}
struct Uw8Sound {
stream: cpal::Stream,
tx: mpsc::SyncSender<RegisterUpdate>,
}
fn init_sound(
engine: &wasmtime::Engine,
platform_module: &wasmtime::Module,
module: &wasmtime::Module,
) -> Result<Uw8Sound> {
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(65536.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::<RegisterUpdate>(30);
struct Resampler {
resampler: rubato::FftFixedIn<f32>,
input_buffers: Vec<Vec<f32>>,
output_buffers: Vec<Vec<f32>>,
output_index: usize,
}
let mut resampler: Option<Resampler> = 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 mut pending_updates: Vec<RegisterUpdate> = Vec::with_capacity(30);
let mut current_time = 0;
let stream = device.build_output_stream(
&config,
move |mut outer_buffer: &mut [f32], _| {
let mut first_update = true;
while let Ok(update) = rx.try_recv() {
if first_update {
current_time += update.time.wrapping_sub(current_time) / 8;
first_update = false;
}
pending_updates.push(update);
}
while !outer_buffer.is_empty() {
while pending_updates
.first()
.into_iter()
.any(|u| u.time.wrapping_sub(current_time) <= 0)
{
let update = pending_updates.remove(0);
memory.write(&mut store, 80, &update.data).unwrap();
}
let duration = if let Some(update) = pending_updates.first() {
((update.time.wrapping_sub(current_time) as usize) * sample_rate + 999) / 1000
} else {
outer_buffer.len()
};
let step_size = (duration.max(64) * 2).min(outer_buffer.len());
let mut buffer = &mut outer_buffer[..step_size];
{
let mem = memory.data_mut(&mut store);
mem[64..68].copy_from_slice(&current_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);
}
}
outer_buffer = &mut outer_buffer[step_size..];
current_time =
current_time.wrapping_add((step_size * 500 / sample_rate).max(1) as i32);
}
},
move |err| {
dbg!(err);
},
)?;
Ok(Uw8Sound { stream, tx })
}

View File

@@ -3,13 +3,17 @@ class APU extends AudioWorkletProcessor {
constructor() { constructor() {
super(); super();
this.sampleIndex = 0; this.sampleIndex = 0;
this.currentTime = 0;
this.isFirstMessage = true;
this.pendingUpdates = [];
this.port.onmessage = (ev) => { this.port.onmessage = (ev) => {
if(this.memory) { if(this.memory) {
if(isNaN(ev.data)) { if(this.isFirstMessage)
U8(this.memory.buffer, 80, 32).set(U8(ev.data)); {
} else { this.currentTime += (ev.data.t - this.currentTime) / 8;
this.startTime = ev.data; this.isFirstMessage = false;
} }
this.pendingUpdates.push(ev.data);
} else { } else {
this.load(ev.data[0], ev.data[1]); this.load(ev.data[0], ev.data[1]);
} }
@@ -55,9 +59,13 @@ class APU extends AudioWorkletProcessor {
} }
process(inputs, outputs, parameters) { process(inputs, outputs, parameters) {
if(this.snd && this.startTime) { this.isFirstMessage = true;
if(this.snd) {
while(this.pendingUpdates.length > 0 && this.pendingUpdates[0].t <= this.currentTime) {
U8(this.memory.buffer, 80, 32).set(U8(this.pendingUpdates.shift().r));
}
let u32Mem = new Uint32Array(this.memory.buffer); let u32Mem = new Uint32Array(this.memory.buffer);
u32Mem[16] = Date.now() - this.startTime; u32Mem[16] = this.currentTime;
let channels = outputs[0]; let channels = outputs[0];
let index = this.sampleIndex; let index = this.sampleIndex;
let numSamples = channels[0].length; let numSamples = channels[0].length;
@@ -66,6 +74,7 @@ class APU extends AudioWorkletProcessor {
channels[1][i] = this.snd(index++); channels[1][i] = this.snd(index++);
} }
this.sampleIndex = index & 0xffffffff; this.sampleIndex = index & 0xffffffff;
this.currentTime += numSamples / 44.1;
} }
return true; return true;

View File

@@ -10,7 +10,7 @@
</head> </head>
<body> <body>
<div id="uw8"> <div id="uw8">
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.1.2 <a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.2.0-rc2
</div> </div>
<div id="centered"> <div id="centered">
<canvas class="screen" id="screen" width="320" height="240"> <canvas class="screen" id="screen" width="320" height="240">

View File

@@ -107,7 +107,7 @@ export default function MicroW8(screen, config = {}) {
audioContext.close(); audioContext.close();
keepRunning = false; keepRunning = false;
abortController.abort(); abortController.abort();
} };
let cartridgeSize = data.byteLength; let cartridgeSize = data.byteLength;
@@ -232,7 +232,6 @@ export default function MicroW8(screen, config = {}) {
let startTime = Date.now(); let startTime = Date.now();
const timePerFrame = 1000 / 60; const timePerFrame = 1000 / 60;
let nextFrame = startTime;
audioNode.connect(audioContext.destination); audioNode.connect(audioContext.destination);
@@ -244,12 +243,10 @@ export default function MicroW8(screen, config = {}) {
isPaused = false; isPaused = false;
audioContext.resume(); audioContext.resume();
startTime += now - pauseTime; startTime += now - pauseTime;
audioNode.port.postMessage(startTime);
} else { } else {
isPaused = true; isPaused = true;
audioContext.suspend(); audioContext.suspend();
pauseTime = now; pauseTime = now;
audioNode.port.postMessage(0);
} }
}; };
window.addEventListener('focus', () => updateVisibility(true), { signal: abortController.signal }); window.addEventListener('focus', () => updateVisibility(true), { signal: abortController.signal });
@@ -263,6 +260,7 @@ export default function MicroW8(screen, config = {}) {
try { try {
let restart = false; let restart = false;
let thisFrame;
if (!isPaused) { if (!isPaused) {
let gamepads = navigator.getGamepads(); let gamepads = navigator.getGamepads();
let gamepad = 0; let gamepad = 0;
@@ -291,7 +289,8 @@ export default function MicroW8(screen, config = {}) {
} }
let u32Mem = U32(memory.buffer); let u32Mem = U32(memory.buffer);
u32Mem[16] = Date.now() - startTime; let time = Date.now() - startTime;
u32Mem[16] = time;
u32Mem[17] = pad | gamepad; u32Mem[17] = pad | gamepad;
if(instance.exports.upd) { if(instance.exports.upd) {
instance.exports.upd(); instance.exports.upd();
@@ -300,16 +299,21 @@ export default function MicroW8(screen, config = {}) {
let soundRegisters = new ArrayBuffer(32); let soundRegisters = new ArrayBuffer(32);
U8(soundRegisters).set(U8(memory.buffer, 80, 32)); U8(soundRegisters).set(U8(memory.buffer, 80, 32));
audioNode.port.postMessage(soundRegisters, [soundRegisters]); audioNode.port.postMessage({t: time, r: soundRegisters}, [soundRegisters]);
let palette = U32(memory.buffer, 0x13000, 1024); let palette = U32(memory.buffer, 0x13000, 1024);
for (let i = 0; i < 320 * 240; ++i) { for (let i = 0; i < 320 * 240; ++i) {
buffer[i] = palette[memU8[i + 120]] | 0xff000000; buffer[i] = palette[memU8[i + 120]] | 0xff000000;
} }
canvasCtx.putImageData(imageData, 0, 0); canvasCtx.putImageData(imageData, 0, 0);
let timeOffset = ((time * 6) % 100 - 50) / 6;
thisFrame = startTime + time - timeOffset / 8;
} else {
thisFrame = Date.now();
} }
let now = Date.now(); let now = Date.now();
nextFrame = Math.max(nextFrame + timePerFrame, now); let nextFrame = Math.max(thisFrame + timePerFrame, now);
if (restart) { if (restart) {
runModule(currentData); runModule(currentData);