@lewismoten/gamemaker-sandbox-engine 0.0.0-stage → 3.1.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.
Files changed (57) hide show
  1. package/LICENSE +24 -0
  2. package/NOTICE.md +753 -0
  3. package/README.md +65 -2
  4. package/dist/assets/music.worklet-NqOeNo6T.js +464 -0
  5. package/dist/engine.js +5436 -0
  6. package/dist/types/audio/cmfLoad.d.ts +7 -0
  7. package/dist/types/audio/cmfPlayer.d.ts +69 -0
  8. package/dist/types/audio/context.d.ts +12 -0
  9. package/dist/types/audio/engine.d.ts +39 -0
  10. package/dist/types/audio/music.worklet.d.ts +14 -0
  11. package/dist/types/audio/opl.d.ts +37 -0
  12. package/dist/types/audio/output.d.ts +13 -0
  13. package/dist/types/dosPlayer/fileExtensions.d.ts +44 -0
  14. package/dist/types/dosPlayer/patchRules.d.ts +24 -0
  15. package/dist/types/dosPlayer/patchTable.d.ts +80 -0
  16. package/dist/types/engine/character.d.ts +30 -0
  17. package/dist/types/engine/funcKeys.d.ts +33 -0
  18. package/dist/types/engine/game.d.ts +52 -0
  19. package/dist/types/engine/keys.d.ts +17 -0
  20. package/dist/types/engine/machine.d.ts +99 -0
  21. package/dist/types/engine/menus.d.ts +46 -0
  22. package/dist/types/engine/monsters.d.ts +19 -0
  23. package/dist/types/engine/package.d.ts +4 -0
  24. package/dist/types/engine/playGain.d.ts +11 -0
  25. package/dist/types/engine/playerLook.d.ts +26 -0
  26. package/dist/types/engine/recorder.d.ts +60 -0
  27. package/dist/types/engine/render.d.ts +8 -0
  28. package/dist/types/engine/scene.d.ts +156 -0
  29. package/dist/types/engine/scroll.d.ts +16 -0
  30. package/dist/types/engine/showGraphic.d.ts +39 -0
  31. package/dist/types/engine/slowPc.d.ts +62 -0
  32. package/dist/types/engine/touch.d.ts +33 -0
  33. package/dist/types/engine/world.d.ts +215 -0
  34. package/dist/types/formats/binary.d.ts +44 -0
  35. package/dist/types/formats/blocks.d.ts +52 -0
  36. package/dist/types/formats/character.d.ts +65 -0
  37. package/dist/types/formats/cmf.d.ts +33 -0
  38. package/dist/types/formats/fli.d.ts +45 -0
  39. package/dist/types/formats/game.d.ts +72 -0
  40. package/dist/types/formats/gif.d.ts +15 -0
  41. package/dist/types/formats/graphicScripts.d.ts +18 -0
  42. package/dist/types/formats/highscore.d.ts +10 -0
  43. package/dist/types/formats/map.d.ts +24 -0
  44. package/dist/types/formats/monster.d.ts +48 -0
  45. package/dist/types/formats/palette.d.ts +15 -0
  46. package/dist/types/formats/recording.d.ts +7 -0
  47. package/dist/types/formats/sound.d.ts +13 -0
  48. package/dist/types/formats/voc.d.ts +13 -0
  49. package/dist/types/gfx/cp437Glyphs.d.ts +2 -0
  50. package/dist/types/gfx/font8x8.d.ts +2 -0
  51. package/dist/types/gfx/screen.d.ts +46 -0
  52. package/dist/types/input/chords.d.ts +22 -0
  53. package/dist/types/input/deskMouse.d.ts +14 -0
  54. package/dist/types/input/keyboard.d.ts +39 -0
  55. package/dist/types/input/scancodes.d.ts +3 -0
  56. package/dist/types/project/gameFiles.d.ts +49 -0
  57. package/package.json +26 -4
