6 Commits

Author SHA1 Message Date
4c82f4ad02 add 0.2.0rc1 web runtime 2022-04-24 23:32:28 +02:00
4dd8c3b029 add audio track to recorded video 2022-04-24 23:17:58 +02:00
2bf8938183 remove back-channel from audio thread for now
It needs some more thought before committing to it.
2022-04-24 15:52:19 +02:00
491bf88ade add first version of sound doku 2022-04-24 00:01:54 +02:00
e05701300c implement backchannel from audio thread 2022-04-22 00:28:19 +02:00
df0c169d54 use the last byte 2022-04-20 23:19:26 +02:00
12 changed files with 867 additions and 3955 deletions

View File

@@ -20,10 +20,10 @@ data 0x20000 {
0x44, 0x0, 0x0, 0x47, 0x49, 0x47, 0x45, 0x41, 0x44, 0x0, 0x0, 0x47, 0x49, 0x47, 0x45, 0x41,
0x44, 0x0, 0x0, 0x0, 0x42, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x42, 0x0, 0x0, 0x0,
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0, 0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0, 0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0, 0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0, 0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
0x2a, 0x0, 0x0, 0x0, 0x2d, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x0, 0x0, 0x2d, 0x0, 0x0, 0x0,
0x2c, 0x0, 0x28, 0x0, 0x2a, 0x0, 0x0, 0x0, 0x2c, 0x0, 0x28, 0x0, 0x2a, 0x0, 0x0, 0x0,

Binary file not shown.

View File

@@ -16,7 +16,7 @@ export fn gesSnd(t: i32) -> f32 {
let i: i32; let i: i32;
loop clearLoop { loop clearLoop {
(baseAddr + i)!GesBufferOffset = 0; (baseAddr + i)!GesBufferOffset = 0;
branch_if (i := i + 4) < 128*8: clearLoop; branch_if (i := i + 4) < 128*4: clearLoop;
} }
let ch: i32; let ch: i32;

View File

@@ -43,27 +43,6 @@ Changes:
* CurlyWas: implement support for constants * CurlyWas: implement support for constants
* fix crash when trying to draw zero sized line * fix crash when trying to draw zero sized line
### v0.1.1 ### Older versions
* [Web runtime](v0.1.1) [Find older versions here.](versions)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-windows.zip)
Changes:
* implement more robust file watcher
* add basic video recording on F10 in web runtime
* add screenshot on F9
* add watchdog to interrupt hanging update in native runtime
* add devkit mode to web runtime
* add unpack and compile commands to uw8
* add support for table/element section in pack command
* disable wayland support (caused missing window decorations in gnome)
### v0.1.0
* [Web runtime](v0.1.0)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-windows.zip)

View File

