Web: add panning, move mixing to wasm

This commit is contained in:
Guy Perfect 2024-11-02 11:14:24 -05:00
parent 8b9152dde9
commit b4b9131f39
5 changed files with 175 additions and 73 deletions

View File

@ -75,10 +75,6 @@ let Constants = {
BREAK_FRAME: 1, BREAK_FRAME: 1,
BREAK_POINT: 2, BREAK_POINT: 2,
// Extra properties
EXT_PIXELS : 0,
EXT_SAMPLES: 1,
// Anaglyph colors // Anaglyph colors
STEREO_CYAN : 0x00C6F0, STEREO_CYAN : 0x00C6F0,
STEREO_GREEN : 0x00B400, STEREO_GREEN : 0x00B400,

View File

@ -82,23 +82,20 @@ new class Core {
let sim = { let sim = {
canvas : null, canvas : null,
keys : Constants.VB.SGN, keys : Constants.VB.SGN,
pointer : sims[x] = this.CreateSim(), pointer : sims[x] = this.CreateSim()
volume : 1
}; };
this.sims.set(sim.pointer, sim); this.sims.set(sim.pointer, sim);
// Video // Video
this.SetAnaglyph(sim.pointer,
Constants.web.STEREO_RED, Constants.web.STEREO_CYAN);
sim.pixels = this.#noalloc( sim.pixels = this.#noalloc(
this.GetExt(sim.pointer, Constants.web.EXT_PIXELS), this.GetExtPixels(sim.pointer),
384*224*4, sim, "pixels", Uint8ClampedArray 384*224*4, sim, "pixels", Uint8ClampedArray
); );
sim.image = new ImageData(sim.pixels, 384, 224); sim.image = new ImageData(sim.pixels, 384, 224);
// Audio // Audio
sim.samples = this.#noalloc( sim.samples = this.#noalloc(
this.GetExt(sim.pointer, Constants.web.EXT_SAMPLES), this.GetExtSamples(sim.pointer),
41700 / 50 * 2, sim, "samples", Float32Array 41700 / 50 * 2, sim, "samples", Float32Array
); );
@ -296,6 +293,12 @@ new class Core {
this.dom.postMessage({ promised: true }); this.dom.postMessage({ promised: true });
} }
// Specify audio panning
setPanning(message) {
this.SetPanning(message.sim, message.panning);
this.dom.postMessage({ promised: true });
}
// Specify a new communication peer // Specify a new communication peer
setPeer(message) { setPeer(message) {
let orphaned = []; let orphaned = [];
@ -318,7 +321,7 @@ new class Core {
// Specify audio volume // Specify audio volume
setVolume(message) { setVolume(message) {
this.sims.get(message.sim).volume = message.volume; this.SetVolume(message.sim, message.volume);
this.dom.postMessage({ promised: true }); this.dom.postMessage({ promised: true });
} }
@ -408,19 +411,13 @@ new class Core {
// Mix and output audio samples // Mix and output audio samples
let buffer = this.audio.buffers.shift(); let buffer = this.audio.buffers.shift();
if (this.automatic.sims.length > 1) { this.Mix(
this.audio.samples.pointer,
this.automatic.pointers.pointer,
this.automatic.sims.length
);
for (let x = 0; x < buffer.length; x++) for (let x = 0; x < buffer.length; x++)
buffer[x] = 0; buffer[x] = this.audio.samples[x];
for (let sim of this.automatic.sims)
for (let x = 0; x < buffer.length; x++)
buffer[x] += sim.samples[x] * sim.volume;
for (let x = 0; x < buffer.length; x++)
buffer[x] = Math.min(Math.max(-1, buffer[x]), +1);
} else {
let sim = this.automatic.sims[0];
for (let x = 0; x < buffer.length; x++)
buffer[x] = sim.samples[x] * sim.volume;
}
this.audio.postMessage(buffer.buffer, [ buffer.buffer ]); this.audio.postMessage(buffer.buffer, [ buffer.buffer ]);
// Output staged images if there's one audio buffer to go // Output staged images if there's one audio buffer to go

View File

@ -285,6 +285,7 @@ class Sim extends HTMLElement {
#core; // Core proxy #core; // Core proxy
#emulating; // Current emulation status #emulating; // Current emulation status
#keys; // Controller state #keys; // Controller state
#panning; // Audio stereo balance
#peer; // Communication peer #peer; // Communication peer
#pointer; // Pointer in core memory #pointer; // Pointer in core memory
#volume; // Audio output volume #volume; // Audio output volume
@ -313,9 +314,10 @@ class Sim extends HTMLElement {
this.#core = core; this.#core = core;
this.#emulating = false; this.#emulating = false;
this.#keys = Constants.VB.SGN; this.#keys = Constants.VB.SGN;
this.#panning = 0.0;
this.#peer = null; this.#peer = null;
this.#pointer = pointer; this.#pointer = pointer;
this.#volume = 1; this.#volume = 1.0;
delete this.proxy; delete this.proxy;
// Create a <canvas> for the video image // Create a <canvas> for the video image
@ -360,6 +362,7 @@ class Sim extends HTMLElement {
get anaglyph() { return this.#anaglyph.slice(); } get anaglyph() { return this.#anaglyph.slice(); }
get core () { return this.#core.core ; } get core () { return this.#core.core ; }
get keys () { return this.#keys ; } get keys () { return this.#keys ; }
get panning () { return this.#panning ; }
get peer () { return this.#peer ; } get peer () { return this.#peer ; }
get volume () { return this.#volume ; } get volume () { return this.#volume ; }
@ -479,6 +482,27 @@ class Sim extends HTMLElement {
}); });
} }
// Specify audio panning
async setPanning(panning) {
// Error checking
if (!Number.isFinite(panning) ||panning < -1 || panning > +1) {
throw new RangeError(
"Panning must be a number from -1 to +1.");
}
// Configure instance fields
this.#panning = panning;
// Send the panning to the core
await this.#core.toCore({
command : "setPanning",
promised: true,
sim : this.#pointer,
panning : panning
});
}
// Specify a new communication peer // Specify a new communication peer
async setPeer(peer = null) { async setPeer(peer = null) {

View File

@ -12,9 +12,14 @@
#define BREAK_FRAME 1 #define BREAK_FRAME 1
#define BREAK_POINT 2 #define BREAK_POINT 2
// Extra properties // Anaglyph colors
#define EXT_PIXELS 0 #define STEREO_CYAN 0x00C6F0
#define EXT_SAMPLES 1 #define STEREO_GREEN 0x00B400
#define STEREO_MAGENTA 0xC800FF
#define STEREO_RED 0xFF0000
// Element counts
#define NUM_SAMPLES (41700 / 50 * 2)
@ -24,9 +29,11 @@
typedef struct { typedef struct {
int32_t breaks; int32_t breaks;
uint32_t left[256]; uint32_t left[256];
float panning;
uint8_t pixels[384 * 224 * 4]; uint8_t pixels[384 * 224 * 4];
uint32_t right[256]; uint32_t right[256];
float samples[41700 * 2]; float samples[NUM_SAMPLES];
float volume;
} Ext; } Ext;
@ -43,6 +50,36 @@ int wasmOnFrame(VB *sim) {
/////////////////////////////// Module Exports //////////////////////////////// /////////////////////////////// Module Exports ////////////////////////////////
// Specify anaglyph colors
EMSCRIPTEN_KEEPALIVE void SetAnaglyph(VB *sim, uint32_t left, uint32_t right) {
Ext *ext = (Ext *) vbGetUserData(sim);
// Erase all RGB values
for (int x = 0; x < 256; x++)
ext->left[x] = ext->right[x] = 0xFF000000;
// Process all RGB channels
for (int c = 0, shift = 16; c < 3; c++, shift -= 8) {
double max; // Magnitude of channel value
uint32_t *dest; // Lookup data
// Select the magnitude and lookup channel
dest = ext->left;
max = (left >> shift & 0xFF) / 255.0;
if (max == 0) {
dest = ext->right;
max = (right >> shift & 0xFF) / 255.0;
if (max == 0)
continue;
}
// Compute the resulting RGB values
for (int x = 0; x < 256; x++)
*dest++ |= (uint32_t) (x * max + 0.5) << (16 - shift);
}
}
// Instantiate a simulation // Instantiate a simulation
EMSCRIPTEN_KEEPALIVE void* CreateSim() { EMSCRIPTEN_KEEPALIVE void* CreateSim() {
size_t sizeOfSim = vbSizeOf(); size_t sizeOfSim = vbSizeOf();
@ -56,7 +93,10 @@ EMSCRIPTEN_KEEPALIVE void* CreateSim() {
// Configure extra // Configure extra
Ext *ext = (Ext *) (pointer + sizeOfSim); Ext *ext = (Ext *) (pointer + sizeOfSim);
ext->breaks = 0; ext->breaks = 0;
ext->panning = 0.0f;
ext->volume = 1.0f;
vbSetUserData(sim, ext); vbSetUserData(sim, ext);
SetAnaglyph(sim, STEREO_RED, STEREO_CYAN);
// Initialize pixels with opaque black // Initialize pixels with opaque black
for (unsigned x = 0; x < 384 * 224; x++) for (unsigned x = 0; x < 384 * 224; x++)
@ -99,6 +139,16 @@ EMSCRIPTEN_KEEPALIVE int Emulate(VB **sims, unsigned count, uint32_t *clocks) {
return vbEmulateEx(sims, count, clocks); return vbEmulateEx(sims, count, clocks);
} }
// Retrieve a sim's pixel pointer
EMSCRIPTEN_KEEPALIVE void* GetExtPixels(VB *sim) {
return ((Ext *) vbGetUserData(sim))->pixels;
}
// Retrieve a sim's sample pointer
EMSCRIPTEN_KEEPALIVE void* GetExtSamples(VB *sim) {
return ((Ext *) vbGetUserData(sim))->samples;
}
// Retrieve the break condition flags for a sim // Retrieve the break condition flags for a sim
EMSCRIPTEN_KEEPALIVE int32_t GetBreaks(VB *sim) { EMSCRIPTEN_KEEPALIVE int32_t GetBreaks(VB *sim) {
return ((Ext *) vbGetUserData(sim))->breaks; return ((Ext *) vbGetUserData(sim))->breaks;
@ -127,16 +177,6 @@ EMSCRIPTEN_KEEPALIVE void GetDasm(uint32_t *buffer, void *dasm, int count) {
} }
} }
// Retrieve an extra property
EMSCRIPTEN_KEEPALIVE void* GetExt(VB *sim, int id) {
Ext *ext = (Ext *) vbGetUserData(sim);
switch (id) {
case EXT_PIXELS : return ext->pixels;
case EXT_SAMPLES: return ext->samples;
}
return NULL;
}
// Retrieve anaglyph-tinted pixels from a sim // Retrieve anaglyph-tinted pixels from a sim
EMSCRIPTEN_KEEPALIVE void GetPixels(VB *sim) { EMSCRIPTEN_KEEPALIVE void GetPixels(VB *sim) {
Ext *ext = (Ext *) vbGetUserData(sim); Ext *ext = (Ext *) vbGetUserData(sim);
@ -146,6 +186,71 @@ EMSCRIPTEN_KEEPALIVE void GetPixels(VB *sim) {
*(uint32_t *) pixels = ext->left[pixels[0]] | ext->right[pixels[1]]; *(uint32_t *) pixels = ext->left[pixels[0]] | ext->right[pixels[1]];
} }
// Mix audio samples for output
EMSCRIPTEN_KEEPALIVE void Mix(float *buffer, VB **sims, int count) {
// Process all sims
for (int x = 0; x < count; x++) {
Ext *ext = (Ext *) vbGetUserData(sims[x]);
float *samples = ext->samples;
float v = ext->volume;
// First sim initializes buffer
if (x == 0) {
if (ext->panning < 0) {
float l = -ext->panning * v;
float r = (1.0f + ext->panning) * v;
for (unsigned y = 0; y < NUM_SAMPLES; y += 2) {
buffer[y ] = samples[y] * v + samples[y + 1] * l;
buffer[y + 1] = samples[y + 1] * r;
}
} else if (ext->panning > 0) {
float r = ext->panning * v;
float l = (1.0f - ext->panning) * v;
for (unsigned y = 0; y < NUM_SAMPLES; y += 2) {
buffer[y ] = samples[y] * l;
buffer[y + 1] = samples[y + 1] * v + samples[y] * r;
}
} else {
for (unsigned y = 0; y < NUM_SAMPLES; y++)
buffer[y] = samples[y] * v;
}
}
// Subsequent sims add to buffer
else {
if (ext->panning < 0) {
float l = -ext->panning * v;
float r = (1.0f + ext->panning) * v;
for (unsigned y = 0; y < NUM_SAMPLES; y += 2) {
buffer[y ] += samples[y] * v + samples[y + 1] * l;
buffer[y + 1] += samples[y + 1] * r;
}
} else if (ext->panning > 0) {
float r = ext->panning * v;
float l = (1.0f - ext->panning) * v;
for (unsigned y = 0; y < NUM_SAMPLES; y += 2) {
buffer[y ] += samples[y] * l;
buffer[y + 1] += samples[y + 1] * v + samples[y] * r;
}
} else {
for (unsigned y = 0; y < NUM_SAMPLES; y++)
buffer[y] += samples[y] * v;
}
}
}
// Clipping
for (unsigned y = 0; y < NUM_SAMPLES; y++) {
if (buffer[y] < -1.0f)
buffer[y] = -1.0f;
else if (buffer[y] > +1.0f)
buffer[y] = +1.0f;
}
}
// Determine the size in bytes of a pointer // Determine the size in bytes of a pointer
EMSCRIPTEN_KEEPALIVE int PointerSize() { EMSCRIPTEN_KEEPALIVE int PointerSize() {
return sizeof (void *); return sizeof (void *);
@ -156,32 +261,12 @@ EMSCRIPTEN_KEEPALIVE void* Realloc(void *data, size_t size) {
return realloc(data, size); return realloc(data, size);
} }
// Specify anaglyph colors // Specify audio panning
EMSCRIPTEN_KEEPALIVE void SetAnaglyph(VB *sim, uint32_t left, uint32_t right) { EMSCRIPTEN_KEEPALIVE void SetPanning(VB *sim, float panning) {
Ext *ext = (Ext *) vbGetUserData(sim); ((Ext *) vbGetUserData(sim))->panning = panning;
// Erase all RGB values
for (int x = 0; x < 256; x++)
ext->left[x] = ext->right[x] = 0xFF000000;
// Process all RGB channels
for (int c = 0, shift = 16; c < 3; c++, shift -= 8) {
double max; // Magnitude of channel value
uint32_t *dest; // Lookup data
// Select the magnitude and lookup channel
dest = ext->left;
max = (left >> shift & 0xFF) / 255.0;
if (max == 0) {
dest = ext->right;
max = (right >> shift & 0xFF) / 255.0;
if (max == 0)
continue;
}
// Compute the resulting RGB values
for (int x = 0; x < 256; x++)
*dest++ |= (uint32_t) (x * max + 0.5) << (16 - shift);
} }
// Specify audio volume
EMSCRIPTEN_KEEPALIVE void SetVolume(VB *sim, float volume) {
((Ext *) vbGetUserData(sim))->volume = volume;
} }