5 Commits

10 changed files with 190 additions and 70 deletions

2
Cargo.lock generated
View File

@@ -2637,7 +2637,7 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "uw8" name = "uw8"
version = "0.2.0-rc1" version = "0.2.0-rc2"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"anyhow", "anyhow",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "uw8" name = "uw8"
version = "0.2.0-rc1" 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

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,9 +29,23 @@ 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 ### v0.2.0-rc1
* [Web runtime](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: Changes:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -197,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
@@ -232,7 +232,10 @@ 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]);
if let Some(ref sound) = instance.sound { if let Some(ref sound) = instance.sound {
sound.tx.send(sound_regs)?; sound.tx.send(RegisterUpdate {
time,
data: sound_regs,
})?;
} }
let framebuffer = &memory[120..(120 + 320 * 240)]; let framebuffer = &memory[120..(120 + 320 * 240)];
@@ -312,9 +315,14 @@ fn instantiate_platform(
Ok(platform_instance) Ok(platform_instance)
} }
struct RegisterUpdate {
time: i32,
data: [u8; 32],
}
struct Uw8Sound { struct Uw8Sound {
stream: cpal::Stream, stream: cpal::Stream,
tx: mpsc::SyncSender<[u8; 32]>, tx: mpsc::SyncSender<RegisterUpdate>,
} }
fn init_sound( fn init_sound(
@@ -368,7 +376,7 @@ fn init_sound(
let buffer_size = match *config.buffer_size() { let buffer_size = match *config.buffer_size() {
cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default, cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default,
cpal::SupportedBufferSize::Range { min, max } => { cpal::SupportedBufferSize::Range { min, max } => {
cpal::BufferSize::Fixed(256.max(min).min(max)) cpal::BufferSize::Fixed(65536.max(min).min(max))
} }
}; };
let config = cpal::StreamConfig { let config = cpal::StreamConfig {
@@ -378,9 +386,7 @@ fn init_sound(
let sample_rate = config.sample_rate.0 as usize; let sample_rate = config.sample_rate.0 as usize;
let (tx, rx) = mpsc::sync_channel::<[u8; 32]>(1); let (tx, rx) = mpsc::sync_channel::<RegisterUpdate>(30);
let start_time = Instant::now();
struct Resampler { struct Resampler {
resampler: rubato::FftFixedIn<f32>, resampler: rubato::FftFixedIn<f32>,
@@ -403,60 +409,91 @@ fn init_sound(
}; };
let mut sample_index = 0; 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( let stream = device.build_output_stream(
&config, &config,
move |mut buffer: &mut [f32], _| { move |mut outer_buffer: &mut [f32], _| {
if let Ok(regs) = rx.try_recv() { let mut first_update = true;
memory.write(&mut store, 80, &regs).unwrap(); 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() {
let time = start_time.elapsed().as_millis() as i32; while pending_updates
let mem = memory.data_mut(&mut store); .first()
mem[64..68].copy_from_slice(&time.to_le_bytes()); .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();
}
if let Some(ref mut resampler) = resampler { let duration = if let Some(update) = pending_updates.first() {
while !buffer.is_empty() { ((update.time.wrapping_sub(current_time) as usize) * sample_rate + 999) / 1000
let copy_size = resampler.output_buffers[0] } else {
.len() outer_buffer.len()
.saturating_sub(resampler.output_index) };
.min(buffer.len() / 2); let step_size = (duration.max(64) * 2).min(outer_buffer.len());
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 let mut buffer = &mut outer_buffer[..step_size];
.resampler
.process_into_buffer( {
&resampler.input_buffers, let mem = memory.data_mut(&mut store);
&mut resampler.output_buffers, mem[64..68].copy_from_slice(&current_time.to_le_bytes());
None, }
)
.unwrap(); if let Some(ref mut resampler) = resampler {
resampler.output_index = 0; while !buffer.is_empty() {
} else { let copy_size = resampler.output_buffers[0]
for i in 0..copy_size { .len()
buffer[i * 2] = resampler.output_buffers[0][resampler.output_index + i]; .saturating_sub(resampler.output_index)
buffer[i * 2 + 1] = .min(buffer.len() / 2);
resampler.output_buffers[1][resampler.output_index + i]; 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..];
} }
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);
} }
} }
} else {
for v in buffer { outer_buffer = &mut outer_buffer[step_size..];
*v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0); current_time =
sample_index = sample_index.wrapping_add(1); current_time.wrapping_add((step_size * 500 / sample_rate).max(1) as i32);
}
} }
}, },
move |err| { move |err| {

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.2.0-rc1 <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);