package/README.md CHANGED
@@ -1,3 +1,66 @@
1
- # Temporary Holding Version
1
+ # Game-Maker: Sandbox Edition browser engine
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@lewismoten/gamemaker-sandbox-engine` runs RSD Game-Maker games from their original DOS files in a browser. It is the same TypeScript engine used by the Game-Maker: Sandbox Edition web player. The package includes its audio worklet and TypeScript declarations; it does not include games or the site's editor and catalog.
4
+
5
+ Build a tarball from this repository with `cd web/packages/engine && npm pack`. The package has not been published to the npm registry. Install the tarball with `npm install ./lewismoten-gamemaker-sandbox-engine-3.1.0.tgz`.
6
+
7
+ Pass the files from one game's DOS directory to `GameFiles`. Names are case insensitive and use DOS 8.3 names. `GameFiles.mainGame()` finds a `.GAM` file. Keep the original file names and bytes; the game refers to its other files by name.
8
+
9
+ ```ts
10
+ import { GameFiles, Machine, runGame } from '@lewismoten/gamemaker-sandbox-engine';
11
+
12
+ const input = document.querySelector<HTMLInputElement>('#files')!;
13
+ const canvas = document.querySelector<HTMLCanvasElement>('#screen')!;
14
+ const play = document.querySelector<HTMLButtonElement>('#play')!;
15
+ let files: GameFiles | undefined;
16
+
17
+ input.addEventListener('change', async () => {
18
+ const entries = await Promise.all(
19
+ Array.from(input.files ?? [], async (file): Promise<[string, Uint8Array]> => [
20
+ file.name,
21
+ new Uint8Array(await file.arrayBuffer()),
22
+ ]),
23
+ );
24
+ files = new GameFiles(entries);
25
+ play.disabled = !files.mainGame();
26
+ });
27
+
28
+ play.addEventListener('click', async () => {
29
+ if (!files) return;
30
+ const gam = files.mainGame();
31
+ if (!gam) throw new Error('Choose a game directory containing a .GAM file');
32
+
33
+ const machine = new Machine(canvas, files);
34
+ try {
35
+ await machine.audio.resume(); // the click is a user gesture
36
+ await runGame(machine, gam);
37
+ } finally {
38
+ machine.destroy();
39
+ }
40
+ });
41
+ ```
42
+
43
+ ```html
44
+ <input id="files" type="file" webkitdirectory multiple aria-label="Game directory" />
45
+ <button id="play" disabled>Play</button>
46
+ <canvas id="screen" tabindex="0" aria-label="Game screen"></canvas>
47
+ <style>
48
+ #screen { width: min(100%, 960px); aspect-ratio: 8 / 5; image-rendering: pixelated; }
49
+ </style>
50
+ ```
51
+
52
+ `Machine` owns the screen, keyboard and audio. Call `machine.destroy()` when play finishes or the containing page closes. `runGame` accepts `RunOptions` for callbacks, joystick input, playback and the optional DOS player appearance. The game may write saves or high scores into `GameFiles`; use `files.onChange` to persist those bytes. The package targets modern browsers and ES modules.
53
+
54
+ The source is MIT licensed; see `LICENSE` and `NOTICE.md` for credits.
55
+
56
+ ## Publishing
57
+
58
+ The [release workflow](../../../.github/workflows/publish-engine.yml) publishes this package to
59
+ GitHub Packages when an `engine-v<version>` tag is pushed. The tagged commit must be on
60
+ `master`, and `<version>` must exactly match this package's `package.json` (for example,
61
+ `engine-v3.1.0`). Merge the package changes to `master` before pushing the tag.
62
+
63
+ The workflow uses GitHub's `GITHUB_TOKEN` with package write permission. It checks the source,
64
+ builds the package, and publishes it to GitHub Packages. No package is published by ordinary
65
+ branch pushes. Publishing to npm is a separate, manual decision; this workflow does not use
66
+ an npm token.
@@ -0,0 +1,464 @@
1
+ const g = [0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 12, 15, 15], M = [0, 9, 12, 13.875, 15, 16.125, 16.875, 17.625, 18, 18.75, 19.125, 19.5, 19.875, 20.25, 20.625, 21], v = [0, 0.5, 0.25, 1], O = 49716, l = 96, D = [0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21];
2
+ class b {
3
+ am = !1;
4
+ vib = !1;
5
+ sustainHold = !1;
6
+ ksr = !1;
7
+ mult = 1;
8
+ ksl = 0;
9
+ tl = 0;
10
+ ar = 0;
11
+ dr = 0;
12
+ sl = 0;
13
+ rr = 0;
14
+ wave = 0;
15
+ // Runtime state. envDb is the envelope attenuation (0 = full, SILENT_DB = off);
16
+ // out/prevOut keep the last two modulator outputs for feedback.
17
+ phase = 0;
18
+ // cycles
19
+ stage = 0;
20
+ envDb = l;
21
+ out = 0;
22
+ prevOut = 0;
23
+ keyOn() {
24
+ this.stage = 1, this.phase = 0;
25
+ }
26
+ keyOff() {
27
+ this.stage !== 0 && (this.stage = 4);
28
+ }
29
+ /** Rate in "rate units" (0..63) including key scale rate offset. */
30
+ rate(t, s) {
31
+ return t === 0 ? 0 : Math.min(63, t * 4 + (this.ksr ? s : s >> 2));
32
+ }
33
+ stepEnvelope(t, s) {
34
+ switch (this.stage) {
35
+ case 1: {
36
+ const e = this.rate(this.ar, s);
37
+ if (e === 0) return;
38
+ if (e >= 60)
39
+ this.envDb = 0;
40
+ else {
41
+ const h = 2.826 / Math.pow(2, (e - 4) / 4);
42
+ this.envDb -= (this.envDb + 8) * (t / h) * 3.2;
43
+ }
44
+ this.envDb <= 0 && (this.envDb = 0, this.stage = 2);
45
+ return;
46
+ }
47
+ case 2: {
48
+ const e = this.sl === 15 ? 93 : this.sl * 3;
49
+ this.envDb += this.decayDbPerSecond(this.dr, s) * t, this.envDb >= e && (this.envDb = e, this.stage = 3);
50
+ return;
51
+ }
52
+ // EG-TYP set (sustainHold) holds the level until key off; otherwise the
53
+ // percussive envelope keeps falling at the release rate.
54
+ case 3:
55
+ this.sustainHold || (this.envDb += this.decayDbPerSecond(this.rr, s) * t);
56
+ break;
57
+ case 4:
58
+ this.envDb += this.decayDbPerSecond(this.rr, s) * t;
59
+ break;
60
+ default:
61
+ return;
62
+ }
63
+ this.envDb >= l && (this.envDb = l, this.stage = 0);
64
+ }
65
+ decayDbPerSecond(t, s) {
66
+ const e = this.rate(t, s);
67
+ return e === 0 ? 0 : l / (39.28 / Math.pow(2, (e - 4) / 4));
68
+ }
69
+ /** Waveform value at phase (cycles): 0 sine, 1 half sine, 2 absolute sine, 3 pulse (quarter) sine. */
70
+ wave01(t) {
71
+ const s = t - Math.floor(t), e = Math.sin(s * 2 * Math.PI);
72
+ switch (this.wave) {
73
+ case 1:
74
+ return s < 0.5 ? e : 0;
75
+ case 2:
76
+ return Math.abs(e);
77
+ case 3:
78
+ return s < 0.25 || s >= 0.5 && s < 0.75 ? Math.abs(e) : 0;
79
+ default:
80
+ return e;
81
+ }
82
+ }
83
+ }
84
+ class S {
85
+ fnum = 0;
86
+ block = 0;
87
+ key = !1;
88
+ feedback = 0;
89
+ additive = !1;
90
+ ops = [new b(), new b()];
91
+ /** Hz = F-number * clock / 2^(20 - block); block is the octave. */
92
+ get freq() {
93
+ return this.fnum * O / Math.pow(2, 20 - this.block);
94
+ }
95
+ /** Key scale rate offset: block * 2 + bit 9 (or 8) of the F-number. */
96
+ rof(t) {
97
+ return this.block * 2 + (this.fnum >> (t ? 8 : 9) & 1);
98
+ }
99
+ /** KSL attenuation: table value minus 6 dB per octave below block 7, scaled by the KSL setting. */
100
+ kslDb(t) {
101
+ return t.ksl ? Math.max(0, M[this.fnum >> 6] - 6 * (7 - this.block)) * v[t.ksl] : 0;
102
+ }
103
+ }
104
+ class P {
105
+ sampleRate;
106
+ channels = Array.from({ length: 9 }, () => new S());
107
+ regs = new Uint8Array(256);
108
+ waveSelect = !1;
109
+ noteSel = !1;
110
+ amDepth = !1;
111
+ vibDepth = !1;
112
+ rhythm = !1;
113
+ time = 0;
114
+ noise = 1;
115
+ constructor(t) {
116
+ this.sampleRate = t;
117
+ }
118
+ reset() {
119
+ for (let t = 0; t < 256; t++) this.write(t, 0);
120
+ for (const t of this.channels)
121
+ for (const s of t.ops)
122
+ s.stage = 0, s.envDb = l;
123
+ }
124
+ read(t) {
125
+ return this.regs[t & 255];
126
+ }
127
+ write(t, s) {
128
+ if (t &= 255, s &= 255, this.regs[t] = s, t === 1) {
129
+ this.waveSelect = (s & 32) !== 0;
130
+ return;
131
+ }
132
+ if (t === 8) {
133
+ this.noteSel = (s & 64) !== 0;
134
+ return;
135
+ }
136
+ if (t === 189) {
137
+ this.writeRhythm(s);
138
+ return;
139
+ }
140
+ const e = t & 224;
141
+ if (e >= 32 && e <= 128 || e === 224) {
142
+ const o = D.indexOf(t & 31);
143
+ if (o < 0) return;
144
+ const r = this.channels[Math.floor(o / 6) * 3 + o % 3].ops[Math.floor(o % 6 / 3)];
145
+ switch (e) {
146
+ case 32:
147
+ r.am = (s & 128) !== 0, r.vib = (s & 64) !== 0, r.sustainHold = (s & 32) !== 0, r.ksr = (s & 16) !== 0, r.mult = g[s & 15];
148
+ break;
149
+ case 64:
150
+ r.ksl = s >> 6, r.tl = s & 63;
151
+ break;
152
+ case 96:
153
+ r.ar = s >> 4, r.dr = s & 15;
154
+ break;
155
+ case 128:
156
+ r.sl = s >> 4, r.rr = s & 15;
157
+ break;
158
+ case 224:
159
+ r.wave = this.waveSelect ? s & 3 : 0;
160
+ break;
161
+ }
162
+ return;
163
+ }
164
+ const h = t & 15;
165
+ if (h > 8) return;
166
+ const i = this.channels[h];
167
+ if (e === 160 && t < 176)
168
+ i.fnum = i.fnum & 768 | s;
169
+ else if (t >= 176 && t <= 184) {
170
+ i.fnum = i.fnum & 255 | (s & 3) << 8, i.block = s >> 2 & 7;
171
+ const o = (s & 32) !== 0;
172
+ if (o && !i.key) for (const a of i.ops) a.keyOn();
173
+ if (!o && i.key) for (const a of i.ops) a.keyOff();
174
+ i.key = o;
175
+ } else t >= 192 && t <= 200 && (i.feedback = s >> 1 & 7, i.additive = (s & 1) !== 0);
176
+ }
177
+ rhythmKeys = 0;
178
+ /**
179
+ * Register 0xBD: bit 7 tremolo depth, bit 6 vibrato depth, bit 5 rhythm mode, bits 0-4 the
180
+ * drum keys. In rhythm mode channels 6-8 become the bass drum (both operators of channel 6),
181
+ * hi-hat and snare (channel 7) and tom-tom and cymbal (channel 8).
182
+ */
183
+ writeRhythm(t) {
184
+ this.amDepth = (t & 128) !== 0, this.vibDepth = (t & 64) !== 0, this.rhythm = (t & 32) !== 0;
185
+ const s = this.rhythm ? t & 31 : 0, e = s ^ this.rhythmKeys, [h, i, o] = [this.channels[6], this.channels[7], this.channels[8]], a = (r, n) => {
186
+ if (e & r)
187
+ for (const f of n) s & r ? f.keyOn() : f.keyOff();
188
+ };
189
+ a(16, h.ops), a(8, [i.ops[1]]), a(4, [o.ops[0]]), a(2, [o.ops[1]]), a(1, [i.ops[0]]), this.rhythmKeys = s;
190
+ }
191
+ /** Renders mono samples into out (added to existing content). */
192
+ render(t, s = 0.12) {
193
+ const e = 1 / this.sampleRate;
194
+ for (let h = 0; h < t.length; h++) {
195
+ this.time += e;
196
+ const i = (1 - Math.cos(this.time * 3.7 * 2 * Math.PI)) / 2 * (this.amDepth ? 4.8 : 1), o = Math.sin(this.time * 6.1 * 2 * Math.PI) * (this.vibDepth ? 14 : 7), a = Math.pow(2, o / 1200);
197
+ this.noise = this.noise >> 1 | ((this.noise >> 0 ^ this.noise >> 14) & 1) << 22;
198
+ const r = this.noise & 1 ? 1 : -1;
199
+ let n = 0;
200
+ const f = this.rhythm ? 6 : 9;
201
+ for (let c = 0; c < f; c++) n += this.channelSample(this.channels[c], e, i, a);
202
+ this.rhythm && (n += this.rhythmSample(e, i, a, r)), t[h] += n * s;
203
+ }
204
+ }
205
+ /** Linear amplitude from the summed attenuation: envelope + TL (0.75 dB steps) + KSL + tremolo. */
206
+ opLevel(t, s, e) {
207
+ const h = s.envDb + s.tl * 0.75 + t.kslDb(s) + (s.am ? e : 0);
208
+ return h >= l ? 0 : Math.pow(10, -h / 20);
209
+ }
210
+ advance(t, s, e, h) {
211
+ s.phase += t.freq * s.mult * (s.vib ? h : 1) * e, s.phase > 1e6 && (s.phase -= Math.floor(s.phase)), s.stepEnvelope(e, t.rof(this.noteSel));
212
+ }
213
+ channelSample(t, s, e, h) {
214
+ const [i, o] = t.ops;
215
+ if (i.stage === 0 && o.stage === 0) return 0;
216
+ this.advance(t, i, s, h), this.advance(t, o, s, h);
217
+ const a = t.feedback ? (i.out + i.prevOut) * Math.pow(2, t.feedback - 7) : 0, r = i.wave01(i.phase + a) * this.opLevel(t, i, e);
218
+ i.prevOut = i.out, i.out = r;
219
+ const n = this.opLevel(t, o, e);
220
+ return t.additive ? r + o.wave01(o.phase) * n : o.wave01(o.phase + r * 4) * n;
221
+ }
222
+ /**
223
+ * Approximates the five drums. The bass drum is a normal FM channel (at double level); the
224
+ * hi-hat, snare and cymbal mix operator tone with LFSR noise, and the tom-tom is a plain tone.
225
+ * The mixing weights are this port's approximation, not the chip's exact phase logic.
226
+ */
227
+ rhythmSample(t, s, e, h) {
228
+ const [i, o, a] = [this.channels[6], this.channels[7], this.channels[8]];
229
+ let r = this.channelSample(i, t, s, e) * 2;
230
+ const [n, f] = o.ops, [c, p] = a.ops;
231
+ for (const [w, k] of [
232
+ [o, n],
233
+ [o, f],
234
+ [a, c],
235
+ [a, p]
236
+ ])
237
+ this.advance(w, k, t, e);
238
+ return n.stage !== 0 && (r += h * 0.5 * this.opLevel(o, n, s) * (n.wave01(n.phase) > 0 ? 1 : 0.6) * 2), f.stage !== 0 && (r += (f.wave01(f.phase) * 0.6 + h * 0.4) * this.opLevel(o, f, s) * 2), c.stage !== 0 && (r += c.wave01(c.phase) * this.opLevel(a, c, s) * 2), p.stage !== 0 && (r += (p.wave01(p.phase) > 0 ? 0.5 : -0.5) * (h > 0 ? 1 : 0.7) * this.opLevel(a, p, s) * 2), r;
239
+ }
240
+ }
241
+ const y = [0, 1, 2, 8, 9, 10, 16, 17, 18], T = 1193180 / Math.floor(65535 / 8), u = { 11: 16, 12: 8, 13: 4, 14: 2, 15: 1 }, d = {
242
+ 11: { ch: 6, ops: ["mod", "car"] },
243
+ 12: { ch: 7, ops: ["car"] },
244
+ 13: { ch: 8, ops: ["mod"] },
245
+ 14: { ch: 8, ops: ["car"] },
246
+ 15: { ch: 7, ops: ["mod"] }
247
+ };
248
+ class K {
249
+ opl;
250
+ music = new Uint8Array(0);
251
+ instruments = [];
252
+ // PlayIt's statics: read position in the music block, ticks left before the next
253
+ // event, and the running status (channel and 8 = note off / 9 = note on).
254
+ offset = 0;
255
+ wait = 0;
256
+ channel = 0;
257
+ funct = 8;
258
+ rhythm = !1;
259
+ rhythmKeys = 0;
260
+ // Note sounding on each MIDI channel (-1 none), so a note off only releases its own note.
261
+ notes = new Int16Array(16).fill(-1);
262
+ programs = new Uint8Array(16);
263
+ playing = !1;
264
+ constructor(t) {
265
+ this.opl = t, this.init();
266
+ }
267
+ /** sbfd_init: enable waveform select, melody mode (0xBD = deep tremolo and vibrato, rhythm off). */
268
+ init() {
269
+ this.opl.reset(), this.opl.write(1, 32), this.opl.write(8, 0), this.opl.write(189, 192);
270
+ }
271
+ load(t, s) {
272
+ this.stop(), this.init(), this.music = t, this.instruments = s, this.rhythm = !1, this.rhythmKeys = 0;
273
+ for (let e = 0; e < 16; e++) this.programChange(e, e);
274
+ this.offset = 0, this.playing = t.length > 0;
275
+ }
276
+ /** sbfd_music_off + rewind. */
277
+ stop() {
278
+ this.allNotesOff(), this.offset = 0, this.playing = !1;
279
+ }
280
+ allNotesOff() {
281
+ for (let t = 0; t < 9; t++) this.opl.write(176 + t, this.opl.read(176 + t) & -33);
282
+ this.rhythmKeys = 0, this.opl.write(189, 192 | (this.rhythm ? 32 : 0)), this.notes.fill(-1);
283
+ }
284
+ /**
285
+ * *(APOS) in SBMUSIC.C. PlayIt read sbmusic+offset with no bounds check, so past the end of
286
+ * the farmalloc'd music block it read whatever heap memory followed. Every CMF ends with the
287
+ * FF 2F 00 end marker, which restarts the song before the read gets there; only a truncated
288
+ * file runs off the end, and for that the port reads 0xFF, which takes the same path as the
289
+ * end marker (sbfd_music_off, offset = 0).
290
+ */
291
+ byte(t) {
292
+ return this.music[t] ?? 255;
293
+ }
294
+ /**
295
+ * getpaws() in SBMUSIC.C: reads a MIDI variable length number (7 bits per byte, high bit
296
+ * set on every byte but the last), with no limit on the number of bytes:
297
+ * do { offset++; value = (value << 7) | ((c=*(APOS)) & 0x07F); } while (c & 0x80);
298
+ * value is an unsigned long, so bits shifted past 32 are lost (>>> 0). The one difference:
299
+ * the loop stops at the end of the data, where DOS read on into unknown memory.
300
+ */
301
+ getPaws() {
302
+ let t = this.byte(this.offset);
303
+ if (t & 128) {
304
+ t &= 127;
305
+ let s;
306
+ do
307
+ this.offset++, s = this.byte(this.offset), t = (t << 7 | s & 127) >>> 0;
308
+ while (s & 128 && this.offset < this.music.length);
309
+ }
310
+ return this.offset++, t;
311
+ }
312
+ /** One timer tick of PlayIt(). */
313
+ tick() {
314
+ if (!this.playing) return;
315
+ this.offset === 0 && (this.wait = 1, this.getPaws()), this.wait--;
316
+ let t = 0;
317
+ for (; this.wait <= 0 && t++ < 1e5; ) {
318
+ const s = this.byte(this.offset);
319
+ switch (s & 240) {
320
+ case 144:
321
+ this.channel = s & 15, this.offset++, this.funct = 9;
322
+ break;
323
+ case 128:
324
+ this.channel = s & 15, this.offset++, this.funct = 8;
325
+ break;
326
+ }
327
+ const e = this.byte(this.offset);
328
+ if (e < 128) {
329
+ const h = this.byte(this.offset + 1);
330
+ this.funct === 8 || h === 0 ? this.noteOff(this.channel, e) : this.noteOn(this.channel, e, h), this.offset += 2;
331
+ } else
332
+ switch (e & 240) {
333
+ case 160:
334
+ this.offset += 3;
335
+ break;
336
+ case 176:
337
+ this.byte(this.offset + 1) === 103 && this.setMode(this.byte(this.offset + 2)), this.offset += 3;
338
+ break;
339
+ case 192:
340
+ this.programChange(e & 15, this.byte(this.offset + 1)), this.offset += 2;
341
+ break;
342
+ case 208:
343
+ this.offset += 2;
344
+ break;
345
+ case 224:
346
+ this.offset += 3;
347
+ break;
348
+ case 240:
349
+ e === 247 || e === 240 ? (this.offset++, this.offset += this.getPaws(), this.offset++) : (this.allNotesOff(), this.offset = 0);
350
+ break;
351
+ default:
352
+ this.allNotesOff(), this.offset = 0;
353
+ }
354
+ this.wait = this.getPaws();
355
+ }
356
+ }
357
+ /** sbfd_setmode: switch melody/rhythm mode and reload the drum channels' instruments. */
358
+ setMode(t) {
359
+ if (this.allNotesOff(), this.rhythm = t !== 0, this.opl.write(189, 192 | (this.rhythm ? 32 : 0)), this.rhythm) for (const s of [11, 12, 13, 14, 15]) this.programChange(s, this.programs[s]);
360
+ }
361
+ programChange(t, s) {
362
+ this.programs[t] = s;
363
+ const e = this.instruments[s];
364
+ if (e) {
365
+ if (this.rhythm && d[t]) {
366
+ const h = d[t];
367
+ h.ops.length === 2 ? this.loadVoice(h.ch, e) : this.loadOperator(y[h.ch] + (h.ops[0] === "car" ? 3 : 0), e, 0);
368
+ return;
369
+ }
370
+ t < this.melodyVoices() && this.loadVoice(t, e);
371
+ }
372
+ }
373
+ melodyVoices() {
374
+ return this.rhythm ? 6 : 9;
375
+ }
376
+ /**
377
+ * Writes one operator from a 16 byte SBI instrument record, whose bytes alternate
378
+ * modulator/carrier: 0/1 AM-VIB-EG-KSR-MULT, 2/3 KSL-TL, 4/5 AR-DR, 6/7 SL-RR, 8/9 waveform.
379
+ */
380
+ loadOperator(t, s, e) {
381
+ this.opl.write(32 + t, s[0 + e]), this.opl.write(64 + t, s[2 + e]), this.opl.write(96 + t, s[4 + e]), this.opl.write(128 + t, s[6 + e]), this.opl.write(224 + t, s[8 + e]);
382
+ }
383
+ loadVoice(t, s) {
384
+ const e = y[t];
385
+ this.loadOperator(e, s, 0), this.loadOperator(e + 3, s, 1), this.opl.write(192 + t, s[10]);
386
+ }
387
+ /**
388
+ * MIDI note -> OPL frequency: picks the lowest block (octave) whose F-number fits in 10 bits,
389
+ * which keeps the most pitch resolution (Hz = fnum * 49716 / 2^(20 - block)).
390
+ */
391
+ setPitch(t, s, e) {
392
+ const h = 440 * Math.pow(2, (s - 69) / 12);
393
+ let i, o = 0;
394
+ for (i = 0; i < 8 && (o = Math.round(h * Math.pow(2, 20 - i) / 49716), !(o < 1024)); i++)
395
+ ;
396
+ i = Math.min(i, 7), o = Math.min(o, 1023), this.opl.write(160 + t, o & 255), this.opl.write(176 + t, (e ? 32 : 0) | i << 2 | o >> 8);
397
+ }
398
+ /**
399
+ * SBFMDRV's loudness by velocity: it scales the instrument's carrier level (TL is an
400
+ * attenuation, 0 loudest .. 63 silent) and keeps the key-scaling bits:
401
+ * TL = 63 - floor((velocity + 128) * (63 - instrumentTL) / 256). So velocity 127 plays
402
+ * the instrument almost as loud as written and velocity 1 about half as loud in TL steps.
403
+ */
404
+ applyVelocity(t, s) {
405
+ const e = this.instruments[this.programs[t]];
406
+ if (!e) return;
407
+ let h, i;
408
+ const o = this.rhythm ? d[t] : void 0;
409
+ if (o && o.ops.length === 1)
410
+ h = y[o.ch] + (o.ops[0] === "car" ? 3 : 0), i = e[2];
411
+ else {
412
+ const r = o ? o.ch : t;
413
+ if (!o && r >= this.melodyVoices()) return;
414
+ h = y[r] + 3, i = e[3];
415
+ }
416
+ const a = 63 - Math.floor((Math.min(127, s) + 128) * (63 - (i & 63)) / 256);
417
+ this.opl.write(64 + h, i & 192 | a);
418
+ }
419
+ noteOn(t, s, e = 127) {
420
+ if (this.applyVelocity(t, e), this.rhythm && u[t]) {
421
+ const h = u[t];
422
+ this.setPitch(d[t].ch, s, !1), this.rhythmKeys &= ~h, this.opl.write(189, 224 | this.rhythmKeys), this.rhythmKeys |= h, this.opl.write(189, 224 | this.rhythmKeys), this.notes[t] = s;
423
+ return;
424
+ }
425
+ t >= this.melodyVoices() || (this.notes[t] >= 0 && this.opl.write(176 + t, this.opl.read(176 + t) & -33), this.setPitch(t, s, !0), this.notes[t] = s);
426
+ }
427
+ noteOff(t, s) {
428
+ if (this.notes[t] === s) {
429
+ if (this.notes[t] = -1, this.rhythm && u[t]) {
430
+ this.rhythmKeys &= ~u[t], this.opl.write(189, 224 | this.rhythmKeys);
431
+ return;
432
+ }
433
+ t < this.melodyVoices() && this.opl.write(176 + t, this.opl.read(176 + t) & -33);
434
+ }
435
+ }
436
+ }
437
+ class L extends AudioWorkletProcessor {
438
+ opl = new P(sampleRate);
439
+ player = new K(this.opl);
440
+ untilTick = 0;
441
+ paused = !1;
442
+ ended = !1;
443
+ samplesPerTick = sampleRate / T;
444
+ constructor() {
445
+ super(), this.port.onmessage = (t) => {
446
+ const s = t.data;
447
+ s.type === "load" ? (this.player.load(s.music, s.instruments), this.untilTick = 0) : s.type === "stop" ? this.player.stop() : s.type === "pause" ? this.paused = s.paused : s.type === "end" && (this.ended = !0);
448
+ };
449
+ }
450
+ process(t, s) {
451
+ if (this.ended) return !1;
452
+ const e = s[0][0];
453
+ if (!e || this.paused) return !0;
454
+ let h = 0;
455
+ for (; h < e.length; ) {
456
+ this.untilTick <= 0 && (this.player.tick(), this.untilTick += this.samplesPerTick);
457
+ const i = Math.min(e.length - h, Math.ceil(this.untilTick));
458
+ this.opl.render(e.subarray(h, h + i)), this.untilTick -= i, h += i;
459
+ }
460
+ for (let i = 1; i < s[0].length; i++) s[0][i].set(e);
461
+ return !0;
462
+ }
463
+ }
464
+ registerProcessor("gm-music", L);