@@ -23,9 +23,7 @@ The memory has to be imported as `env` `memory` and has a maximum size of 256kb
00070-00078: reserved 00070-00078: reserved
00078-12c78: frame buffer 00078-12c78: frame buffer
12c78-12c7c: sound registers/work area base address (for sndGes function) 12c78-12c7c: sound registers/work area base address (for sndGes function)
12c7c-12c80: reserved 12c7c-13000: reserved
12c80-12ca0: sound data (synced from sound thread)
12ca0-13000: reserved
13000-13400: palette 13000-13400: palette
13400-13c00: font 13400-13c00: font
13c00-14000: reserved 13c00-14000: reserved
@@ -234,7 +232,8 @@ Avoid the reserved control chars, they are currently NOPs but their behavior can
| 2-3 | - | Reserved | | 2-3 | - | Reserved |
| 4 | - | Switch to normal mode | | 4 | - | Switch to normal mode |
| 5 | - | Switch to graphics mode | | 5 | - | Switch to graphics mode |
| 6-7 | - | Reserved | | 6 | - | Reserved |
| 7 | - | Bell / trigger sound channel 0 |
| 8 | - | Move cursor left | | 8 | - | Move cursor left |
| 9 | - | Move cursor right | | 9 | - | Move cursor right |
| 10 | - | Move cursor down | | 10 | - | Move cursor down |
@@ -276,34 +275,120 @@ Sets the cursor position. In normal mode `x` and `y` are multiplied by 8 to get
## Sound ## Sound
### Low level operation
MicroW8 actually runs two instances of your module. On the first instance, it calls `upd` and displays the framebuffer found in its memory. On the
second instance, it calls `snd` instead with an incrementing sample index and expects that function to return sound samples for the left and right
channel at 44100 Hz. If your module does not export a `snd` function, it calls the api function `sndGes` instead.
As the only means of communication, 32 bytes starting at address 0x00050 are copied from main to sound memory after `upd` returns.
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.
### 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
sound playing on that channel. A note value 128-255 will trigger note-128 and immediately stop it (playing attack+release parts of envelope).
This function assumes the default setup, with the `sndGes` registers located at 0x00050.
### fn sndGes(sampleIndex: i32) -> f32
This implements a sound chip, generating sound based on 32 bytes of sound registers.
The spec of this sound chip are:
- 4 channels with individual volume control (0-15)
- rect, saw, tri, noise wave forms selectable per channel
- each wave form supports some kind of pulse width modulation
- each channel has an optional automatic low pass filter, or can be sent to one of two manually controllable filters
- each channel can select between a narrow and a wide stereo positioning. The two stereo positions of each channel are fixed.
- optional ring modulation
This function requires 1024 bytes of working memory, the first 32 bytes of which are interpreted as the sound registers.
The base address of its working memory can be configured by writing the address to 0x12c78. It defaults to 0x00050.
Here is a short description of the 32 sound registers.
``` ```
Per channel: 00 - CTRL0
06 - CTRL1
0c - CTRL2
12 - CTRL3
| 7 6 | 5 | 4 | 3 2 | 1 | 0 |
| wave | ring | wide | filter | trigger | note on |
00 : CTRL - wave form, ring, sync, filter send, trigger note on: stay in decay/sustain part of envelope
bit 0: note on flag trigger: the attack part of the envlope is triggered when either this changes
bit 1: note trigger or note on is changed from 0 to 1.
bit 2,3: filter 0,1 send filter : 0 - no filter
bit 6,7: wave form (rect, saw, tri, noise) 1 - fixed 6db 1-pole filter with cutoff two octaves above note
01 : PULS - pulse width 2 - programmable filter 0
02 : FINE - fine tuning 3 - programmable filter 1
03 : NOTE - note wide : use wide stereo panning
04 : ENVA - attack, decay ring : ring modulate with triangle wave at frequency of previous channel
05 : ENVR - sustain, release wave : 0 - rectangle
1 - saw
2 - triangle
3 - noise
50-56: channel 0 01 - PULS0
56-5b: channel 1 07 - PULS1
5c-61: channel 2 0d - PULS2
62-67: channel 3 13 - PULS3
Pulse width 0-255, with 0 being the plain version of each wave form.
rectangle - 50%-100% pulse width
saw - inverts 0%-100% of the saw wave form around the center
triangle - morphs into an octave up triangle wave
noise - blends into a decimated saw wave (just try it out)
68: VO01 - volumes channel 0&1 02 - FINE0
69: VO23 - volumes channel 2&3 08 - FINE1
0e - FINE2
14 - FINE3
Fractional note
6a : FCTR 0 - type, resonance 03 - NOTE0
6b : FCTR 1 - type, resonance 09 - NOTE1
6c : FFIN 0 - cutoff fine tuning 0f - NOTE2
6d : FNOT 0 - cutoff note 15 - NOTE3
6e : FFIN 1 - cutoff fine tuning Note, 69 = A4
6f : FNOT 1 - cutoff note
04 - ENVA0
0a - ENVA1
10 - ENVA2
16 - ENVA3
| 7 6 5 4 | 3 2 1 0 |
| decay | attack |
05 - ENVB0
0b - ENVB1
11 - ENVB2
17 - ENVB3
| 7 6 5 4 | 3 2 1 0 |
| release | sustain |
18 - VO01
| 7 6 5 4 | 3 2 1 0 |
| volume 1 | volume 0 |
19 - VO23
| 7 6 5 4 | 3 2 1 0 |
| volume 3 | volume 2 |
1a - FCTR0
1b - FCTR1
| 7 6 5 4 | 3 | 2 | 1 | 0 |
| resonance | 0 | band | high | low |
1c - FFIN0
1e - FFIN1
cutoff frequency - fractional note
1d - FNOT0
1f - FNOT1
cutoff frequency - note
``` ```
# The `uw8` tool # The `uw8` tool

42
site/content/versions.md Normal file
View File

