@lewismoten/gamemaker-sandbox-engine 0.0.0-stage → 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 (129) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +24 -0
  3. package/NOTICE.md +58 -0
  4. package/README.md +149 -2
  5. package/dist/assets/music.worklet-aD2x7RRM.js +466 -0
  6. package/dist/audio/cmfLoad.js +10 -0
  7. package/dist/audio/cmfPlayer.js +201 -0
  8. package/dist/audio/context.js +75 -0
  9. package/dist/audio/engine.js +173 -0
  10. package/dist/audio/mix.js +40 -0
  11. package/dist/audio/opl.js +245 -0
  12. package/dist/audio/output.js +40 -0
  13. package/dist/core/gameFiles.js +89 -0
  14. package/dist/engine/character.js +187 -0
  15. package/dist/engine/funcKeys.js +164 -0
  16. package/dist/engine/game.js +133 -0
  17. package/dist/engine/keys.js +68 -0
  18. package/dist/engine/machine.js +192 -0
  19. package/dist/engine/menus.js +135 -0
  20. package/dist/engine/monsters.js +259 -0
  21. package/dist/engine/playGain.js +10 -0
  22. package/dist/engine/playerLook.js +48 -0
  23. package/dist/engine/recorder.js +127 -0
  24. package/dist/engine/render.js +45 -0
  25. package/dist/engine/scene.js +324 -0
  26. package/dist/engine/scroll.js +79 -0
  27. package/dist/engine/showGraphic.js +107 -0
  28. package/dist/engine/slowPc.js +98 -0
  29. package/dist/engine/slowPcMusic.js +46 -0
  30. package/dist/engine/touch.js +61 -0
  31. package/dist/engine/world.js +205 -0
  32. package/dist/formats/binary.js +120 -0
  33. package/dist/formats/blocks.js +103 -0
  34. package/dist/formats/character.js +83 -0
  35. package/dist/formats/cmf.js +37 -0
  36. package/dist/formats/cmfEdit.js +154 -0
  37. package/dist/formats/cmfPreview.js +89 -0
  38. package/dist/formats/cmfSong.js +190 -0
  39. package/dist/formats/fli.js +340 -0
  40. package/dist/formats/fliFrames.js +34 -0
  41. package/dist/formats/fliLimits.js +54 -0
  42. package/dist/formats/game.js +198 -0
  43. package/dist/formats/gif.js +125 -0
  44. package/dist/formats/gifProgressive.js +99 -0
  45. package/dist/formats/graphicScripts.js +44 -0
  46. package/dist/formats/highscore.js +30 -0
  47. package/dist/formats/jpeg.js +359 -0
  48. package/dist/formats/map.js +38 -0
  49. package/dist/formats/monster.js +69 -0
  50. package/dist/formats/palette.js +58 -0
  51. package/dist/formats/recording.js +17 -0
  52. package/dist/formats/sound.js +44 -0
  53. package/dist/formats/voc.js +303 -0
  54. package/dist/gfx/colorVision.js +45 -0
  55. package/dist/gfx/cp437Glyphs.js +337 -0
  56. package/dist/gfx/font8x8.js +10 -0
  57. package/dist/gfx/screen.js +151 -0
  58. package/dist/index.js +15 -0
  59. package/dist/input/chords.js +46 -0
  60. package/dist/input/deskMouse.js +24 -0
  61. package/dist/input/keyboard.js +82 -0
  62. package/dist/input/scancodes.js +143 -0
  63. package/dist/player/fileExtensions.js +118 -0
  64. package/dist/player/patchRules.js +129 -0
  65. package/dist/player/patchTable.js +1753 -0
  66. package/dist/text/cp437.js +44 -0
  67. package/dist/types/audio/cmfLoad.d.ts +7 -0
  68. package/dist/types/audio/cmfPlayer.d.ts +69 -0
  69. package/dist/types/audio/context.d.ts +12 -0
  70. package/dist/types/audio/engine.d.ts +60 -0
  71. package/dist/types/audio/mix.d.ts +14 -0
  72. package/dist/types/audio/music.worklet.d.ts +22 -0
  73. package/dist/types/audio/opl.d.ts +37 -0
  74. package/dist/types/audio/output.d.ts +13 -0
  75. package/dist/types/core/gameFiles.d.ts +49 -0
  76. package/dist/types/engine/character.d.ts +30 -0
  77. package/dist/types/engine/funcKeys.d.ts +35 -0
  78. package/dist/types/engine/game.d.ts +57 -0
  79. package/dist/types/engine/keys.d.ts +17 -0
  80. package/dist/types/engine/machine.d.ts +103 -0
  81. package/dist/types/engine/menus.d.ts +46 -0
  82. package/dist/types/engine/monsters.d.ts +19 -0
  83. package/dist/types/engine/playGain.d.ts +11 -0
  84. package/dist/types/engine/playerLook.d.ts +44 -0
  85. package/dist/types/engine/recorder.d.ts +67 -0
  86. package/dist/types/engine/render.d.ts +8 -0
  87. package/dist/types/engine/scene.d.ts +188 -0
  88. package/dist/types/engine/scroll.d.ts +24 -0
  89. package/dist/types/engine/showGraphic.d.ts +39 -0
  90. package/dist/types/engine/slowPc.d.ts +124 -0
  91. package/dist/types/engine/slowPcMusic.d.ts +18 -0
  92. package/dist/types/engine/touch.d.ts +33 -0
  93. package/dist/types/engine/world.d.ts +237 -0
  94. package/dist/types/formats/binary.d.ts +44 -0
  95. package/dist/types/formats/blocks.d.ts +52 -0
  96. package/dist/types/formats/character.d.ts +65 -0
  97. package/dist/types/formats/cmf.d.ts +33 -0
  98. package/dist/types/formats/cmfEdit.d.ts +53 -0
  99. package/dist/types/formats/cmfPreview.d.ts +19 -0
  100. package/dist/types/formats/cmfSong.d.ts +74 -0
  101. package/dist/types/formats/fli.d.ts +56 -0
  102. package/dist/types/formats/fliFrames.d.ts +18 -0
  103. package/dist/types/formats/fliLimits.d.ts +13 -0
  104. package/dist/types/formats/game.d.ts +79 -0
  105. package/dist/types/formats/gif.d.ts +15 -0
  106. package/dist/types/formats/gifProgressive.d.ts +46 -0
  107. package/dist/types/formats/graphicScripts.d.ts +18 -0
  108. package/dist/types/formats/highscore.d.ts +10 -0
  109. package/dist/types/formats/jpeg.d.ts +20 -0
  110. package/dist/types/formats/map.d.ts +24 -0
  111. package/dist/types/formats/monster.d.ts +48 -0
  112. package/dist/types/formats/palette.d.ts +15 -0
  113. package/dist/types/formats/recording.d.ts +7 -0
  114. package/dist/types/formats/sound.d.ts +13 -0
  115. package/dist/types/formats/voc.d.ts +13 -0
  116. package/dist/types/gfx/colorVision.d.ts +26 -0
  117. package/dist/types/gfx/cp437Glyphs.d.ts +2 -0
  118. package/dist/types/gfx/font8x8.d.ts +2 -0
  119. package/dist/types/gfx/screen.d.ts +50 -0
  120. package/dist/types/index.d.ts +4 -0
  121. package/dist/types/input/chords.d.ts +22 -0
  122. package/dist/types/input/deskMouse.d.ts +14 -0
  123. package/dist/types/input/keyboard.d.ts +39 -0
  124. package/dist/types/input/scancodes.d.ts +3 -0
  125. package/dist/types/player/fileExtensions.d.ts +44 -0
  126. package/dist/types/player/patchRules.d.ts +24 -0
  127. package/dist/types/player/patchTable.d.ts +83 -0
  128. package/dist/types/text/cp437.d.ts +6 -0
  129. package/package.json +339 -4
