@lewismoten/gamemaker-sandbox-engine 3.1.0 → 3.3.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 (108) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/NOTICE.md +58 -753
  3. package/README.md +94 -10
  4. package/dist/assets/{music.worklet-NqOeNo6T.js → music.worklet-aD2x7RRM.js} +23 -21
  5. package/dist/audio/cmfLoad.js +10 -0
  6. package/dist/audio/cmfPlayer.js +201 -0
  7. package/dist/audio/context.js +75 -0
  8. package/dist/audio/engine.js +173 -0
  9. package/dist/audio/mix.js +40 -0
  10. package/dist/audio/opl.js +245 -0
  11. package/dist/audio/output.js +40 -0
  12. package/dist/core/gameFiles.js +89 -0
  13. package/dist/engine/character.js +187 -0
  14. package/dist/engine/funcKeys.js +164 -0
  15. package/dist/engine/game.js +133 -0
  16. package/dist/engine/keys.js +68 -0
  17. package/dist/engine/machine.js +192 -0
  18. package/dist/engine/menus.js +135 -0
  19. package/dist/engine/monsters.js +259 -0
  20. package/dist/engine/playGain.js +10 -0
  21. package/dist/engine/playerLook.js +48 -0
  22. package/dist/engine/recorder.js +127 -0
  23. package/dist/engine/render.js +45 -0
  24. package/dist/engine/scene.js +324 -0
  25. package/dist/engine/scroll.js +79 -0
  26. package/dist/engine/showGraphic.js +107 -0
  27. package/dist/engine/slowPc.js +98 -0
  28. package/dist/engine/slowPcMusic.js +46 -0
  29. package/dist/engine/touch.js +61 -0
  30. package/dist/engine/world.js +205 -0
  31. package/dist/formats/binary.js +120 -0
  32. package/dist/formats/blocks.js +103 -0
  33. package/dist/formats/character.js +83 -0
  34. package/dist/formats/cmf.js +37 -0
  35. package/dist/formats/cmfEdit.js +154 -0
  36. package/dist/formats/cmfPreview.js +89 -0
  37. package/dist/formats/cmfSong.js +190 -0
  38. package/dist/formats/fli.js +340 -0
  39. package/dist/formats/fliFrames.js +34 -0
  40. package/dist/formats/fliLimits.js +54 -0
  41. package/dist/formats/game.js +198 -0
  42. package/dist/formats/gif.js +125 -0
  43. package/dist/formats/gifProgressive.js +99 -0
  44. package/dist/formats/graphicScripts.js +44 -0
  45. package/dist/formats/highscore.js +30 -0
  46. package/dist/formats/jpeg.js +359 -0
  47. package/dist/formats/map.js +38 -0
  48. package/dist/formats/monster.js +69 -0
  49. package/dist/formats/palette.js +58 -0
  50. package/dist/formats/recording.js +17 -0
  51. package/dist/formats/sound.js +44 -0
  52. package/dist/formats/voc.js +303 -0
  53. package/dist/gfx/colorVision.js +45 -0
  54. package/dist/gfx/cp437Glyphs.js +337 -0
  55. package/dist/gfx/font8x8.js +10 -0
  56. package/dist/gfx/screen.js +151 -0
  57. package/dist/index.js +15 -0
  58. package/dist/input/chords.js +46 -0
  59. package/dist/input/deskMouse.js +24 -0
  60. package/dist/input/keyboard.js +82 -0
  61. package/dist/input/scancodes.js +143 -0
  62. package/dist/player/fileExtensions.js +118 -0
  63. package/dist/player/patchRules.js +129 -0
  64. package/dist/player/patchTable.js +1753 -0
  65. package/dist/text/cp437.js +44 -0
  66. package/dist/types/audio/cmfPlayer.d.ts +1 -1
  67. package/dist/types/audio/engine.d.ts +21 -0
  68. package/dist/types/audio/mix.d.ts +14 -0
  69. package/dist/types/audio/music.worklet.d.ts +8 -0
  70. package/dist/types/engine/character.d.ts +3 -3
  71. package/dist/types/engine/funcKeys.d.ts +8 -6
  72. package/dist/types/engine/game.d.ts +9 -4
  73. package/dist/types/engine/keys.d.ts +2 -2
  74. package/dist/types/engine/machine.d.ts +8 -4
  75. package/dist/types/engine/menus.d.ts +4 -4
  76. package/dist/types/engine/monsters.d.ts +1 -1
  77. package/dist/types/engine/playerLook.d.ts +20 -2
  78. package/dist/types/engine/recorder.d.ts +8 -1
  79. package/dist/types/engine/render.d.ts +2 -2
  80. package/dist/types/engine/scene.d.ts +41 -9
  81. package/dist/types/engine/scroll.d.ts +9 -1
  82. package/dist/types/engine/showGraphic.d.ts +2 -2
  83. package/dist/types/engine/slowPc.d.ts +64 -2
  84. package/dist/types/engine/slowPcMusic.d.ts +18 -0
  85. package/dist/types/engine/touch.d.ts +2 -2
  86. package/dist/types/engine/world.d.ts +28 -6
  87. package/dist/types/formats/cmfEdit.d.ts +53 -0
  88. package/dist/types/formats/cmfPreview.d.ts +19 -0
  89. package/dist/types/formats/cmfSong.d.ts +74 -0
  90. package/dist/types/formats/fli.d.ts +11 -0
  91. package/dist/types/formats/fliFrames.d.ts +18 -0
  92. package/dist/types/formats/fliLimits.d.ts +13 -0
  93. package/dist/types/formats/game.d.ts +7 -0
  94. package/dist/types/formats/gif.d.ts +1 -1
  95. package/dist/types/formats/gifProgressive.d.ts +46 -0
  96. package/dist/types/formats/jpeg.d.ts +20 -0
  97. package/dist/types/gfx/colorVision.d.ts +26 -0
  98. package/dist/types/gfx/screen.d.ts +4 -0
  99. package/dist/types/index.d.ts +4 -0
  100. package/dist/types/input/keyboard.d.ts +1 -1
  101. package/dist/types/{dosPlayer → player}/patchRules.d.ts +1 -1
  102. package/dist/types/{dosPlayer → player}/patchTable.d.ts +8 -5
  103. package/dist/types/text/cp437.d.ts +6 -0
  104. package/package.json +322 -9
  105. package/dist/engine.js +0 -5436
  106. package/dist/types/engine/package.d.ts +0 -4
  107. /package/dist/types/{project → core}/gameFiles.d.ts +0 -0
  108. /package/dist/types/{dosPlayer → player}/fileExtensions.d.ts +0 -0
