@raskell/wasm-ad 0.1.0 → 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/README.md +31 -6
- package/assembly/index.ts +222 -2
- package/bin/wasmad.mjs +43 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# WASM-AD for AssemblyScript
|
|
2
2
|
|
|
3
3
|
An AssemblyScript SDK for [WASM-AD](https://github.com/raskell-io/wasm-ad) carts: register
|
|
4
|
-
addresses, typed builders for backgrounds
|
|
5
|
-
`trace`, with no runtime allocation and no mutable globals.
|
|
4
|
+
addresses, typed builders for backgrounds (tiled and affine), sprites, affine matrices and
|
|
5
|
+
sound, input, save RAM and `trace`, with no runtime allocation and no mutable globals.
|
|
6
6
|
|
|
7
7
|
## The starter cart
|
|
8
8
|
|
|
@@ -12,8 +12,9 @@ npm run build # build/starter.wasm
|
|
|
12
12
|
node ../../tools/wasmad.mjs check build/starter.wasm
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
[`examples/starter.ts`](examples/starter.ts) is the same cart as every other SDK's starter
|
|
16
|
-
and
|
|
15
|
+
[`examples/starter.ts`](examples/starter.ts) is the same cart as every other SDK's starter: a
|
|
16
|
+
rotating affine checkerboard, a spinning star and a little tune on every sound channel, with
|
|
17
|
+
bit-identical frames and sound. Drop `build/starter.wasm` into the player.
|
|
17
18
|
|
|
18
19
|
## A new cart
|
|
19
20
|
|
|
@@ -72,7 +73,27 @@ export function start(): void {
|
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
`const` values are fine. Avoid `new`, string concatenation, arrays and anything else that
|
|
75
|
-
allocates; use `memory.data` for static buffers.
|
|
76
|
+
allocates; use `memory.data` for static buffers and tables, e.g.
|
|
77
|
+
`const NOTES = memory.data<u32>([262, 330, 392])` read with `load<u32>(NOTES + (i << 2))`.
|
|
78
|
+
|
|
79
|
+
## Sound
|
|
80
|
+
|
|
81
|
+
`sound.enable()` turns sound on with every channel on both sides. A channel plays when its
|
|
82
|
+
start bit is written; the host clears it after the frame (SPEC.md §14):
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
write16(reg.SOUND2CNT_L, sound.DUTY_50 | sound.envelope(12, 2, false));
|
|
86
|
+
write16(reg.SOUND2CNT_H, sound.START | sound.rate(440));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Sample channels play signed 8-bit samples you write to `sound.sampleRam()`:
|
|
90
|
+
`pcm.play(0, offset, length, 16000, 64)`.
|
|
91
|
+
|
|
92
|
+
## Affine backgrounds
|
|
93
|
+
|
|
94
|
+
In modes 1 and 2, BG2 (and BG3 in mode 2) is drawn through a matrix. Its tiles are 8bpp
|
|
95
|
+
(`bgTile8`) and its map is one byte per tile (`affineMap`). `setBgAffine(2, m, tx, ty, sx, sy)`
|
|
96
|
+
puts texture pixel (tx, ty) at screen pixel (sx, sy) and rotates and scales around it.
|
|
76
97
|
|
|
77
98
|
## API
|
|
78
99
|
|
|
@@ -83,11 +104,12 @@ All values are plain integers; builders take a value and return a new one.
|
|
|
83
104
|
| `read16`, `write16`, `read32`, `write32` | Raw register access, e.g. `write16(reg.DISPCNT, dispcnt.MODE0 \| dispcnt.BG0)` |
|
|
84
105
|
| `reg.*`, `reg.BGCNT(i)`, `reg.BGHOFS(i)`, `reg.BGVOFS(i)` | Register addresses by their SPEC.md names |
|
|
85
106
|
| `dispcnt`, `layer`, `blend`, `key` | Bit constants; `blend.cnt(effect, first, second)`, `blend.alpha(eva, evb)` |
|
|
86
|
-
| `bgcnt.priority/charBlock/screenBlock/size/bpp8/mosaic` | `BGxCNT` builders |
|
|
107
|
+
| `bgcnt.priority/charBlock/screenBlock/size/bpp8/mosaic/affineSize/wrap` | `BGxCNT` builders |
|
|
87
108
|
| `rgb(r, g, b)` | 8-bit channels to RGB555 |
|
|
88
109
|
| `setBgColor(i, c)`, `setObjColor(i, c)` | Palette RAM |
|
|
89
110
|
| `bgTile(block, i)`, `objTile(i)`, `fillTile(addr, byte)` | Addresses of 32-byte 4bpp tiles |
|
|
90
111
|
| `screenBlock(b)`, `setScreenEntry(b, i, e)`, `screenEntry.make/hflip/vflip` | Background maps |
|
|
112
|
+
| `bgTile8(block, i)`, `affineMap(b)`, `setBgAffine(bg, m, tx, ty, sx, sy)` | Affine backgrounds (modes 1 and 2) |
|
|
91
113
|
| `sprite.make(x, y, SpriteSize.S16x16, tile)` then `.bank/.priority/.hflip/.vflip/.semiTransparent/.window/.mosaic/.bpp8/.affine` | Sprites, packed in a `u64` |
|
|
92
114
|
| `oam.set(i, s)`, `oam.hide(i)`, `oam.hideAll()`, `oam.setAffine(slot, m)` | OAM |
|
|
93
115
|
| `affine.rotateScaleFixed(angle, sx, sy)`, `affine.IDENTITY`, `affine.make` | Affine matrices; angle in 1/256 turns, scale in 8.8 |
|
|
@@ -95,4 +117,7 @@ All values are plain integers; builders take a value and return a new one.
|
|
|
95
117
|
| `buttons.update()`, `buttons.held(k)`, `buttons.pressed(k)`, `keys()` | Input |
|
|
96
118
|
| `frame()`, `seed()` | Time and the only entropy source |
|
|
97
119
|
| `saveRam()`, `saveRestored()`, `requestSave()` | 32 KB of save RAM at 0x19000 |
|
|
120
|
+
| `sound.enable/disable`, `sound.rate(hz)`, `sound.sweep/envelope/length/noise`, `sound.START/LENGTH/DUTY_*/WAVE_*` | Sound channels 1–4, through `reg.SOUND*` |
|
|
121
|
+
| `sound.waveRam()`, `sound.sampleRam()`, `sound.status()` | Wave RAM, sample RAM, which channels are playing |
|
|
122
|
+
| `pcm.play(ch, start, len, rate, vol)`, `pcm.playLooped`, `pcm.stop`, `pcm.position`, `pcm.playing` | Sample channels A (0) and B (1) |
|
|
98
123
|
| `trace(msg)` | UTF-8 to the host's debug log (at most 1 KB) |
|
package/assembly/index.ts
CHANGED
|
@@ -27,13 +27,18 @@ export const VRAM_SIZE: usize = 0x18000;
|
|
|
27
27
|
export const OBJ_VRAM: usize = VRAM + 0x10000;
|
|
28
28
|
export const SAVE: usize = 0x19000;
|
|
29
29
|
export const SAVE_SIZE: usize = 0x8000;
|
|
30
|
+
/** Sample RAM: signed 8-bit samples for the sample channels (SPEC.md §14.6). */
|
|
31
|
+
export const SAMPLE_RAM: usize = 0x21000;
|
|
32
|
+
export const SAMPLE_RAM_SIZE: usize = 0x7000;
|
|
33
|
+
/** Wave RAM: two banks of 16 bytes (SPEC.md §14.5). */
|
|
34
|
+
export const WAVE_RAM: usize = 0x490;
|
|
30
35
|
/** First address the cart may use for its own data, stack and heap. */
|
|
31
36
|
export const CART: usize = 0x28000;
|
|
32
37
|
/** OAM attribute 0 that hides a sprite. */
|
|
33
38
|
export const OBJ_HIDDEN: u16 = 0x0200;
|
|
34
|
-
export const SPEC_VERSION: u16 =
|
|
39
|
+
export const SPEC_VERSION: u16 = 0x0002;
|
|
35
40
|
|
|
36
|
-
// ---- Registers (SPEC.md §5, §6)
|
|
41
|
+
// ---- Registers (SPEC.md §5, §6, §14) ----------------------------------------
|
|
37
42
|
|
|
38
43
|
/** Every register address, by its SPEC.md name. Use with `read16`/`write16`. */
|
|
39
44
|
export namespace reg {
|
|
@@ -56,6 +61,46 @@ export namespace reg {
|
|
|
56
61
|
export const SAVESTAT: usize = 0x70e;
|
|
57
62
|
export const SPEC: usize = 0x710;
|
|
58
63
|
|
|
64
|
+
export const BG2PA: usize = 0x420;
|
|
65
|
+
export const BG2PB: usize = 0x422;
|
|
66
|
+
export const BG2PC: usize = 0x424;
|
|
67
|
+
export const BG2PD: usize = 0x426;
|
|
68
|
+
export const BG2X: usize = 0x428; // u32
|
|
69
|
+
export const BG2Y: usize = 0x42c; // u32
|
|
70
|
+
export const BG3PA: usize = 0x430;
|
|
71
|
+
export const BG3PB: usize = 0x432;
|
|
72
|
+
export const BG3PC: usize = 0x434;
|
|
73
|
+
export const BG3PD: usize = 0x436;
|
|
74
|
+
export const BG3X: usize = 0x438; // u32
|
|
75
|
+
export const BG3Y: usize = 0x43c; // u32
|
|
76
|
+
|
|
77
|
+
export const SOUND1CNT_L: usize = 0x460;
|
|
78
|
+
export const SOUND1CNT_H: usize = 0x462;
|
|
79
|
+
export const SOUND1CNT_X: usize = 0x464;
|
|
80
|
+
export const SOUND2CNT_L: usize = 0x468;
|
|
81
|
+
export const SOUND2CNT_H: usize = 0x46c;
|
|
82
|
+
export const SOUND3CNT_L: usize = 0x470;
|
|
83
|
+
export const SOUND3CNT_H: usize = 0x472;
|
|
84
|
+
export const SOUND3CNT_X: usize = 0x474;
|
|
85
|
+
export const SOUND4CNT_L: usize = 0x478;
|
|
86
|
+
export const SOUND4CNT_H: usize = 0x47c;
|
|
87
|
+
export const SOUNDCNT_L: usize = 0x480;
|
|
88
|
+
export const SOUNDCNT_H: usize = 0x482;
|
|
89
|
+
export const SOUNDCNT_X: usize = 0x484;
|
|
90
|
+
|
|
91
|
+
export const PCMA_SAD: usize = 0x4b0;
|
|
92
|
+
export const PCMA_LEN: usize = 0x4b2;
|
|
93
|
+
export const PCMA_LOOP: usize = 0x4b4;
|
|
94
|
+
export const PCMA_RATE: usize = 0x4b6;
|
|
95
|
+
export const PCMA_CNT: usize = 0x4b8;
|
|
96
|
+
export const PCMA_POS: usize = 0x4ba;
|
|
97
|
+
export const PCMB_SAD: usize = 0x4c0;
|
|
98
|
+
export const PCMB_LEN: usize = 0x4c2;
|
|
99
|
+
export const PCMB_LOOP: usize = 0x4c4;
|
|
100
|
+
export const PCMB_RATE: usize = 0x4c6;
|
|
101
|
+
export const PCMB_CNT: usize = 0x4c8;
|
|
102
|
+
export const PCMB_POS: usize = 0x4ca;
|
|
103
|
+
|
|
59
104
|
/** `BGxCNT` for background 0–3. */
|
|
60
105
|
// @ts-ignore: decorator
|
|
61
106
|
@inline export function BGCNT(bg: i32): usize {
|
|
@@ -93,6 +138,10 @@ export namespace reg {
|
|
|
93
138
|
/** `DISPCNT` bits (SPEC.md §6.1). */
|
|
94
139
|
export namespace dispcnt {
|
|
95
140
|
export const MODE0: u16 = 0;
|
|
141
|
+
/** BG0 and BG1 tiled, BG2 affine. */
|
|
142
|
+
export const MODE1: u16 = 1;
|
|
143
|
+
/** BG2 and BG3 affine. */
|
|
144
|
+
export const MODE2: u16 = 2;
|
|
96
145
|
export const MODE3: u16 = 3;
|
|
97
146
|
export const MODE4: u16 = 4;
|
|
98
147
|
export const MODE5: u16 = 5;
|
|
@@ -165,6 +214,14 @@ export enum MapSize {
|
|
|
165
214
|
S64x64 = 3,
|
|
166
215
|
}
|
|
167
216
|
|
|
217
|
+
/** Affine background map sizes, in tiles (SPEC.md §8.4). */
|
|
218
|
+
export enum AffineSize {
|
|
219
|
+
S16x16 = 0,
|
|
220
|
+
S32x32 = 1,
|
|
221
|
+
S64x64 = 2,
|
|
222
|
+
S128x128 = 3,
|
|
223
|
+
}
|
|
224
|
+
|
|
168
225
|
/** Builders for a `BGxCNT` value (SPEC.md §6.2), e.g. `bgcnt.priority(bgcnt.screenBlock(0, 31), 1)`. */
|
|
169
226
|
export namespace bgcnt {
|
|
170
227
|
// @ts-ignore: decorator
|
|
@@ -191,6 +248,16 @@ export namespace bgcnt {
|
|
|
191
248
|
@inline export function mosaic(c: u16): u16 {
|
|
192
249
|
return c | 0x40;
|
|
193
250
|
}
|
|
251
|
+
/** Map size of an affine background. */
|
|
252
|
+
// @ts-ignore: decorator
|
|
253
|
+
@inline export function affineSize(c: u16, s: AffineSize): u16 {
|
|
254
|
+
return (c & 0x3fff) | (<u16>s << 14);
|
|
255
|
+
}
|
|
256
|
+
/** An affine background repeats instead of being transparent outside its map. */
|
|
257
|
+
// @ts-ignore: decorator
|
|
258
|
+
@inline export function wrap(c: u16): u16 {
|
|
259
|
+
return c | 0x2000;
|
|
260
|
+
}
|
|
194
261
|
}
|
|
195
262
|
|
|
196
263
|
/** Packs 8-bit channels into RGB555. */
|
|
@@ -215,6 +282,19 @@ export namespace bgcnt {
|
|
|
215
282
|
@inline export function bgTile(block: i32, index: i32): usize {
|
|
216
283
|
return VRAM + <usize>block * 0x4000 + (<usize>index << 5);
|
|
217
284
|
}
|
|
285
|
+
/** Address of 8bpp background tile `index` counted from character block `block` (0–3). Tiles are 64 bytes;
|
|
286
|
+
* affine backgrounds always use these. */
|
|
287
|
+
// @ts-ignore: decorator
|
|
288
|
+
@inline export function bgTile8(block: i32, index: i32): usize {
|
|
289
|
+
return VRAM + <usize>block * 0x4000 + (<usize>index << 6);
|
|
290
|
+
}
|
|
291
|
+
/** Address of an affine background's map at screen block `block` (0–31): one tile index per byte,
|
|
292
|
+
* row-major (SPEC.md §8.4). */
|
|
293
|
+
// @ts-ignore: decorator
|
|
294
|
+
@inline export function affineMap(block: i32): usize {
|
|
295
|
+
return VRAM + <usize>block * 0x800;
|
|
296
|
+
}
|
|
297
|
+
|
|
218
298
|
/** Address of 4bpp sprite tile `index` (0–1023). */
|
|
219
299
|
// @ts-ignore: decorator
|
|
220
300
|
@inline export function objTile(index: i32): usize {
|
|
@@ -412,6 +492,146 @@ export namespace oam {
|
|
|
412
492
|
}
|
|
413
493
|
}
|
|
414
494
|
|
|
495
|
+
/** Sets affine background `bg` (2 or 3) to matrix `m`, placed so that texture
|
|
496
|
+
* pixel (texX, texY) appears at screen pixel (scrX, scrY). Rotation and
|
|
497
|
+
* scaling then happen around that point. */
|
|
498
|
+
export function setBgAffine(bg: i32, m: AffineMatrix, texX: i32, texY: i32, scrX: i32, scrY: i32): void {
|
|
499
|
+
const at = reg.BG2PA + <usize>(bg - 2) * 0x10;
|
|
500
|
+
const pa = <i32>affine.pa(m), pb = <i32>affine.pb(m), pc = <i32>affine.pc(m), pd = <i32>affine.pd(m);
|
|
501
|
+
store<u16>(at, <u16>pa);
|
|
502
|
+
store<u16>(at + 2, <u16>pb);
|
|
503
|
+
store<u16>(at + 4, <u16>pc);
|
|
504
|
+
store<u16>(at + 6, <u16>pd);
|
|
505
|
+
store<u32>(at + 8, <u32>((texX << 8) - pa * scrX - pb * scrY));
|
|
506
|
+
store<u32>(at + 12, <u32>((texY << 8) - pc * scrX - pd * scrY));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ---- Sound (SPEC.md §14) -------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
/** Sound: four GBA-style channels written through `reg.SOUND*`, with
|
|
512
|
+
* builders for their fields, and setup:
|
|
513
|
+
*
|
|
514
|
+
* sound.enable();
|
|
515
|
+
* write16(reg.SOUND2CNT_L, sound.DUTY_50 | sound.envelope(12, 2, false));
|
|
516
|
+
* write16(reg.SOUND2CNT_H, sound.START | sound.rate(440));
|
|
517
|
+
*/
|
|
518
|
+
export namespace sound {
|
|
519
|
+
/** Starts (or restarts) a channel; the host clears it. */
|
|
520
|
+
export const START: u16 = 1 << 15;
|
|
521
|
+
/** Stops the channel when its length runs out. */
|
|
522
|
+
export const LENGTH: u16 = 1 << 14;
|
|
523
|
+
|
|
524
|
+
export const DUTY_12: u16 = 0 << 6;
|
|
525
|
+
export const DUTY_25: u16 = 1 << 6;
|
|
526
|
+
export const DUTY_50: u16 = 2 << 6;
|
|
527
|
+
export const DUTY_75: u16 = 3 << 6;
|
|
528
|
+
|
|
529
|
+
/** `SOUND3CNT_L`: the wave channel is on. */
|
|
530
|
+
export const WAVE_ON: u16 = 1 << 7;
|
|
531
|
+
/** `SOUND3CNT_L`: play bank 1 (or start with it, with `WAVE_64`). */
|
|
532
|
+
export const WAVE_BANK1: u16 = 1 << 6;
|
|
533
|
+
/** `SOUND3CNT_L`: play both banks as one 64-sample wave. */
|
|
534
|
+
export const WAVE_64: u16 = 1 << 5;
|
|
535
|
+
/** `SOUND3CNT_H` volumes. */
|
|
536
|
+
export const WAVE_100: u16 = 1 << 13;
|
|
537
|
+
export const WAVE_50: u16 = 2 << 13;
|
|
538
|
+
export const WAVE_25: u16 = 3 << 13;
|
|
539
|
+
export const WAVE_75: u16 = 1 << 15;
|
|
540
|
+
|
|
541
|
+
/** Turns sound on with every channel on both sides at full volume. */
|
|
542
|
+
export function enable(): void {
|
|
543
|
+
store<u16>(reg.SOUNDCNT_X, 0x80);
|
|
544
|
+
store<u16>(reg.SOUNDCNT_L, 0xff77);
|
|
545
|
+
store<u16>(reg.SOUNDCNT_H, 0x330e);
|
|
546
|
+
}
|
|
547
|
+
/** Turns sound off; every channel stops. */
|
|
548
|
+
export function disable(): void {
|
|
549
|
+
store<u16>(reg.SOUNDCNT_X, 0);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** The rate field for a square channel playing `hz` (64 Hz and up); the
|
|
553
|
+
* wave channel plays an octave lower at the same rate. */
|
|
554
|
+
// @ts-ignore: decorator
|
|
555
|
+
@inline export function rate(hz: u32): u16 {
|
|
556
|
+
return hz < 64 ? 0 : <u16>(2048 - 131072 / hz);
|
|
557
|
+
}
|
|
558
|
+
/** `SOUND1CNT_L`: every `time` 128ths of a second (1–7, 0 = off), the
|
|
559
|
+
* rate changes by rate >> `shift` (1–7), down if `decrease`. */
|
|
560
|
+
// @ts-ignore: decorator
|
|
561
|
+
@inline export function sweep(time: u16, shift: u16, decrease: bool): u16 {
|
|
562
|
+
return ((time & 7) << 4) | (shift & 7) | (decrease ? 8 : 0);
|
|
563
|
+
}
|
|
564
|
+
/** The envelope fields: start at `volume` (0–15), stepping by one every
|
|
565
|
+
* `step` 64ths of a second (0 = constant), up if `increase`. */
|
|
566
|
+
// @ts-ignore: decorator
|
|
567
|
+
@inline export function envelope(volume: u16, step: u16, increase: bool): u16 {
|
|
568
|
+
return ((volume & 15) << 12) | ((step & 7) << 8) | (increase ? 1 << 11 : 0);
|
|
569
|
+
}
|
|
570
|
+
/** The length field of channels 1, 2 and 4 for `n` 256ths of a second
|
|
571
|
+
* (1–64); it only counts with `LENGTH` set. Channel 3 takes 256 − n. */
|
|
572
|
+
// @ts-ignore: decorator
|
|
573
|
+
@inline export function length(n: u16): u16 {
|
|
574
|
+
return (64 - n) & 63;
|
|
575
|
+
}
|
|
576
|
+
/** `SOUND4CNT_H`: noise from a divider (0–7) and shift (0–13), with the
|
|
577
|
+
* 7-bit register if `short` (more tonal). */
|
|
578
|
+
// @ts-ignore: decorator
|
|
579
|
+
@inline export function noise(divider: u16, shift: u16, short: bool): u16 {
|
|
580
|
+
return (divider & 7) | ((shift & 15) << 4) | (short ? 8 : 0);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Address of wave RAM: two banks of 16 bytes, each 32 4-bit samples, high nibble first. */
|
|
584
|
+
// @ts-ignore: decorator
|
|
585
|
+
@inline export function waveRam(): usize {
|
|
586
|
+
return WAVE_RAM;
|
|
587
|
+
}
|
|
588
|
+
/** Address of the 28 KB of sample RAM (signed 8-bit) that the sample channels play from. */
|
|
589
|
+
// @ts-ignore: decorator
|
|
590
|
+
@inline export function sampleRam(): usize {
|
|
591
|
+
return SAMPLE_RAM;
|
|
592
|
+
}
|
|
593
|
+
/** Which channels are playing: bits 0–3 channels 1–4, bits 4 and 5 sample
|
|
594
|
+
* channels A and B, as of the end of the last frame. */
|
|
595
|
+
// @ts-ignore: decorator
|
|
596
|
+
@inline export function status(): u16 {
|
|
597
|
+
return load<u16>(reg.SOUNDCNT_X) & 0x3f;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** The two sample channels, A (0) and B (1), playing signed 8-bit samples
|
|
602
|
+
* from sample RAM (SPEC.md §14.6). */
|
|
603
|
+
export namespace pcm {
|
|
604
|
+
// @ts-ignore: decorator
|
|
605
|
+
@inline function base(ch: i32): usize {
|
|
606
|
+
return ch == 0 ? reg.PCMA_SAD : reg.PCMB_SAD;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Plays `len` samples from sample RAM offset `start` at `rate` Hz and `volume` 0–64, once. */
|
|
610
|
+
export function play(ch: i32, start: u16, len: u16, rate: u16, volume: u16): void {
|
|
611
|
+
const b = base(ch);
|
|
612
|
+
store<u16>(b, start);
|
|
613
|
+
store<u16>(b + 2, len);
|
|
614
|
+
store<u16>(b + 6, rate);
|
|
615
|
+
store<u16>(b + 8, sound.START | (volume < 64 ? volume : 64));
|
|
616
|
+
}
|
|
617
|
+
/** Like `play`, then repeats from sample `loopAt` until stopped. */
|
|
618
|
+
export function playLooped(ch: i32, start: u16, len: u16, loopAt: u16, rate: u16, volume: u16): void {
|
|
619
|
+
store<u16>(base(ch) + 4, loopAt);
|
|
620
|
+
play(ch, start, len, rate, volume);
|
|
621
|
+
store<u16>(base(ch) + 8, load<u16>(base(ch) + 8) | (1 << 13));
|
|
622
|
+
}
|
|
623
|
+
export function stop(ch: i32): void {
|
|
624
|
+
store<u16>(base(ch) + 8, 1 << 14);
|
|
625
|
+
}
|
|
626
|
+
/** The playback position, in samples from the start, as of the end of the last frame. */
|
|
627
|
+
export function position(ch: i32): u16 {
|
|
628
|
+
return load<u16>(base(ch) + 10);
|
|
629
|
+
}
|
|
630
|
+
export function playing(ch: i32): bool {
|
|
631
|
+
return (sound.status() & (1 << (4 + ch))) != 0;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
415
635
|
// ---- Fixed-point trigonometry --------------------------------------------------
|
|
416
636
|
|
|
417
637
|
/** Fixed-point trigonometry shared by all WASM-AD SDKs, so carts in any
|
package/bin/wasmad.mjs
CHANGED
|
@@ -10,26 +10,31 @@
|
|
|
10
10
|
// defaults to the version this tool implements. It is for toolchains that
|
|
11
11
|
// cannot emit custom sections themselves (AssemblyScript, TinyGo).
|
|
12
12
|
//
|
|
13
|
-
// `run` boots the cart like a host does (SPEC.md §3) and renders
|
|
14
|
-
// reference
|
|
13
|
+
// `run` boots the cart like a host does (SPEC.md §3) and renders and makes
|
|
14
|
+
// sound with the reference core, player/web/core.wasm (`make player`). Options:
|
|
15
15
|
// --frames N frames to run (default 1)
|
|
16
16
|
// --keys SCRIPT held buttons per frame, e.g. "0-29:right,30-59:down+b,60:a"
|
|
17
17
|
// --seed N value for SEED (default 1)
|
|
18
|
-
// --hash F,F,... print a hash of the frame rendered at each listed frame
|
|
18
|
+
// --hash F,F,... print a hash of the frame rendered at each listed frame,
|
|
19
|
+
// and one of all the sound
|
|
19
20
|
// --ppm FILE write the last rendered frame as a PPM image
|
|
21
|
+
// --wav FILE write the sound of every frame as a 48 kHz stereo WAV
|
|
20
22
|
// --core FILE a different core.wasm
|
|
21
23
|
|
|
22
24
|
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
23
25
|
import { createHash } from 'node:crypto';
|
|
24
26
|
import { fileURLToPath } from 'node:url';
|
|
25
27
|
|
|
26
|
-
const SPEC = '0.
|
|
27
|
-
const SPEC_VERSION =
|
|
28
|
+
const SPEC = '0.2';
|
|
29
|
+
const SPEC_VERSION = 0x0002;
|
|
28
30
|
const W = 240, H = 160;
|
|
29
31
|
const A = {
|
|
30
32
|
IO: 0x400, PAL_END: 0xC00, OAM: 0xC00, VIDEO_END: 0x19000, SAVE: 0x19000, SAVE_SIZE: 0x8000, CART: 0x28000,
|
|
31
33
|
VCOUNT: 0x406, KEYS: 0x700, FRAME: 0x704, SEED: 0x708, SAVECNT: 0x70C, SAVESTAT: 0x70E, SPEC: 0x710,
|
|
34
|
+
BG2PA: 0x420, BG2PD: 0x426, BG3PA: 0x430, BG3PD: 0x436,
|
|
35
|
+
SOUND_REGS: 0x460, SOUND_REGS_END: 0x4D0, SOUND_STATE: 0x780, SOUND_STATE_END: 0x800,
|
|
32
36
|
};
|
|
37
|
+
const SOUND_SAMPLES = 800;
|
|
33
38
|
const KEY = { a: 1, b: 2, select: 4, start: 8, right: 16, left: 32, up: 64, down: 128, r: 256, l: 512 };
|
|
34
39
|
const MAX_CART = 32 * 1024 * 1024;
|
|
35
40
|
|
|
@@ -315,13 +320,16 @@ export async function boot(bytes, { core: corePath, seed = 1, onTrace = () => {}
|
|
|
315
320
|
const dv = () => new DataView(ex.memory.buffer);
|
|
316
321
|
mem().fill(0, 0, A.CART);
|
|
317
322
|
for (let i = 0; i < 128; i++) dv().setUint16(A.OAM + i * 8, 0x0200, true);
|
|
323
|
+
for (const a of [A.BG2PA, A.BG2PD, A.BG3PA, A.BG3PD]) dv().setUint16(a, 0x100, true);
|
|
318
324
|
dv().setUint32(A.SEED, seed >>> 0, true);
|
|
319
325
|
dv().setUint16(A.SPEC, SPEC_VERSION, true);
|
|
320
326
|
ex.start?.();
|
|
321
327
|
|
|
322
|
-
let frame = 0;
|
|
328
|
+
let frame = 0, sound = new Int16Array(SOUND_SAMPLES * 2);
|
|
323
329
|
return {
|
|
324
330
|
exports: ex,
|
|
331
|
+
/** The last frame's sound: 800 stereo pairs of i16, left first. */
|
|
332
|
+
get sound() { return sound; },
|
|
325
333
|
/** Runs one frame with `keys` held and returns the rendered RGBA8 pixels. */
|
|
326
334
|
frame(keys = 0) {
|
|
327
335
|
dv().setUint16(A.KEYS, keys & 0x3FF, true);
|
|
@@ -329,7 +337,12 @@ export async function boot(bytes, { core: corePath, seed = 1, onTrace = () => {}
|
|
|
329
337
|
dv().setUint16(A.VCOUNT, 160, true);
|
|
330
338
|
ex.update();
|
|
331
339
|
const coreMem = new Uint8Array(core.memory.buffer, core.ad_mem(), core.ad_mem_len());
|
|
332
|
-
coreMem.set(mem().subarray(0, A.
|
|
340
|
+
coreMem.set(mem().subarray(0, A.CART));
|
|
341
|
+
core.ad_sound_frame();
|
|
342
|
+
for (const [from, to] of [[A.SOUND_REGS, A.SOUND_REGS_END], [A.SOUND_STATE, A.SOUND_STATE_END]]) {
|
|
343
|
+
mem().set(coreMem.subarray(from, to), from);
|
|
344
|
+
}
|
|
345
|
+
sound = new Int16Array(core.memory.buffer, core.ad_sound_buffer(), SOUND_SAMPLES * 2).slice();
|
|
333
346
|
for (let y = 0; y < H; y++) {
|
|
334
347
|
if (ex.scanline) {
|
|
335
348
|
dv().setUint16(A.VCOUNT, y, true);
|
|
@@ -345,8 +358,24 @@ export async function boot(bytes, { core: corePath, seed = 1, onTrace = () => {}
|
|
|
345
358
|
};
|
|
346
359
|
}
|
|
347
360
|
|
|
348
|
-
export function hash(
|
|
349
|
-
|
|
361
|
+
export function hash(...chunks) {
|
|
362
|
+
const h = createHash('sha256');
|
|
363
|
+
for (const c of chunks) h.update(new Uint8Array(c.buffer, c.byteOffset, c.byteLength));
|
|
364
|
+
return h.digest('hex').slice(0, 16);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** A 48 kHz 16-bit stereo WAV file of `frames` (Int16Arrays from `sound`). */
|
|
368
|
+
export function wav(frames) {
|
|
369
|
+
const bytes = frames.reduce((n, f) => n + f.byteLength, 0);
|
|
370
|
+
const out = new DataView(new ArrayBuffer(44 + bytes));
|
|
371
|
+
const tag = (at, s) => [...s].forEach((c, i) => out.setUint8(at + i, c.charCodeAt(0)));
|
|
372
|
+
tag(0, 'RIFF'); out.setUint32(4, 36 + bytes, true); tag(8, 'WAVEfmt ');
|
|
373
|
+
out.setUint32(16, 16, true); out.setUint16(20, 1, true); out.setUint16(22, 2, true);
|
|
374
|
+
out.setUint32(24, 48000, true); out.setUint32(28, 48000 * 4, true); out.setUint16(32, 4, true);
|
|
375
|
+
out.setUint16(34, 16, true); tag(36, 'data'); out.setUint32(40, bytes, true);
|
|
376
|
+
let at = 44;
|
|
377
|
+
for (const f of frames) for (const s of f) { out.setInt16(at, s, true); at += 2; }
|
|
378
|
+
return new Uint8Array(out.buffer);
|
|
350
379
|
}
|
|
351
380
|
|
|
352
381
|
function ppm(pixels) {
|
|
@@ -373,7 +402,7 @@ async function main([cmd, ...args]) {
|
|
|
373
402
|
const opts = options(args);
|
|
374
403
|
const [file, ...rest] = opts._;
|
|
375
404
|
if (!cmd || !file || !['check', 'meta', 'run'].includes(cmd)) {
|
|
376
|
-
console.error('usage: wasmad check <cart.wasm>\n wasmad meta <cart.wasm> [-o out.wasm] key=value...\n wasmad run <cart.wasm> [--frames N] [--keys SCRIPT] [--seed N] [--hash F,F] [--ppm FILE]');
|
|
405
|
+
console.error('usage: wasmad check <cart.wasm>\n wasmad meta <cart.wasm> [-o out.wasm] key=value...\n wasmad run <cart.wasm> [--frames N] [--keys SCRIPT] [--seed N] [--hash F,F] [--ppm FILE] [--wav FILE]');
|
|
377
406
|
return 2;
|
|
378
407
|
}
|
|
379
408
|
const bytes = new Uint8Array(readFileSync(file));
|
|
@@ -411,11 +440,15 @@ async function main([cmd, ...args]) {
|
|
|
411
440
|
const frames = Number(opts.frames ?? 1);
|
|
412
441
|
const marks = new Set((opts.hash ?? '').split(',').filter(Boolean).map(Number));
|
|
413
442
|
let pixels;
|
|
443
|
+
const sound = [];
|
|
414
444
|
for (let f = 0; f < frames; f++) {
|
|
415
445
|
pixels = host.frame(keys(f));
|
|
446
|
+
sound.push(host.sound);
|
|
416
447
|
if (marks.has(f)) console.log(`frame ${f}: ${hash(pixels)}`);
|
|
417
448
|
}
|
|
449
|
+
if (opts.hash) console.log(`sound: ${hash(...sound)}`);
|
|
418
450
|
if (opts.ppm) writeFileSync(opts.ppm, ppm(pixels));
|
|
451
|
+
if (opts.wav) writeFileSync(opts.wav, wav(sound));
|
|
419
452
|
return 0;
|
|
420
453
|
}
|
|
421
454
|
|