@@ -0,0 +1,466 @@
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
+ 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 = c;
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 >= c && (this.envDb = c, this.stage = 0);
64
+ }
65
+ decayDbPerSecond(t, s) {
66
+ const e = this.rate(t, s);
67
+ return e === 0 ? 0 : c / (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 = c;
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 l = 0; l < f; l++) n += this.channelSample(this.channels[l], 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 >= c ? 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, [l, p] = a.ops;
231
+ for (const [w, k] of [
232
+ [o, n],
233
+ [o, f],
234
+ [a, l],
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), 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
+ }
240
+ }
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
+ 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 && u[t]) {
366
+ const h = u[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 ? u[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 && 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
+ 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 && d[t]) {
430
+ this.rhythmKeys &= ~d[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
+ /** Samples left of a hold: the chip renders, the sequencer does not move. */
442
+ held = 0;
443
+ paused = !1;
444
+ ended = !1;
445
+ samplesPerTick = sampleRate / T;
446
+ constructor() {
447
+ super(), this.port.onmessage = (t) => {
448
+ const s = t.data;
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);
450
+ };
451
+ }
452
+ process(t, s) {
453
+ if (this.ended) return !1;
454
+ const e = s[0][0];
455
+ if (!e || this.paused) return !0;
456
+ let h = 0;
457
+ for (; h < e.length; ) {
458
+ this.untilTick <= 0 && (this.held > 0 ? this.held -= this.samplesPerTick : this.player.tick(), this.untilTick += this.samplesPerTick);
459
+ const i = Math.min(e.length - h, Math.ceil(this.untilTick));
460
+ this.opl.render(e.subarray(h, h + i)), this.untilTick -= i, h += i;
461
+ }
462
+ for (let i = 1; i < s[0].length; i++) s[0][i].set(e);
463
+ return !0;
464
+ }
465
+ }
466
+ registerProcessor("gm-music", L);
@@ -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
+ };