@@ -0,0 +1,42 @@
+++
description = "Versions"
+++
### v0.1.2
* [Web runtime](v0.1.2)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-windows.zip)
Changes:
* add option to `uw8 run` to run the cart in the browser using the web runtime
* CurlyWas: implement `include` support
* CurlyWas: implement support for constants
* fix crash when trying to draw zero sized line
### v0.1.1
* [Web runtime](v0.1.1)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-windows.zip)
Changes:
* implement more robust file watcher
* add basic video recording on F10 in web runtime
* add screenshot on F9
* add watchdog to interrupt hanging update in native runtime
* add devkit mode to web runtime
* add unpack and compile commands to uw8
* add support for table/element section in pack command
* disable wayland support (caused missing window decorations in gnome)
### v0.1.0
* [Web runtime](v0.1.0)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-windows.zip)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -261,6 +261,8 @@ impl super::Runtime for MicroW8 {
let (tx, rx) = mpsc::sync_channel::<[u8; 32]>(1); let (tx, rx) = mpsc::sync_channel::<[u8; 32]>(1);
let start_time = Instant::now();
let mut sample_index = 0; let mut sample_index = 0;
let stream = { let stream = {
device.build_output_stream( device.build_output_stream(
@@ -269,6 +271,13 @@ impl super::Runtime for MicroW8 {
if let Ok(regs) = rx.try_recv() { if let Ok(regs) = rx.try_recv() {
memory.write(&mut store, 80, &regs).unwrap(); 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 { for v in buffer {
*v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0); *v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0);
sample_index = sample_index.wrapping_add(1); sample_index = sample_index.wrapping_add(1);

View File

@@ -5,7 +5,11 @@ class APU extends AudioWorkletProcessor {
this.sampleIndex = 0; this.sampleIndex = 0;
this.port.onmessage = (ev) => { this.port.onmessage = (ev) => {
if(this.memory) { if(this.memory) {
U8(this.memory.buffer, 80, 32).set(U8(ev.data)); if(isNaN(ev.data)) {
U8(this.memory.buffer, 80, 32).set(U8(ev.data));
} else {
this.startTime = ev.data;
}
} else { } else {
this.load(ev.data[0], ev.data[1]); this.load(ev.data[0], ev.data[1]);
} }
@@ -51,7 +55,9 @@ class APU extends AudioWorkletProcessor {
} }
process(inputs, outputs, parameters) { process(inputs, outputs, parameters) {
if(this.snd) { if(this.snd && this.startTime) {
let u32Mem = new Uint32Array(this.memory.buffer);
u32Mem[16] = Date.now() - this.startTime;
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;

View File

@@ -91,13 +91,16 @@ export default function MicroW8(screen, config = {}) {
keyboardElement.onkeyup = keyHandler; keyboardElement.onkeyup = keyHandler;
} }
let audioContext;
let audioNode;
async function runModule(data, keepUrl) { async function runModule(data, keepUrl) {
if (cancelFunction) { if (cancelFunction) {
cancelFunction(); cancelFunction();
cancelFunction = null; cancelFunction = null;
} }
let audioContext = new AudioContext({sampleRate: 44100}); audioContext = new AudioContext({sampleRate: 44100});
let keepRunning = true; let keepRunning = true;
let abortController = new AbortController(); let abortController = new AbortController();
cancelFunction = () => { cancelFunction = () => {
@@ -114,7 +117,7 @@ export default function MicroW8(screen, config = {}) {
} }
await audioContext.audioWorklet.addModule(audioWorkletUrl); await audioContext.audioWorklet.addModule(audioWorkletUrl);
let audioNode = new AudioNode(audioContext); audioNode = new AudioNode(audioContext);
let audioReadyFlags = 0; let audioReadyFlags = 0;
let audioReadyResolve; let audioReadyResolve;
@@ -126,7 +129,6 @@ export default function MicroW8(screen, config = {}) {
audioReadyResolve = null; audioReadyResolve = null;
} }
}; };
audioNode.port.onmessage = (e) => updateAudioReady(e.data);
let audioStateChange = () => { let audioStateChange = () => {
if(audioContext.state == 'suspended') { if(audioContext.state == 'suspended') {
if(config.startButton) { if(config.startButton) {
@@ -211,6 +213,8 @@ export default function MicroW8(screen, config = {}) {
data = loadModuleData(data); data = loadModuleData(data);
let platform_data = await loadModuleURL(platformUrl); let platform_data = await loadModuleURL(platformUrl);
audioNode.port.onmessage = (e) => updateAudioReady(e.data);
audioNode.port.postMessage([platform_data, data]); audioNode.port.postMessage([platform_data, data]);
let platform_instance = await instantiate(platform_data); let platform_instance = await instantiate(platform_data);
@@ -240,10 +244,12 @@ 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 });
@@ -331,14 +337,25 @@ export default function MicroW8(screen, config = {}) {
let videoRecorder; let videoRecorder;
let videoStartTime; let videoStartTime;
let videoAudioSourceNode;
let videoAudioStreamNode;
function recordVideo() { function recordVideo() {
if(videoRecorder) { if(videoRecorder) {
videoRecorder.stop(); videoRecorder.stop();
videoRecorder = null; videoRecorder = null;
videoAudioSourceNode.disconnect(videoAudioStreamNode);
videoAudioSourceNode = null;
videoAudioStreamNode = null;
return; return;
} }
videoRecorder = new MediaRecorder(screen.captureStream(), { let stream = screen.captureStream();
videoAudioStreamNode = audioContext.createMediaStreamDestination();
videoAudioSourceNode = audioNode;
audioNode.connect(videoAudioStreamNode);
stream.addTrack(videoAudioStreamNode.stream.getAudioTracks()[0]);
videoRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm', mimeType: 'video/webm',
videoBitsPerSecond: 25000000 videoBitsPerSecond: 25000000
}); });

File diff suppressed because it is too large Load Diff