package/README.md CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
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
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`.
5
+ Install from your configured npm registry:
6
+
7
+ ```sh
8
+ npm install @lewismoten/gamemaker-sandbox-engine
9
+ ```
10
+
11
+ If your registry requires authentication or a scope mapping, configure npm using that registry's instructions.
6
12
 
7
13
  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
14
 
@@ -51,16 +57,94 @@ play.addEventListener('click', async () => {
51
57
 
52
58
  `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
59
 
60
+ `machine.screen.dosCropped` is observer metadata for PLAYGAME’s CropScreen state: true during level rendering and its fades, false for function-key screens and after a level. `sceneNumber` identifies the last rendered level. These fields do not change rendering or game timing.
61
+
62
+ ## Read a game file in Node.js
63
+
64
+ The format modules need no browser. This reads a `.GAM` file from the original CD (run it from the repository root) and lists its scene titles:
65
+
66
+ ```ts
67
+ import { readFileSync } from 'node:fs';
68
+ import { parseGame } from '@lewismoten/gamemaker-sandbox-engine/formats/game';
69
+ import { fromCp437 } from '@lewismoten/gamemaker-sandbox-engine/text/cp437';
70
+
71
+ const game = parseGame(readFileSync('cd/gameware/peach/peach.gam'));
72
+ console.log(`${game.scenes.length} scenes: ${game.scenes.slice(2, 4).map((s) => fromCp437(s.desc)).join(' and ')}`);
73
+ // prints: 20 scenes: Grasslands 1 and Grasslands 2
74
+ ```
75
+
76
+ A test (`web/test/readmeExamples.test.ts`) runs this block in Node against the built package and compares what it prints with the `// prints:` line.
77
+
78
+ Parsed descriptions are DOS byte strings, not Unicode. For a game name outside DOS use
79
+ `gameTitle(game)` from `formats/game`: it decodes CP437 and trims decorative menu frames.
80
+ For other DOS labels use `fromCp437`; for DOS text-file bytes use `cp437Text` from `text/cp437`,
81
+ which retains ASCII controls so the caller can handle line endings and DOS EOF. Neither API
82
+ is for already decoded Unicode, JSON, HTML or UTF-8 text. Keep the parsed fields and original
83
+ bytes for DOS rendering and round trips; convert only at the presentation boundary.
84
+
85
+ ## Native player screens
86
+
87
+ The package includes the complete DOS player front end. The example above shows
88
+ its title images or animation and menu before play. The menu offers instructions,
89
+ storyline, credits, top scores and quitting; during play the native function keys
90
+ open help, inventory, load and save screens. Include the files named by the game's
91
+ `.GAM` file, along with any saved games or high scores you want to keep.
92
+
93
+ `runGame` uses these screens by default. For a direct play test, pass
94
+ `{ skipTitle: true }` to start a new game at its initial scene link; its normal
95
+ ending screens and menu still follow when play ends. `RunOptions.player` accepts
96
+ DOS player customizations for words, colors, frames, positions, timing and function
97
+ keys. `gmHelp` optionally supplies Game-Maker's own help text before the game's
98
+ instructions. See the exported `RunOptions` type for these settings and callbacks.
99
+
100
+ Observe actual life loss with `onLifeLost`, including the last life, or the final
101
+ result and score with `onPlayEnd`. Recorded demo playback does not emit either
102
+ callback. Persist `GameFiles.onChange` to retain save files and high scores between
103
+ visits.
104
+
54
105
  The source is MIT licensed; see `LICENSE` and `NOTICE.md` for credits.
