Web: add panning, move mixing to wasm
This commit is contained in:
parent
8b9152dde9
commit
b4b9131f39
|
@ -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,
|
||||||
|
|
35
web/Core.js
35
web/Core.js
|
@ -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
|
||||||
|
|
26
web/VB.js
26
web/VB.js
|
@ -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) {
|
||||||
|
|
||||||
|
|
165
web/wasm.c
165
web/wasm.c
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue