wasmcart 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +410 -0
- package/SPEC.md +477 -0
- package/bin/wasmcart-pack.js +257 -0
- package/docs/bind_framebuffer.md +275 -0
- package/docs/fetch.md +105 -0
- package/docs/gl-surface.md +111 -0
- package/docs/input.md +102 -0
- package/docs/networking.md +78 -0
- package/docs/porting.md +88 -0
- package/include/wc_cart.h +144 -0
- package/include/wc_fb.h +275 -0
- package/include/wc_gl.h +224 -0
- package/include/wc_gl_blit.h +129 -0
- package/include/wc_mat4.h +210 -0
- package/include/wc_math.h +116 -0
- package/include/wc_pcm_mixer.h +487 -0
- package/include/wc_vec3.h +80 -0
- package/index.js +3 -0
- package/package.json +55 -0
- package/src/CartHost.js +1713 -0
- package/src/CartHostWeb.js +1381 -0
- package/src/abi.js +94 -0
- package/src/cartWorker.js +201 -0
- package/src/cartWorkerWeb.js +170 -0
- package/src/webgl_imports.js +1483 -0
- package/web.js +3 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* wc_pcm_mixer.h - Multi-channel PCM audio mixer for wasmcart carts
|
|
3
|
+
*
|
|
4
|
+
* Single-header library providing a simple multi-channel audio mixer that
|
|
5
|
+
* writes stereo output to the wasmcart ring buffer. Supports both S16 and
|
|
6
|
+
* Float32 output formats. Includes a WAV file parser.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* In exactly ONE .c/.cpp file:
|
|
10
|
+
* #define WC_PCM_MIXER_IMPLEMENTATION
|
|
11
|
+
* #include "wc_pcm_mixer.h"
|
|
12
|
+
*
|
|
13
|
+
* In all other files that need the API:
|
|
14
|
+
* #include "wc_pcm_mixer.h"
|
|
15
|
+
*
|
|
16
|
+
* EXAMPLE:
|
|
17
|
+
* // In wc_init():
|
|
18
|
+
* wc_mixer_init();
|
|
19
|
+
*
|
|
20
|
+
* // Load sounds from .wasc assets:
|
|
21
|
+
* unsigned char buf[512000];
|
|
22
|
+
* int len = wc_load_asset("sfx/boom.wav", 12, buf, sizeof(buf));
|
|
23
|
+
* int snd_boom = wc_mixer_load_wav(buf, len);
|
|
24
|
+
*
|
|
25
|
+
* // Play a sound:
|
|
26
|
+
* wc_mixer_play(snd_boom, 1.0f, 0); // volume 1.0, no loop
|
|
27
|
+
*
|
|
28
|
+
* // In wc_render():
|
|
29
|
+
* int frames = time_based_frame_count(); // see below
|
|
30
|
+
* wc_mixer_mix(ring_buffer, ring_cap, &write_cursor, frames);
|
|
31
|
+
*
|
|
32
|
+
* IMPORTANT: Framerate and audio timing
|
|
33
|
+
*
|
|
34
|
+
* The `frames` parameter to wc_mixer_mix() controls how many audio
|
|
35
|
+
* samples are written to the ring buffer each call. Getting this wrong
|
|
36
|
+
* causes choppy or stuttering audio.
|
|
37
|
+
*
|
|
38
|
+
* BAD - fixed count assumes constant 60fps:
|
|
39
|
+
* wc_mixer_mix(..., host_rate / 60); // 800 at 48kHz
|
|
40
|
+
* If a frame takes 33ms (loading, state transitions, GC), only 16.7ms
|
|
41
|
+
* of audio is written, leaving a gap. If two frames run in 8ms each,
|
|
42
|
+
* audio is written faster than real time, causing buffer overflow.
|
|
43
|
+
* This is especially noticeable in menus where frame timing is
|
|
44
|
+
* inconsistent (asset loading, screen transitions).
|
|
45
|
+
*
|
|
46
|
+
* GOOD - time-based count adapts to actual frame timing:
|
|
47
|
+
* static uint32_t last_ms = 0;
|
|
48
|
+
* uint32_t now_ms = <your tick counter>;
|
|
49
|
+
* uint32_t delta = now_ms - last_ms;
|
|
50
|
+
* if (delta > 100) delta = 100; // cap to avoid huge decode
|
|
51
|
+
* last_ms = now_ms;
|
|
52
|
+
* int frames = (int)((uint64_t)host_rate * delta / 1000);
|
|
53
|
+
* wc_mixer_mix(ring_buffer, ring_cap, &write_cursor, frames);
|
|
54
|
+
*
|
|
55
|
+
* This produces exactly the right number of samples regardless of
|
|
56
|
+
* whether the frame took 8ms or 50ms, so audio stays smooth even
|
|
57
|
+
* when the game hitches.
|
|
58
|
+
*
|
|
59
|
+
* CONFIGURATION (define before including):
|
|
60
|
+
* WC_MIXER_MAX_SOUNDS - max loaded sounds (default 32)
|
|
61
|
+
* WC_MIXER_MAX_CHANNELS - max simultaneous voices (default 16)
|
|
62
|
+
* WC_MIXER_RATE - output sample rate (default 48000)
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
#ifndef WC_PCM_MIXER_H
|
|
66
|
+
#define WC_PCM_MIXER_H
|
|
67
|
+
|
|
68
|
+
#include <stdint.h>
|
|
69
|
+
|
|
70
|
+
#ifndef WC_MIXER_MAX_SOUNDS
|
|
71
|
+
#define WC_MIXER_MAX_SOUNDS 32
|
|
72
|
+
#endif
|
|
73
|
+
|
|
74
|
+
#ifndef WC_MIXER_MAX_CHANNELS
|
|
75
|
+
#define WC_MIXER_MAX_CHANNELS 16
|
|
76
|
+
#endif
|
|
77
|
+
|
|
78
|
+
#ifndef WC_MIXER_RATE
|
|
79
|
+
#define WC_MIXER_RATE 48000
|
|
80
|
+
#endif
|
|
81
|
+
|
|
82
|
+
/* ── Sound slot (loaded PCM data) ─────────────────────────────────── */
|
|
83
|
+
|
|
84
|
+
typedef struct {
|
|
85
|
+
int16_t *samples; /* PCM data (interleaved if stereo) */
|
|
86
|
+
int length; /* length in frames (per channel) */
|
|
87
|
+
int channels; /* 1 = mono, 2 = stereo */
|
|
88
|
+
int sample_rate; /* original sample rate */
|
|
89
|
+
int active; /* 1 if slot is loaded */
|
|
90
|
+
} wc_sound_t;
|
|
91
|
+
|
|
92
|
+
/* ── Playback channel ─────────────────────────────────────────────── */
|
|
93
|
+
|
|
94
|
+
typedef struct {
|
|
95
|
+
wc_sound_t *sound; /* pointer to sound slot */
|
|
96
|
+
uint32_t pos_frac; /* playback position (16.16 fixed-point) */
|
|
97
|
+
uint32_t step_frac; /* step per output sample (16.16 fixed-point) */
|
|
98
|
+
float volume; /* 0.0 - 1.0 */
|
|
99
|
+
float pan; /* -1.0 (left) to 1.0 (right), 0.0 = center */
|
|
100
|
+
int loop; /* 1 = loop playback */
|
|
101
|
+
int active; /* 1 = channel is playing */
|
|
102
|
+
} wc_channel_t;
|
|
103
|
+
|
|
104
|
+
/* ── API ──────────────────────────────────────────────────────────── */
|
|
105
|
+
|
|
106
|
+
/* Initialize the mixer. Call once. */
|
|
107
|
+
void wc_mixer_init(void);
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* Parse a WAV file buffer and load it into a sound slot.
|
|
111
|
+
* Returns the sound slot index (0..MAX_SOUNDS-1), or -1 on error.
|
|
112
|
+
* Supports 8-bit unsigned and 16-bit signed PCM, mono or stereo.
|
|
113
|
+
*/
|
|
114
|
+
int wc_mixer_load_wav(const unsigned char *data, int size);
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
* Load raw S16 PCM data directly (no WAV parsing).
|
|
118
|
+
* Returns sound slot index or -1.
|
|
119
|
+
*/
|
|
120
|
+
int wc_mixer_load_raw(const int16_t *pcm, int length_frames,
|
|
121
|
+
int channels, int sample_rate);
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
* Play a loaded sound. Returns the channel index or -1 if all busy.
|
|
125
|
+
* sound_id - slot index from wc_mixer_load_wav/load_raw
|
|
126
|
+
* volume - 0.0 to 1.0
|
|
127
|
+
* loop - 1 to loop, 0 for one-shot
|
|
128
|
+
*/
|
|
129
|
+
int wc_mixer_play(int sound_id, float volume, int loop);
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* Play with stereo panning.
|
|
133
|
+
* pan - -1.0 (full left) to 1.0 (full right), 0.0 = center
|
|
134
|
+
*/
|
|
135
|
+
int wc_mixer_play_pan(int sound_id, float volume, float pan, int loop);
|
|
136
|
+
|
|
137
|
+
/* Stop a specific channel. */
|
|
138
|
+
void wc_mixer_stop(int channel);
|
|
139
|
+
|
|
140
|
+
/* Stop all channels. */
|
|
141
|
+
void wc_mixer_stop_all(void);
|
|
142
|
+
|
|
143
|
+
/* Check if a channel is still playing. */
|
|
144
|
+
int wc_mixer_is_playing(int channel);
|
|
145
|
+
|
|
146
|
+
/* Set volume on an active channel. */
|
|
147
|
+
void wc_mixer_set_volume(int channel, float volume);
|
|
148
|
+
|
|
149
|
+
/*
|
|
150
|
+
* Mix audio into the wasmcart ring buffer (S16 output).
|
|
151
|
+
* Call this every frame (in wc_render or wc_audio_buf).
|
|
152
|
+
*
|
|
153
|
+
* ring - pointer to S16 stereo ring buffer
|
|
154
|
+
* cap - ring buffer capacity in stereo frames
|
|
155
|
+
* write_cur - pointer to write cursor (updated by this function)
|
|
156
|
+
* frames - number of stereo frames to produce.
|
|
157
|
+
* Use time-based calculation (see header docs), NOT a
|
|
158
|
+
* fixed value, to avoid choppy audio on variable framerates.
|
|
159
|
+
*/
|
|
160
|
+
void wc_mixer_mix(int16_t *ring, uint32_t cap, uint32_t *write_cur, int frames);
|
|
161
|
+
|
|
162
|
+
/*
|
|
163
|
+
* Mix audio into a Float32 wasmcart ring buffer.
|
|
164
|
+
* Same as wc_mixer_mix but writes normalized floats [-1.0, 1.0].
|
|
165
|
+
* Use this when the cart sets WC_FLAG_AUDIO_F32.
|
|
166
|
+
*/
|
|
167
|
+
void wc_mixer_mix_f32(float *ring, uint32_t cap, uint32_t *write_cur, int frames);
|
|
168
|
+
|
|
169
|
+
/* Free a loaded sound's memory. */
|
|
170
|
+
void wc_mixer_unload(int sound_id);
|
|
171
|
+
|
|
172
|
+
/* ── Direct access to state (for advanced use) ────────────────────── */
|
|
173
|
+
|
|
174
|
+
extern wc_sound_t wc_mixer_sounds[WC_MIXER_MAX_SOUNDS];
|
|
175
|
+
extern wc_channel_t wc_mixer_channels[WC_MIXER_MAX_CHANNELS];
|
|
176
|
+
|
|
177
|
+
#endif /* WC_PCM_MIXER_H */
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
181
|
+
* IMPLEMENTATION
|
|
182
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
183
|
+
|
|
184
|
+
#ifdef WC_PCM_MIXER_IMPLEMENTATION
|
|
185
|
+
|
|
186
|
+
#include <string.h>
|
|
187
|
+
#include <stdlib.h>
|
|
188
|
+
|
|
189
|
+
wc_sound_t wc_mixer_sounds[WC_MIXER_MAX_SOUNDS];
|
|
190
|
+
wc_channel_t wc_mixer_channels[WC_MIXER_MAX_CHANNELS];
|
|
191
|
+
|
|
192
|
+
void wc_mixer_init(void) {
|
|
193
|
+
memset(wc_mixer_sounds, 0, sizeof(wc_mixer_sounds));
|
|
194
|
+
memset(wc_mixer_channels, 0, sizeof(wc_mixer_channels));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* ── WAV parser ───────────────────────────────────────────────────── */
|
|
198
|
+
|
|
199
|
+
int wc_mixer_load_wav(const unsigned char *data, int size) {
|
|
200
|
+
if (size < 44) return -1;
|
|
201
|
+
/* Check RIFF/WAVE header */
|
|
202
|
+
if (data[0]!='R' || data[1]!='I' || data[2]!='F' || data[3]!='F') return -1;
|
|
203
|
+
if (data[8]!='W' || data[9]!='A' || data[10]!='V' || data[11]!='E') return -1;
|
|
204
|
+
|
|
205
|
+
int pos = 12;
|
|
206
|
+
int fmt_found = 0;
|
|
207
|
+
int num_channels = 1, sample_rate = 44100, bits_per_sample = 16;
|
|
208
|
+
|
|
209
|
+
while (pos + 8 <= size) {
|
|
210
|
+
int chunk_size = data[pos+4] | (data[pos+5]<<8) | (data[pos+6]<<16) | (data[pos+7]<<24);
|
|
211
|
+
|
|
212
|
+
if (data[pos]=='f' && data[pos+1]=='m' && data[pos+2]=='t' && data[pos+3]==' ') {
|
|
213
|
+
if (pos + 8 + 16 > size) return -1;
|
|
214
|
+
num_channels = data[pos+10] | (data[pos+11]<<8);
|
|
215
|
+
sample_rate = data[pos+12] | (data[pos+13]<<8) | (data[pos+14]<<16) | (data[pos+15]<<24);
|
|
216
|
+
bits_per_sample = data[pos+22] | (data[pos+23]<<8);
|
|
217
|
+
fmt_found = 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (data[pos]=='d' && data[pos+1]=='a' && data[pos+2]=='t' && data[pos+3]=='a') {
|
|
221
|
+
if (!fmt_found) return -1;
|
|
222
|
+
int data_start = pos + 8;
|
|
223
|
+
int data_size = chunk_size;
|
|
224
|
+
if (data_start + data_size > size) data_size = size - data_start;
|
|
225
|
+
|
|
226
|
+
int bytes_per_sample = bits_per_sample / 8;
|
|
227
|
+
int total_samples = data_size / bytes_per_sample;
|
|
228
|
+
int frames = total_samples / num_channels;
|
|
229
|
+
|
|
230
|
+
/* Find free slot */
|
|
231
|
+
int slot = -1;
|
|
232
|
+
for (int i = 0; i < WC_MIXER_MAX_SOUNDS; i++) {
|
|
233
|
+
if (!wc_mixer_sounds[i].active) { slot = i; break; }
|
|
234
|
+
}
|
|
235
|
+
if (slot < 0) return -1;
|
|
236
|
+
|
|
237
|
+
/* Allocate and convert to S16 */
|
|
238
|
+
int16_t *pcm = (int16_t*)malloc(total_samples * sizeof(int16_t));
|
|
239
|
+
if (!pcm) return -1;
|
|
240
|
+
|
|
241
|
+
if (bits_per_sample == 16) {
|
|
242
|
+
memcpy(pcm, data + data_start, total_samples * 2);
|
|
243
|
+
} else if (bits_per_sample == 8) {
|
|
244
|
+
for (int i = 0; i < total_samples; i++)
|
|
245
|
+
pcm[i] = ((int16_t)data[data_start + i] - 128) * 256;
|
|
246
|
+
} else {
|
|
247
|
+
free(pcm);
|
|
248
|
+
return -1;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
wc_mixer_sounds[slot].samples = pcm;
|
|
252
|
+
wc_mixer_sounds[slot].length = frames;
|
|
253
|
+
wc_mixer_sounds[slot].channels = num_channels;
|
|
254
|
+
wc_mixer_sounds[slot].sample_rate = sample_rate;
|
|
255
|
+
wc_mixer_sounds[slot].active = 1;
|
|
256
|
+
return slot;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
pos += 8 + chunk_size;
|
|
260
|
+
if (chunk_size & 1) pos++; /* word alignment */
|
|
261
|
+
}
|
|
262
|
+
return -1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
int wc_mixer_load_raw(const int16_t *pcm, int length_frames,
|
|
266
|
+
int channels, int sample_rate) {
|
|
267
|
+
int slot = -1;
|
|
268
|
+
for (int i = 0; i < WC_MIXER_MAX_SOUNDS; i++) {
|
|
269
|
+
if (!wc_mixer_sounds[i].active) { slot = i; break; }
|
|
270
|
+
}
|
|
271
|
+
if (slot < 0) return -1;
|
|
272
|
+
|
|
273
|
+
int total = length_frames * channels;
|
|
274
|
+
int16_t *copy = (int16_t*)malloc(total * sizeof(int16_t));
|
|
275
|
+
if (!copy) return -1;
|
|
276
|
+
memcpy(copy, pcm, total * sizeof(int16_t));
|
|
277
|
+
|
|
278
|
+
wc_mixer_sounds[slot].samples = copy;
|
|
279
|
+
wc_mixer_sounds[slot].length = length_frames;
|
|
280
|
+
wc_mixer_sounds[slot].channels = channels;
|
|
281
|
+
wc_mixer_sounds[slot].sample_rate = sample_rate;
|
|
282
|
+
wc_mixer_sounds[slot].active = 1;
|
|
283
|
+
return slot;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* ── Playback control ─────────────────────────────────────────────── */
|
|
287
|
+
|
|
288
|
+
int wc_mixer_play(int sound_id, float volume, int loop) {
|
|
289
|
+
return wc_mixer_play_pan(sound_id, volume, 0.0f, loop);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
int wc_mixer_play_pan(int sound_id, float volume, float pan, int loop) {
|
|
293
|
+
if (sound_id < 0 || sound_id >= WC_MIXER_MAX_SOUNDS) return -1;
|
|
294
|
+
if (!wc_mixer_sounds[sound_id].active) return -1;
|
|
295
|
+
|
|
296
|
+
/* Find free channel */
|
|
297
|
+
int ch = -1;
|
|
298
|
+
for (int i = 0; i < WC_MIXER_MAX_CHANNELS; i++) {
|
|
299
|
+
if (!wc_mixer_channels[i].active) { ch = i; break; }
|
|
300
|
+
}
|
|
301
|
+
/* All busy: steal channel 0 */
|
|
302
|
+
if (ch < 0) ch = 0;
|
|
303
|
+
|
|
304
|
+
wc_sound_t *s = &wc_mixer_sounds[sound_id];
|
|
305
|
+
wc_mixer_channels[ch].sound = s;
|
|
306
|
+
wc_mixer_channels[ch].pos_frac = 0;
|
|
307
|
+
wc_mixer_channels[ch].step_frac = ((uint32_t)s->sample_rate << 16) / WC_MIXER_RATE;
|
|
308
|
+
wc_mixer_channels[ch].volume = volume;
|
|
309
|
+
wc_mixer_channels[ch].pan = pan;
|
|
310
|
+
wc_mixer_channels[ch].loop = loop;
|
|
311
|
+
wc_mixer_channels[ch].active = 1;
|
|
312
|
+
return ch;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
void wc_mixer_stop(int channel) {
|
|
316
|
+
if (channel >= 0 && channel < WC_MIXER_MAX_CHANNELS)
|
|
317
|
+
wc_mixer_channels[channel].active = 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
void wc_mixer_stop_all(void) {
|
|
321
|
+
for (int i = 0; i < WC_MIXER_MAX_CHANNELS; i++)
|
|
322
|
+
wc_mixer_channels[i].active = 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
int wc_mixer_is_playing(int channel) {
|
|
326
|
+
if (channel < 0 || channel >= WC_MIXER_MAX_CHANNELS) return 0;
|
|
327
|
+
return wc_mixer_channels[channel].active;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
void wc_mixer_set_volume(int channel, float volume) {
|
|
331
|
+
if (channel >= 0 && channel < WC_MIXER_MAX_CHANNELS)
|
|
332
|
+
wc_mixer_channels[channel].volume = volume;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ── Mixer core ───────────────────────────────────────────────────── */
|
|
336
|
+
|
|
337
|
+
void wc_mixer_mix(int16_t *ring, uint32_t cap, uint32_t *write_cur, int frames) {
|
|
338
|
+
if (!ring || cap == 0 || frames <= 0) return;
|
|
339
|
+
|
|
340
|
+
uint32_t wr = *write_cur;
|
|
341
|
+
|
|
342
|
+
for (int f = 0; f < frames; f++) {
|
|
343
|
+
int32_t mix_left = 0, mix_right = 0;
|
|
344
|
+
|
|
345
|
+
for (int ch = 0; ch < WC_MIXER_MAX_CHANNELS; ch++) {
|
|
346
|
+
wc_channel_t *c = &wc_mixer_channels[ch];
|
|
347
|
+
if (!c->active || !c->sound) continue;
|
|
348
|
+
|
|
349
|
+
wc_sound_t *s = c->sound;
|
|
350
|
+
uint32_t pos = c->pos_frac >> 16;
|
|
351
|
+
|
|
352
|
+
if ((int)pos >= s->length) {
|
|
353
|
+
if (c->loop) {
|
|
354
|
+
c->pos_frac = 0;
|
|
355
|
+
pos = 0;
|
|
356
|
+
} else {
|
|
357
|
+
c->active = 0;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* Read sample */
|
|
363
|
+
int32_t left, right;
|
|
364
|
+
if (s->channels == 2) {
|
|
365
|
+
left = s->samples[pos * 2];
|
|
366
|
+
right = s->samples[pos * 2 + 1];
|
|
367
|
+
} else {
|
|
368
|
+
left = right = s->samples[pos];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* Apply volume */
|
|
372
|
+
float vol = c->volume;
|
|
373
|
+
left = (int32_t)(left * vol);
|
|
374
|
+
right = (int32_t)(right * vol);
|
|
375
|
+
|
|
376
|
+
/* Apply panning: pan -1..1, 0=center */
|
|
377
|
+
if (c->pan != 0.0f) {
|
|
378
|
+
float pan_l = (c->pan <= 0.0f) ? 1.0f : 1.0f - c->pan;
|
|
379
|
+
float pan_r = (c->pan >= 0.0f) ? 1.0f : 1.0f + c->pan;
|
|
380
|
+
left = (int32_t)(left * pan_l);
|
|
381
|
+
right = (int32_t)(right * pan_r);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
mix_left += left;
|
|
385
|
+
mix_right += right;
|
|
386
|
+
|
|
387
|
+
c->pos_frac += c->step_frac;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Clamp to S16 */
|
|
391
|
+
if (mix_left > 32767) mix_left = 32767;
|
|
392
|
+
if (mix_left < -32768) mix_left = -32768;
|
|
393
|
+
if (mix_right > 32767) mix_right = 32767;
|
|
394
|
+
if (mix_right < -32768) mix_right = -32768;
|
|
395
|
+
|
|
396
|
+
/* Write to ring buffer (stereo interleaved) */
|
|
397
|
+
uint32_t idx = (wr % cap) * 2;
|
|
398
|
+
ring[idx] = (int16_t)mix_left;
|
|
399
|
+
ring[idx + 1] = (int16_t)mix_right;
|
|
400
|
+
wr++;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
*write_cur = wr;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
void wc_mixer_mix_f32(float *ring, uint32_t cap, uint32_t *write_cur, int frames) {
|
|
407
|
+
if (!ring || cap == 0 || frames <= 0) return;
|
|
408
|
+
|
|
409
|
+
uint32_t wr = *write_cur;
|
|
410
|
+
|
|
411
|
+
for (int f = 0; f < frames; f++) {
|
|
412
|
+
float mix_left = 0.0f, mix_right = 0.0f;
|
|
413
|
+
|
|
414
|
+
for (int ch = 0; ch < WC_MIXER_MAX_CHANNELS; ch++) {
|
|
415
|
+
wc_channel_t *c = &wc_mixer_channels[ch];
|
|
416
|
+
if (!c->active || !c->sound) continue;
|
|
417
|
+
|
|
418
|
+
wc_sound_t *s = c->sound;
|
|
419
|
+
uint32_t pos = c->pos_frac >> 16;
|
|
420
|
+
|
|
421
|
+
if ((int)pos >= s->length) {
|
|
422
|
+
if (c->loop) {
|
|
423
|
+
c->pos_frac = 0;
|
|
424
|
+
pos = 0;
|
|
425
|
+
} else {
|
|
426
|
+
c->active = 0;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* Read sample and normalize to [-1.0, 1.0] */
|
|
432
|
+
float left, right;
|
|
433
|
+
if (s->channels == 2) {
|
|
434
|
+
left = s->samples[pos * 2] * (1.0f / 32768.0f);
|
|
435
|
+
right = s->samples[pos * 2 + 1] * (1.0f / 32768.0f);
|
|
436
|
+
} else {
|
|
437
|
+
left = right = s->samples[pos] * (1.0f / 32768.0f);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* Apply volume */
|
|
441
|
+
left *= c->volume;
|
|
442
|
+
right *= c->volume;
|
|
443
|
+
|
|
444
|
+
/* Apply panning */
|
|
445
|
+
if (c->pan != 0.0f) {
|
|
446
|
+
float pan_l = (c->pan <= 0.0f) ? 1.0f : 1.0f - c->pan;
|
|
447
|
+
float pan_r = (c->pan >= 0.0f) ? 1.0f : 1.0f + c->pan;
|
|
448
|
+
left *= pan_l;
|
|
449
|
+
right *= pan_r;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
mix_left += left;
|
|
453
|
+
mix_right += right;
|
|
454
|
+
|
|
455
|
+
c->pos_frac += c->step_frac;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* Clamp to [-1.0, 1.0] */
|
|
459
|
+
if (mix_left > 1.0f) mix_left = 1.0f;
|
|
460
|
+
if (mix_left < -1.0f) mix_left = -1.0f;
|
|
461
|
+
if (mix_right > 1.0f) mix_right = 1.0f;
|
|
462
|
+
if (mix_right < -1.0f) mix_right = -1.0f;
|
|
463
|
+
|
|
464
|
+
/* Write to ring buffer (stereo interleaved) */
|
|
465
|
+
uint32_t idx = (wr % cap) * 2;
|
|
466
|
+
ring[idx] = mix_left;
|
|
467
|
+
ring[idx + 1] = mix_right;
|
|
468
|
+
wr++;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
*write_cur = wr;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
void wc_mixer_unload(int sound_id) {
|
|
475
|
+
if (sound_id < 0 || sound_id >= WC_MIXER_MAX_SOUNDS) return;
|
|
476
|
+
/* Stop any channels using this sound */
|
|
477
|
+
for (int i = 0; i < WC_MIXER_MAX_CHANNELS; i++) {
|
|
478
|
+
if (wc_mixer_channels[i].sound == &wc_mixer_sounds[sound_id])
|
|
479
|
+
wc_mixer_channels[i].active = 0;
|
|
480
|
+
}
|
|
481
|
+
if (wc_mixer_sounds[sound_id].samples) {
|
|
482
|
+
free(wc_mixer_sounds[sound_id].samples);
|
|
483
|
+
}
|
|
484
|
+
memset(&wc_mixer_sounds[sound_id], 0, sizeof(wc_sound_t));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
#endif /* WC_PCM_MIXER_IMPLEMENTATION */
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* wc_vec3.h - 3D vector operations for wasmcart carts
|
|
3
|
+
*
|
|
4
|
+
* Single-header library providing vec3 struct and common operations.
|
|
5
|
+
* Uses wc_math.h for sqrt.
|
|
6
|
+
*
|
|
7
|
+
* USAGE:
|
|
8
|
+
* #include "wc_math.h"
|
|
9
|
+
* #include "wc_vec3.h"
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
#ifndef WC_VEC3_H
|
|
13
|
+
#define WC_VEC3_H
|
|
14
|
+
|
|
15
|
+
#ifndef WC_MATH_H
|
|
16
|
+
#error "Include wc_math.h before wc_vec3.h"
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
typedef struct { float x, y, z; } wc_vec3;
|
|
20
|
+
|
|
21
|
+
static inline wc_vec3 wc_v3(float x, float y, float z) {
|
|
22
|
+
wc_vec3 v = {x, y, z};
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static inline wc_vec3 wc_v3_add(wc_vec3 a, wc_vec3 b) {
|
|
27
|
+
return wc_v3(a.x + b.x, a.y + b.y, a.z + b.z);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static inline wc_vec3 wc_v3_sub(wc_vec3 a, wc_vec3 b) {
|
|
31
|
+
return wc_v3(a.x - b.x, a.y - b.y, a.z - b.z);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static inline wc_vec3 wc_v3_scale(wc_vec3 a, float s) {
|
|
35
|
+
return wc_v3(a.x * s, a.y * s, a.z * s);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static inline wc_vec3 wc_v3_neg(wc_vec3 a) {
|
|
39
|
+
return wc_v3(-a.x, -a.y, -a.z);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static inline float wc_v3_dot(wc_vec3 a, wc_vec3 b) {
|
|
43
|
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static inline wc_vec3 wc_v3_cross(wc_vec3 a, wc_vec3 b) {
|
|
47
|
+
return wc_v3(
|
|
48
|
+
a.y * b.z - a.z * b.y,
|
|
49
|
+
a.z * b.x - a.x * b.z,
|
|
50
|
+
a.x * b.y - a.y * b.x
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static inline float wc_v3_length(wc_vec3 a) {
|
|
55
|
+
return wc_sqrtf(a.x * a.x + a.y * a.y + a.z * a.z);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static inline float wc_v3_length_sq(wc_vec3 a) {
|
|
59
|
+
return a.x * a.x + a.y * a.y + a.z * a.z;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static inline wc_vec3 wc_v3_normalize(wc_vec3 a) {
|
|
63
|
+
float l = wc_v3_length(a);
|
|
64
|
+
if (l < 0.0001f) return wc_v3(0, 0, 0);
|
|
65
|
+
return wc_v3_scale(a, 1.0f / l);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static inline wc_vec3 wc_v3_lerp(wc_vec3 a, wc_vec3 b, float t) {
|
|
69
|
+
return wc_v3(
|
|
70
|
+
a.x + (b.x - a.x) * t,
|
|
71
|
+
a.y + (b.y - a.y) * t,
|
|
72
|
+
a.z + (b.z - a.z) * t
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static inline float wc_v3_distance(wc_vec3 a, wc_vec3 b) {
|
|
77
|
+
return wc_v3_length(wc_v3_sub(a, b));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#endif /* WC_VEC3_H */
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wasmcart",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "WASM cartridge host - load and run sandboxed .wasm and .wasc game carts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test 'test/**/*.test.js'",
|
|
9
|
+
"prepublishOnly": "npm test"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"browser": "./web.js",
|
|
14
|
+
"default": "./index.js"
|
|
15
|
+
},
|
|
16
|
+
"./web": "./web.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"web.js",
|
|
21
|
+
"src/",
|
|
22
|
+
"bin/",
|
|
23
|
+
"include/",
|
|
24
|
+
"docs/",
|
|
25
|
+
"SPEC.md",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"bin": {
|
|
30
|
+
"wasmcart-pack": "./bin/wasmcart-pack.js"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"wasm",
|
|
34
|
+
"webassembly",
|
|
35
|
+
"cartridge",
|
|
36
|
+
"game",
|
|
37
|
+
"sandbox",
|
|
38
|
+
"emulator",
|
|
39
|
+
"retro"
|
|
40
|
+
],
|
|
41
|
+
"author": "Luis Montes",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/monteslu/wasmcart.git"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.0.0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"fflate": "^0.8.3",
|
|
52
|
+
"yauzl": "^3.2.0",
|
|
53
|
+
"yazl": "^2.5.1"
|
|
54
|
+
}
|
|
55
|
+
}
|