55
106
 
56
- ## Publishing
107
+ Hosts can call `machine.audio.setSoundCard(enabled)` to change the optional sound-card
108
+ output during play. Removing it pauses FM and cuts digital effects without stopping the
109
+ PC speaker. Restoring it resumes the loaded song, or starts the latest song requested
110
+ while absent. `soundCardEnabled` reports the current card state; returning the card
111
+ preserves a disabled music preference. An explicit song stop remains stopped. Normal DOS-compatible startup
112
+ behavior is unchanged unless the host opts into live card control.
113
+ Set the audio card state before `runGame` and call the same method for later switches;
114
+ effects then use the PC speaker while the card is absent. `RunOptions.noSoundCard` retains
115
+ the original startup-only detection, including its fixed speaker-only choice for that run.
116
+
117
+ ## Incremental picture decoding
118
+
119
+ `formats/jpeg` exports `decodePartialJpeg(bytes)` for baseline and progressive Huffman JPEGs.
120
+ Pass only the bytes received so far. `shown` marks pixels with completed luminance/DC blocks;
121
+ `pixels` holds grayscale or RGB data. A partial first progressive scan reveals only its
122
+ completed blocks; later scans retain the previous coefficients until each MCU is complete.
123
+ Restart markers preserve already decoded MCUs. `scans` counts fully delimited scans and
124
+ `complete` reports an end-of-image marker. The decoder does not invent data for unread blocks.
125
+
126
+ `RunOptions.onLifeLost` observes each player death, including the final life.
127
+ `onPlayEnd(score, how, gained)` reports `won`, `lost`, or `quit` once a play ends.
128
+ Neither callback runs during demo or walkthrough playback. These callbacks do not
129
+ change DOS gameplay or the game files.
130
+
131
+ `Machine.onItem` is an observer hook: `showGraphic` calls it as each picture, cartoon, text page or slide list begins (the walkthrough player's item marks). It never changes timing or behavior.
132
+
133
+ ## Subpath exports
134
+
135
+ Everything is also importable from its own subpath, for example `import { parseBlocks } from '@lewismoten/gamemaker-sandbox-engine/formats/blocks'`, so a Node script or a bundler can take one module without the player:
136
+
137
+ - `formats/*`: readers and writers for the DOS files: `blocks`, `character`, `cmf`, `cmfEdit`, `cmfPreview`, `cmfSong`, `fli`, `fliFrames`, `fliLimits`, `game`, `gif`, `gifProgressive`, `graphicScripts`, `highscore`, `jpeg`, `map`, `monster`, `palette`, `recording`, `sound`, `voc`, and `binary` (byte helpers).
138
+ - `engine/*`: the PLAYGAME port: `machine`, `game`, `world`, `scene`, `scroll`, `render`, `showGraphic`, `menus`, `monsters`, `character`, `keys`, `funcKeys`, `touch`, `recorder`, `playGain`, `playerLook`, `slowPc`.
139
+ - `audio/*`: OPL2 and CMF playback: `opl`, `cmfPlayer`, `cmfLoad`, `engine`, `mix`, `output`, `context`.
140
+ - `gfx/*`: `screen`, `font8x8`, `cp437Glyphs`, `colorVision` (color-vision simulation matrices); `text/cp437`.
141
+ - `input/*`: `keyboard`, `scancodes`, `chords`, `deskMouse`.
142
+ - `core/gameFiles` (`GameFiles`) and `player/*` (`fileExtensions`, `patchRules`, `patchTable`).
143
+
144
+ ## Supported environments
145
+
146
+ Playback needs a browser with canvas, Web Audio and keyboard events (the `Machine` class owns them). The file-format, decoding and simulation modules have no DOM dependency and also run in Node.js 22.12 or later, which is the version the package declares in `engines`. It ships as ES modules with TypeScript declarations.
57
147
 
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.
148
+ ## Developing
62
149
 
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.
150
+ This package's source lives in [the project's repository](https://github.com/lewismoten/GameMaker-SandboxEdition), in `web/packages/engine`. The folder holds all its source in `src/`, plus its build configuration, license and notices. It can be copied outside the repository: run `npm install` and `npm run verify` there to build and test a packed consumer. Inside the repository, run `npm ci` at the root, then `npm run build -w @lewismoten/gamemaker-sandbox-engine`. Run `npm pack` in the package folder to inspect a local distribution. Tarballs are build artifacts and should not be committed. The [monorepo guide](https://github.com/lewismoten/GameMaker-SandboxEdition/blob/master/docs/monorepo.md) covers the workspace layout, and the [package release guide](https://github.com/lewismoten/GameMaker-SandboxEdition/blob/master/docs/package-releases.md) covers publishing.
@@ -1,4 +1,4 @@
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];
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, c = 96, D = [0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 16, 17, 18, 19, 20, 21];
2
2
  class b {
3
3
  am = !1;
4
4
  vib = !1;
@@ -17,7 +17,7 @@ class b {
17
17
  phase = 0;
18
18
  // cycles
19
19
  stage = 0;
20
- envDb = l;
20
+ envDb = c;
21
21
  out = 0;
22
22
  prevOut = 0;
23
23
  keyOn() {
@@ -60,11 +60,11 @@ class b {
60
60
  default:
61
61
  return;
62
62
  }
63
- this.envDb >= l && (this.envDb = l, this.stage = 0);
63
+ this.envDb >= c && (this.envDb = c, this.stage = 0);
64
64
  }
65
65
  decayDbPerSecond(t, s) {
66
66
  const e = this.rate(t, s);
67
- return e === 0 ? 0 : l / (39.28 / Math.pow(2, (e - 4) / 4));
67
+ return e === 0 ? 0 : c / (39.28 / Math.pow(2, (e - 4) / 4));
68
68
  }
69
69
  /** Waveform value at phase (cycles): 0 sine, 1 half sine, 2 absolute sine, 3 pulse (quarter) sine. */
70
70
  wave01(t) {
@@ -119,7 +119,7 @@ class P {
119
119
  for (let t = 0; t < 256; t++) this.write(t, 0);
120
120
  for (const t of this.channels)
121
121
  for (const s of t.ops)
122
- s.stage = 0, s.envDb = l;
122
+ s.stage = 0, s.envDb = c;
123
123
  }
124
124
  read(t) {
125
125
  return this.regs[t & 255];
@@ -198,14 +198,14 @@ class P {
198
198
  const r = this.noise & 1 ? 1 : -1;
199
199
  let n = 0;
200
200
  const f = this.rhythm ? 6 : 9;
201
- for (let c = 0; c < f; c++) n += this.channelSample(this.channels[c], e, i, a);
201
+ for (let l = 0; l < f; l++) n += this.channelSample(this.channels[l], e, i, a);
202
202
  this.rhythm && (n += this.rhythmSample(e, i, a, r)), t[h] += n * s;
203
203
  }
204
204
  }
205
205
  /** Linear amplitude from the summed attenuation: envelope + TL (0.75 dB steps) + KSL + tremolo. */
206
206
  opLevel(t, s, e) {
207
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);
208
+ return h >= c ? 0 : Math.pow(10, -h / 20);
209
209
  }
210
210
  advance(t, s, e, h) {
211
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));
@@ -227,18 +227,18 @@ class P {
227
227
  rhythmSample(t, s, e, h) {
228
228
  const [i, o, a] = [this.channels[6], this.channels[7], this.channels[8]];
229
229
  let r = this.channelSample(i, t, s, e) * 2;
230
- const [n, f] = o.ops, [c, p] = a.ops;
230
+ const [n, f] = o.ops, [l, p] = a.ops;
231
231
  for (const [w, k] of [
232
232
  [o, n],
233
233
  [o, f],
234
- [a, c],
234
+ [a, l],
235
235
  [a, p]
236
236
  ])
237
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;
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), l.stage !== 0 && (r += l.wave01(l.phase) * this.opLevel(a, l, 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
239
  }
240
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 = {
241
+ const y = [0, 1, 2, 8, 9, 10, 16, 17, 18], T = 1193180 / Math.floor(65535 / 8), d = { 11: 16, 12: 8, 13: 4, 14: 2, 15: 1 }, u = {
242
242
  11: { ch: 6, ops: ["mod", "car"] },
243
243
  12: { ch: 7, ops: ["car"] },
244
244
  13: { ch: 8, ops: ["mod"] },
@@ -362,8 +362,8 @@ class K {
362
362
  this.programs[t] = s;
363
363
  const e = this.instruments[s];
364
364
  if (e) {
365
- if (this.rhythm && d[t]) {
366
- const h = d[t];
365
+ if (this.rhythm && u[t]) {
366
+ const h = u[t];
367
367
  h.ops.length === 2 ? this.loadVoice(h.ch, e) : this.loadOperator(y[h.ch] + (h.ops[0] === "car" ? 3 : 0), e, 0);
368
368
  return;
369
369
  }
@@ -405,7 +405,7 @@ class K {
405
405
  const e = this.instruments[this.programs[t]];
406
406
  if (!e) return;
407
407
  let h, i;
408
- const o = this.rhythm ? d[t] : void 0;
408
+ const o = this.rhythm ? u[t] : void 0;
409
409
  if (o && o.ops.length === 1)
410
410
  h = y[o.ch] + (o.ops[0] === "car" ? 3 : 0), i = e[2];
411
411
  else {
@@ -417,17 +417,17 @@ class K {
417
417
  this.opl.write(64 + h, i & 192 | a);
418
418
  }
419
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;
420
+ if (this.applyVelocity(t, e), this.rhythm && d[t]) {
421
+ const h = d[t];
422
+ this.setPitch(u[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
423
  return;
424
424
  }
425
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
426
  }
427
427
  noteOff(t, s) {
428
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);
429
+ if (this.notes[t] = -1, this.rhythm && d[t]) {
430
+ this.rhythmKeys &= ~d[t], this.opl.write(189, 224 | this.rhythmKeys);
431
431
  return;
432
432
  }
433
433
  t < this.melodyVoices() && this.opl.write(176 + t, this.opl.read(176 + t) & -33);
@@ -438,13 +438,15 @@ class L extends AudioWorkletProcessor {
438
438
  opl = new P(sampleRate);
439
439
  player = new K(this.opl);
440
440
  untilTick = 0;
441
+ /** Samples left of a hold: the chip renders, the sequencer does not move. */
442
+ held = 0;
441
443
  paused = !1;
442
444
  ended = !1;
443
445
  samplesPerTick = sampleRate / T;
444
446
  constructor() {
445
447
  super(), this.port.onmessage = (t) => {
446
448
  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);
449
+ s.type === "load" ? (this.player.load(s.music, s.instruments), this.untilTick = 0, this.held = 0) : s.type === "hold" ? this.held += s.ms * sampleRate / 1e3 : s.type === "stop" ? this.player.stop() : s.type === "pause" ? this.paused = s.paused : s.type === "end" && (this.ended = !0);
448
450
  };
449
451
  }
450
452
  process(t, s) {
@@ -453,7 +455,7 @@ class L extends AudioWorkletProcessor {
453
455
  if (!e || this.paused) return !0;
454
456
  let h = 0;
455
457
  for (; h < e.length; ) {
456
- this.untilTick <= 0 && (this.player.tick(), this.untilTick += this.samplesPerTick);
458
+ this.untilTick <= 0 && (this.held > 0 ? this.held -= this.samplesPerTick : this.player.tick(), this.untilTick += this.samplesPerTick);
457
459
  const i = Math.min(e.length - h, Math.ceil(this.untilTick));
458
460
  this.opl.render(e.subarray(h, h + i)), this.untilTick -= i, h += i;
459
461
  }
@@ -0,0 +1,10 @@
1
+ import { isCmf as r, parseCmf as o, CMF_GAMEMAKER_CLOCK as m } from "../formats/cmf.js";
2
+ function c(i) {
3
+ if (!i || !r(i) || (i[36] | i[37] << 8) * 16 & 32768) return;
4
+ const n = o(i);
5
+ if (n.clockTicks === m)
6
+ return { music: n.music, instruments: n.instruments };
7
+ }
8
+ export {
9
+ c as loadSong
10
+ };
@@ -0,0 +1,201 @@
1
+ import { MODULATOR_OFFSET as r } from "./opl.js";
2
+ const m = 1193180 / Math.floor(65535 / 8), f = { 11: 16, 12: 8, 13: 4, 14: 2, 15: 1 }, l = {
3
+ 11: { ch: 6, ops: ["mod", "car"] },
4
+ 12: { ch: 7, ops: ["car"] },
5
+ 13: { ch: 8, ops: ["mod"] },
6
+ 14: { ch: 8, ops: ["car"] },
7
+ 15: { ch: 7, ops: ["mod"] }
8
+ };
9
+ class n {
10
+ opl;
11
+ music = new Uint8Array(0);
12
+ instruments = [];
13
+ // PlayIt's statics: read position in the music block, ticks left before the next
14
+ // event, and the running status (channel and 8 = note off / 9 = note on).
15
+ offset = 0;
16
+ wait = 0;
17
+ channel = 0;
18
+ funct = 8;
19
+ rhythm = !1;
20
+ rhythmKeys = 0;
21
+ // Note sounding on each MIDI channel (-1 none), so a note off only releases its own note.
22
+ notes = new Int16Array(16).fill(-1);
23
+ programs = new Uint8Array(16);
24
+ playing = !1;
25
+ constructor(t) {
26
+ this.opl = t, this.init();
27
+ }
28
+ /** sbfd_init: enable waveform select, melody mode (0xBD = deep tremolo and vibrato, rhythm off). */
29
+ init() {
30
+ this.opl.reset(), this.opl.write(1, 32), this.opl.write(8, 0), this.opl.write(189, 192);
31
+ }
32
+ load(t, s) {
33
+ this.stop(), this.init(), this.music = t, this.instruments = s, this.rhythm = !1, this.rhythmKeys = 0;
34
+ for (let i = 0; i < 16; i++) this.programChange(i, i);
35
+ this.offset = 0, this.playing = t.length > 0;
36
+ }
37
+ /** sbfd_music_off + rewind. */
38
+ stop() {
39
+ this.allNotesOff(), this.offset = 0, this.playing = !1;
40
+ }
41
+ allNotesOff() {
42
+ for (let t = 0; t < 9; t++) this.opl.write(176 + t, this.opl.read(176 + t) & -33);
43
+ this.rhythmKeys = 0, this.opl.write(189, 192 | (this.rhythm ? 32 : 0)), this.notes.fill(-1);
44
+ }
45
+ /**
46
+ * *(APOS) in SBMUSIC.C. PlayIt read sbmusic+offset with no bounds check, so past the end of
47
+ * the farmalloc'd music block it read whatever heap memory followed. Every CMF ends with the
48
+ * FF 2F 00 end marker, which restarts the song before the read gets there; only a truncated
49
+ * file runs off the end, and for that the port reads 0xFF, which takes the same path as the
50
+ * end marker (sbfd_music_off, offset = 0).
51
+ */
52
+ byte(t) {
53
+ return this.music[t] ?? 255;
54
+ }
55
+ /**
56
+ * getpaws() in SBMUSIC.C: reads a MIDI variable length number (7 bits per byte, high bit
57
+ * set on every byte but the last), with no limit on the number of bytes:
58
+ * do { offset++; value = (value << 7) | ((c=*(APOS)) & 0x07F); } while (c & 0x80);
59
+ * value is an unsigned long, so bits shifted past 32 are lost (>>> 0). The one difference:
60
+ * the loop stops at the end of the data, where DOS read on into unknown memory.
61
+ */
62
+ getPaws() {
63
+ let t = this.byte(this.offset);
64
+ if (t & 128) {
65
+ t &= 127;
66
+ let s;
67
+ do
68
+ this.offset++, s = this.byte(this.offset), t = (t << 7 | s & 127) >>> 0;
69
+ while (s & 128 && this.offset < this.music.length);
70
+ }
71
+ return this.offset++, t;
72
+ }
73
+ /** One timer tick of PlayIt(). */
74
+ tick() {
75
+ if (!this.playing) return;
76
+ this.offset === 0 && (this.wait = 1, this.getPaws()), this.wait--;
77
+ let t = 0;
78
+ for (; this.wait <= 0 && t++ < 1e5; ) {
79
+ const s = this.byte(this.offset);
80
+ switch (s & 240) {
81
+ case 144:
82
+ this.channel = s & 15, this.offset++, this.funct = 9;
83
+ break;
84
+ case 128:
85
+ this.channel = s & 15, this.offset++, this.funct = 8;
86
+ break;
87
+ }
88
+ const i = this.byte(this.offset);
89
+ if (i < 128) {
90
+ const h = this.byte(this.offset + 1);
91
+ this.funct === 8 || h === 0 ? this.noteOff(this.channel, i) : this.noteOn(this.channel, i, h), this.offset += 2;
92
+ } else
93
+ switch (i & 240) {
94
+ case 160:
95
+ this.offset += 3;
96
+ break;
97
+ case 176:
98
+ this.byte(this.offset + 1) === 103 && this.setMode(this.byte(this.offset + 2)), this.offset += 3;
99
+ break;
100
+ case 192:
101
+ this.programChange(i & 15, this.byte(this.offset + 1)), this.offset += 2;
102
+ break;
103
+ case 208:
104
+ this.offset += 2;
105
+ break;
106
+ case 224:
107
+ this.offset += 3;
108
+ break;
109
+ case 240:
110
+ i === 247 || i === 240 ? (this.offset++, this.offset += this.getPaws(), this.offset++) : (this.allNotesOff(), this.offset = 0);
111
+ break;
112
+ default:
113
+ this.allNotesOff(), this.offset = 0;
114
+ }
115
+ this.wait = this.getPaws();
116
+ }
117
+ }
118
+ /** sbfd_setmode: switch melody/rhythm mode and reload the drum channels' instruments. */
119
+ setMode(t) {
120
+ 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]);
121
+ }
122
+ programChange(t, s) {
123
+ this.programs[t] = s;
124
+ const i = this.instruments[s];
125
+ if (i) {
126
+ if (this.rhythm && l[t]) {
127
+ const h = l[t];
128
+ h.ops.length === 2 ? this.loadVoice(h.ch, i) : this.loadOperator(r[h.ch] + (h.ops[0] === "car" ? 3 : 0), i, 0);
129
+ return;
130
+ }
131
+ t < this.melodyVoices() && this.loadVoice(t, i);
132
+ }
133
+ }
134
+ melodyVoices() {
135
+ return this.rhythm ? 6 : 9;
136
+ }
137
+ /**
138
+ * Writes one operator from a 16 byte SBI instrument record, whose bytes alternate
139
+ * modulator/carrier: 0/1 AM-VIB-EG-KSR-MULT, 2/3 KSL-TL, 4/5 AR-DR, 6/7 SL-RR, 8/9 waveform.
140
+ */
141
+ loadOperator(t, s, i) {
142
+ this.opl.write(32 + t, s[0 + i]), this.opl.write(64 + t, s[2 + i]), this.opl.write(96 + t, s[4 + i]), this.opl.write(128 + t, s[6 + i]), this.opl.write(224 + t, s[8 + i]);
143
+ }
144
+ loadVoice(t, s) {
145
+ const i = r[t];
146
+ this.loadOperator(i, s, 0), this.loadOperator(i + 3, s, 1), this.opl.write(192 + t, s[10]);
147
+ }
148
+ /**
149
+ * MIDI note -> OPL frequency: picks the lowest block (octave) whose F-number fits in 10 bits,
150
+ * which keeps the most pitch resolution (Hz = fnum * 49716 / 2^(20 - block)).
151
+ */
152
+ setPitch(t, s, i) {
153
+ const h = 440 * Math.pow(2, (s - 69) / 12);
154
+ let o, e = 0;
155
+ for (o = 0; o < 8 && (e = Math.round(h * Math.pow(2, 20 - o) / 49716), !(e < 1024)); o++)
156
+ ;
157
+ o = Math.min(o, 7), e = Math.min(e, 1023), this.opl.write(160 + t, e & 255), this.opl.write(176 + t, (i ? 32 : 0) | o << 2 | e >> 8);
158
+ }
159
+ /**
160
+ * SBFMDRV's loudness by velocity: it scales the instrument's carrier level (TL is an
161
+ * attenuation, 0 loudest .. 63 silent) and keeps the key-scaling bits:
162
+ * TL = 63 - floor((velocity + 128) * (63 - instrumentTL) / 256). So velocity 127 plays
163
+ * the instrument almost as loud as written and velocity 1 about half as loud in TL steps.
164
+ */
165
+ applyVelocity(t, s) {
166
+ const i = this.instruments[this.programs[t]];
167
+ if (!i) return;
168
+ let h, o;
169
+ const e = this.rhythm ? l[t] : void 0;
170
+ if (e && e.ops.length === 1)
171
+ h = r[e.ch] + (e.ops[0] === "car" ? 3 : 0), o = i[2];
172
+ else {
173
+ const a = e ? e.ch : t;
174
+ if (!e && a >= this.melodyVoices()) return;
175
+ h = r[a] + 3, o = i[3];
176
+ }
177
+ const p = 63 - Math.floor((Math.min(127, s) + 128) * (63 - (o & 63)) / 256);
178
+ this.opl.write(64 + h, o & 192 | p);
179
+ }
180
+ noteOn(t, s, i = 127) {
181
+ if (this.applyVelocity(t, i), this.rhythm && f[t]) {
182
+ const h = f[t];
183
+ this.setPitch(l[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;
184
+ return;
185
+ }
186
+ 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);
187
+ }
188
+ noteOff(t, s) {
189
+ if (this.notes[t] === s) {
190
+ if (this.notes[t] = -1, this.rhythm && f[t]) {
191
+ this.rhythmKeys &= ~f[t], this.opl.write(189, 224 | this.rhythmKeys);
192
+ return;
193
+ }
194
+ t < this.melodyVoices() && this.opl.write(176 + t, this.opl.read(176 + t) & -33);
195
+ }
196
+ }
197
+ }
198
+ export {
199
+ n as CmfPlayer,
200
+ m as TICK_RATE
201
+ };
@@ -0,0 +1,75 @@
1
+ let e, i = !1;
2
+ const r = /* @__PURE__ */ new Set(), l = ["pointerup", "touchend", "click", "keydown", "mousedown"];
3
+ function v() {
4
+ if (!(typeof window > "u"))
5
+ return window.AudioContext ?? window.webkitAudioContext;
6
+ }
7
+ function u() {
8
+ try {
9
+ const t = navigator.audioSession;
10
+ t && t.type !== "playback" && (t.type = "playback");
11
+ } catch {
12
+ }
13
+ }
14
+ function w() {
15
+ if (e) return e;
16
+ const t = v();
17
+ if (t) {
18
+ try {
19
+ u(), e = new t({ latencyHint: "interactive" });
20
+ } catch {
21
+ return;
22
+ }
23
+ return d(), e.addEventListener("statechange", () => {
24
+ e && e.state !== "running" && i && document.visibilityState === "visible" && e.resume().catch(() => {
25
+ });
26
+ }), navigator.userActivation?.isActive && s(), e;
27
+ }
28
+ }
29
+ function y() {
30
+ return i && !!e && e.state === "running";
31
+ }
32
+ function k(t) {
33
+ return r.add(t), () => r.delete(t);
34
+ }
35
+ function s() {
36
+ const t = w();
37
+ if (!t) return;
38
+ u();
39
+ const a = !i;
40
+ if (i = !0, t.state !== "running" && t.resume().catch(() => {
41
+ }), a)
42
+ try {
43
+ const n = t.createBufferSource();
44
+ n.buffer = t.createBuffer(1, 1, t.sampleRate), n.connect(t.destination), n.start(0);
45
+ } catch {
46
+ }
47
+ for (const n of [...r])
48
+ try {
49
+ n();
50
+ } catch (f) {
51
+ console.warn(f);
52
+ }
53
+ }
54
+ let c = !1;
55
+ function p(t) {
56
+ t.type === "keydown" && (t.key === "Shift" || t.key === "Meta") || t.isTrusted && (y() || s());
57
+ }
58
+ function o() {
59
+ e && i && e.state !== "running" && document.visibilityState === "visible" && e.resume().catch(() => {
60
+ });
61
+ }
62
+ function d() {
63
+ if (!(c || typeof window > "u")) {
64
+ c = !0;
65
+ for (const t of l) window.addEventListener(t, p, { capture: !0, passive: !0 });
66
+ document.addEventListener("visibilitychange", o), window.addEventListener("pageshow", o), window.addEventListener("focus", o);
67
+ }
68
+ }
69
+ typeof window < "u" && d();
70
+ export {
71
+ w as audioContext,
72
+ k as onSoundUnlocked,
73
+ y as soundUnlocked,
74
+ s as unlock
75
+ };