sci2-ts 0.0.0-stage → 0.2.2
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/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +113 -2
- package/apps/viewer/editor.html +43 -0
- package/apps/viewer/explore.html +27 -0
- package/apps/viewer/index.html +26 -0
- package/apps/viewer/package.json +27 -0
- package/apps/viewer/play.html +26 -0
- package/apps/viewer/plugin.d.ts +18 -0
- package/apps/viewer/plugin.js +75 -0
- package/apps/viewer/plugin.ts +84 -0
- package/apps/viewer/src/adlib.d.ts +20 -0
- package/apps/viewer/src/adlib.js +55 -0
- package/apps/viewer/src/adlib.ts +62 -0
- package/apps/viewer/src/app.d.ts +5 -0
- package/apps/viewer/src/app.js +5 -0
- package/apps/viewer/src/app.ts +5 -0
- package/apps/viewer/src/asset-index.d.ts +22 -0
- package/apps/viewer/src/asset-index.js +74 -0
- package/apps/viewer/src/asset-index.ts +90 -0
- package/apps/viewer/src/audio.d.ts +28 -0
- package/apps/viewer/src/audio.js +102 -0
- package/apps/viewer/src/audio.ts +110 -0
- package/apps/viewer/src/editor-handles.d.ts +63 -0
- package/apps/viewer/src/editor-handles.js +332 -0
- package/apps/viewer/src/editor-handles.ts +368 -0
- package/apps/viewer/src/editor-languages.d.ts +23 -0
- package/apps/viewer/src/editor-languages.js +157 -0
- package/apps/viewer/src/editor-languages.ts +135 -0
- package/apps/viewer/src/editor.css +213 -0
- package/apps/viewer/src/editor.d.ts +1 -0
- package/apps/viewer/src/editor.js +393 -0
- package/apps/viewer/src/editor.ts +412 -0
- package/apps/viewer/src/explore/classes.d.ts +7 -0
- package/apps/viewer/src/explore/classes.js +63 -0
- package/apps/viewer/src/explore/classes.ts +61 -0
- package/apps/viewer/src/explore/context.d.ts +33 -0
- package/apps/viewer/src/explore/context.js +96 -0
- package/apps/viewer/src/explore/context.ts +124 -0
- package/apps/viewer/src/explore/explore.css +93 -0
- package/apps/viewer/src/explore/explore.d.ts +1 -0
- package/apps/viewer/src/explore/explore.js +102 -0
- package/apps/viewer/src/explore/explore.ts +104 -0
- package/apps/viewer/src/explore/inspect.d.ts +5 -0
- package/apps/viewer/src/explore/inspect.js +208 -0
- package/apps/viewer/src/explore/inspect.ts +279 -0
- package/apps/viewer/src/explore/live.d.ts +22 -0
- package/apps/viewer/src/explore/live.js +318 -0
- package/apps/viewer/src/explore/live.ts +365 -0
- package/apps/viewer/src/explore/map.d.ts +11 -0
- package/apps/viewer/src/explore/map.js +182 -0
- package/apps/viewer/src/explore/map.ts +193 -0
- package/apps/viewer/src/files.d.ts +21 -0
- package/apps/viewer/src/files.js +69 -0
- package/apps/viewer/src/files.ts +72 -0
- package/apps/viewer/src/game-profile.d.ts +19 -0
- package/apps/viewer/src/game-profile.js +4 -0
- package/apps/viewer/src/game-profile.ts +26 -0
- package/apps/viewer/src/main.d.ts +1 -0
- package/apps/viewer/src/main.js +473 -0
- package/apps/viewer/src/main.ts +527 -0
- package/apps/viewer/src/music.d.ts +14 -0
- package/apps/viewer/src/music.js +32 -0
- package/apps/viewer/src/music.ts +34 -0
- package/apps/viewer/src/opl-worklet.d.ts +1 -0
- package/apps/viewer/src/opl-worklet.js +47 -0
- package/apps/viewer/src/opl-worklet.ts +55 -0
- package/apps/viewer/src/overlay.d.ts +31 -0
- package/apps/viewer/src/overlay.js +103 -0
- package/apps/viewer/src/overlay.ts +123 -0
- package/apps/viewer/src/play.css +14 -0
- package/apps/viewer/src/play.d.ts +1 -0
- package/apps/viewer/src/play.js +179 -0
- package/apps/viewer/src/play.ts +177 -0
- package/apps/viewer/src/saves.d.ts +10 -0
- package/apps/viewer/src/saves.js +50 -0
- package/apps/viewer/src/saves.ts +51 -0
- package/apps/viewer/src/session.d.ts +91 -0
- package/apps/viewer/src/session.js +301 -0
- package/apps/viewer/src/session.ts +321 -0
- package/apps/viewer/src/sound-preview.d.ts +7 -0
- package/apps/viewer/src/sound-preview.js +214 -0
- package/apps/viewer/src/sound-preview.ts +228 -0
- package/apps/viewer/src/style.css +64 -0
- package/apps/viewer/vite.config.ts +25 -0
- package/bin/sci-ts.mjs +9 -0
- package/lib/994.sc +49 -0
- package/lib/995.sc +53 -0
- package/lib/996.sc +299 -0
- package/lib/997.sc +198 -0
- package/lib/998.sc +338 -0
- package/lib/999.sc +189 -0
- package/lib/resources.d.ts +2 -0
- package/lib/resources.js +84 -0
- package/lib/resources.ts +88 -0
- package/lib/system.sh +62 -0
- package/package.json +75 -4
- package/packages/content/room.schema.json +267 -0
- package/packages/content/src/index.d.ts +3 -0
- package/packages/content/src/index.js +3 -0
- package/packages/content/src/index.ts +3 -0
- package/packages/content/src/room.d.ts +111 -0
- package/packages/content/src/room.js +886 -0
- package/packages/content/src/room.ts +1011 -0
- package/packages/content/src/target.d.ts +80 -0
- package/packages/content/src/target.js +1 -0
- package/packages/content/src/target.ts +60 -0
- package/packages/content/src/yarn.d.ts +90 -0
- package/packages/content/src/yarn.js +214 -0
- package/packages/content/src/yarn.ts +261 -0
- package/packages/sci/src/analysis/game.d.ts +90 -0
- package/packages/sci/src/analysis/game.js +221 -0
- package/packages/sci/src/analysis/game.ts +305 -0
- package/packages/sci/src/analysis/runtime.d.ts +58 -0
- package/packages/sci/src/analysis/runtime.js +133 -0
- package/packages/sci/src/analysis/runtime.ts +163 -0
- package/packages/sci/src/audio/adlib.d.ts +73 -0
- package/packages/sci/src/audio/adlib.js +280 -0
- package/packages/sci/src/audio/adlib.ts +329 -0
- package/packages/sci/src/audio/index.d.ts +43 -0
- package/packages/sci/src/audio/index.js +99 -0
- package/packages/sci/src/audio/index.ts +122 -0
- package/packages/sci/src/audio/midi.d.ts +92 -0
- package/packages/sci/src/audio/midi.js +242 -0
- package/packages/sci/src/audio/midi.ts +262 -0
- package/packages/sci/src/audio/opl2.d.ts +74 -0
- package/packages/sci/src/audio/opl2.js +283 -0
- package/packages/sci/src/audio/opl2.ts +283 -0
- package/packages/sci/src/audio/sol.d.ts +32 -0
- package/packages/sci/src/audio/sol.js +81 -0
- package/packages/sci/src/audio/sol.ts +100 -0
- package/packages/sci/src/audio/write.d.ts +35 -0
- package/packages/sci/src/audio/write.js +91 -0
- package/packages/sci/src/audio/write.ts +108 -0
- package/packages/sci/src/gfx/cel.d.ts +36 -0
- package/packages/sci/src/gfx/cel.js +82 -0
- package/packages/sci/src/gfx/cel.ts +110 -0
- package/packages/sci/src/gfx/palette.d.ts +24 -0
- package/packages/sci/src/gfx/palette.js +49 -0
- package/packages/sci/src/gfx/palette.ts +56 -0
- package/packages/sci/src/gfx/pic-file.d.ts +46 -0
- package/packages/sci/src/gfx/pic-file.js +114 -0
- package/packages/sci/src/gfx/pic-file.ts +155 -0
- package/packages/sci/src/gfx/pic.d.ts +27 -0
- package/packages/sci/src/gfx/pic.js +34 -0
- package/packages/sci/src/gfx/pic.ts +53 -0
- package/packages/sci/src/gfx/quantize.d.ts +42 -0
- package/packages/sci/src/gfx/quantize.js +199 -0
- package/packages/sci/src/gfx/quantize.ts +218 -0
- package/packages/sci/src/gfx/view-file.d.ts +62 -0
- package/packages/sci/src/gfx/view-file.js +182 -0
- package/packages/sci/src/gfx/view-file.ts +233 -0
- package/packages/sci/src/gfx/view.d.ts +28 -0
- package/packages/sci/src/gfx/view.js +45 -0
- package/packages/sci/src/gfx/view.ts +62 -0
- package/packages/sci/src/index.d.ts +38 -0
- package/packages/sci/src/index.js +38 -0
- package/packages/sci/src/index.ts +38 -0
- package/packages/sci/src/motion/pathfinding.d.ts +40 -0
- package/packages/sci/src/motion/pathfinding.js +140 -0
- package/packages/sci/src/motion/pathfinding.ts +147 -0
- package/packages/sci/src/resource/archive.d.ts +16 -0
- package/packages/sci/src/resource/archive.js +51 -0
- package/packages/sci/src/resource/archive.ts +56 -0
- package/packages/sci/src/resource/lzs.d.ts +12 -0
- package/packages/sci/src/resource/lzs.js +64 -0
- package/packages/sci/src/resource/lzs.ts +66 -0
- package/packages/sci/src/resource/manager.d.ts +45 -0
- package/packages/sci/src/resource/manager.js +158 -0
- package/packages/sci/src/resource/manager.ts +184 -0
- package/packages/sci/src/resource/map.d.ts +18 -0
- package/packages/sci/src/resource/map.js +34 -0
- package/packages/sci/src/resource/map.ts +44 -0
- package/packages/sci/src/resource/types.d.ts +37 -0
- package/packages/sci/src/resource/types.js +45 -0
- package/packages/sci/src/resource/types.ts +57 -0
- package/packages/sci/src/script/assembly.d.ts +67 -0
- package/packages/sci/src/script/assembly.js +748 -0
- package/packages/sci/src/script/assembly.ts +714 -0
- package/packages/sci/src/script/compiler.d.ts +75 -0
- package/packages/sci/src/script/compiler.js +993 -0
- package/packages/sci/src/script/compiler.ts +1040 -0
- package/packages/sci/src/script/disasm.d.ts +81 -0
- package/packages/sci/src/script/disasm.js +318 -0
- package/packages/sci/src/script/disasm.ts +323 -0
- package/packages/sci/src/script/kernel-names.d.ts +6 -0
- package/packages/sci/src/script/kernel-names.js +165 -0
- package/packages/sci/src/script/kernel-names.ts +165 -0
- package/packages/sci/src/script/opcodes.d.ts +22 -0
- package/packages/sci/src/script/opcodes.js +45 -0
- package/packages/sci/src/script/opcodes.ts +72 -0
- package/packages/sci/src/script/script.d.ts +56 -0
- package/packages/sci/src/script/script.js +63 -0
- package/packages/sci/src/script/script.ts +111 -0
- package/packages/sci/src/script/sexpr.d.ts +45 -0
- package/packages/sci/src/script/sexpr.js +151 -0
- package/packages/sci/src/script/sexpr.ts +139 -0
- package/packages/sci/src/script/vocab.d.ts +15 -0
- package/packages/sci/src/script/vocab.js +44 -0
- package/packages/sci/src/script/vocab.ts +48 -0
- package/packages/sci/src/script/world.d.ts +25 -0
- package/packages/sci/src/script/world.js +65 -0
- package/packages/sci/src/script/world.ts +70 -0
- package/packages/sci/src/text/font.d.ts +37 -0
- package/packages/sci/src/text/font.js +103 -0
- package/packages/sci/src/text/font.ts +125 -0
- package/packages/sci/src/text/message.d.ts +69 -0
- package/packages/sci/src/text/message.js +171 -0
- package/packages/sci/src/text/message.ts +205 -0
- package/packages/sci/src/vm/kernels/arrays.d.ts +19 -0
- package/packages/sci/src/vm/kernels/arrays.js +182 -0
- package/packages/sci/src/vm/kernels/arrays.ts +164 -0
- package/packages/sci/src/vm/kernels/audio.d.ts +70 -0
- package/packages/sci/src/vm/kernels/audio.js +347 -0
- package/packages/sci/src/vm/kernels/audio.ts +349 -0
- package/packages/sci/src/vm/kernels/core.d.ts +5 -0
- package/packages/sci/src/vm/kernels/core.js +87 -0
- package/packages/sci/src/vm/kernels/core.ts +91 -0
- package/packages/sci/src/vm/kernels/graphics.d.ts +141 -0
- package/packages/sci/src/vm/kernels/graphics.js +467 -0
- package/packages/sci/src/vm/kernels/graphics.ts +502 -0
- package/packages/sci/src/vm/kernels/index.d.ts +8 -0
- package/packages/sci/src/vm/kernels/index.js +29 -0
- package/packages/sci/src/vm/kernels/index.ts +32 -0
- package/packages/sci/src/vm/kernels/lists.d.ts +6 -0
- package/packages/sci/src/vm/kernels/lists.js +127 -0
- package/packages/sci/src/vm/kernels/lists.ts +111 -0
- package/packages/sci/src/vm/kernels/motion.d.ts +7 -0
- package/packages/sci/src/vm/kernels/motion.js +204 -0
- package/packages/sci/src/vm/kernels/motion.ts +194 -0
- package/packages/sci/src/vm/kernels/palette.d.ts +82 -0
- package/packages/sci/src/vm/kernels/palette.js +292 -0
- package/packages/sci/src/vm/kernels/palette.ts +303 -0
- package/packages/sci/src/vm/kernels/saves.d.ts +45 -0
- package/packages/sci/src/vm/kernels/saves.js +123 -0
- package/packages/sci/src/vm/kernels/saves.ts +149 -0
- package/packages/sci/src/vm/kernels/system.d.ts +62 -0
- package/packages/sci/src/vm/kernels/system.js +110 -0
- package/packages/sci/src/vm/kernels/system.ts +128 -0
- package/packages/sci/src/vm/kernels/text.d.ts +13 -0
- package/packages/sci/src/vm/kernels/text.js +338 -0
- package/packages/sci/src/vm/kernels/text.ts +328 -0
- package/packages/sci/src/vm/memory.d.ts +138 -0
- package/packages/sci/src/vm/memory.js +168 -0
- package/packages/sci/src/vm/memory.ts +266 -0
- package/packages/sci/src/vm/replace.d.ts +11 -0
- package/packages/sci/src/vm/replace.js +14 -0
- package/packages/sci/src/vm/replace.ts +16 -0
- package/packages/sci/src/vm/savegame.d.ts +89 -0
- package/packages/sci/src/vm/savegame.js +161 -0
- package/packages/sci/src/vm/savegame.ts +188 -0
- package/packages/sci/src/vm/value.d.ts +17 -0
- package/packages/sci/src/vm/value.js +12 -0
- package/packages/sci/src/vm/value.ts +25 -0
- package/packages/sci/src/vm/vm.d.ts +155 -0
- package/packages/sci/src/vm/vm.js +1053 -0
- package/packages/sci/src/vm/vm.ts +914 -0
- package/tools/adlib-render.d.ts +1 -0
- package/tools/adlib-render.js +49 -0
- package/tools/adlib-render.ts +42 -0
- package/tools/analyze.d.ts +1 -0
- package/tools/analyze.js +25 -0
- package/tools/analyze.ts +25 -0
- package/tools/art/build.d.ts +34 -0
- package/tools/art/build.js +175 -0
- package/tools/art/build.ts +153 -0
- package/tools/art.d.ts +1 -0
- package/tools/art.js +31 -0
- package/tools/art.ts +29 -0
- package/tools/asm.d.ts +1 -0
- package/tools/asm.js +61 -0
- package/tools/asm.ts +55 -0
- package/tools/cli.d.ts +1 -0
- package/tools/cli.js +99 -0
- package/tools/cli.ts +100 -0
- package/tools/editor-server.d.ts +29 -0
- package/tools/editor-server.js +162 -0
- package/tools/editor-server.ts +176 -0
- package/tools/explore.d.ts +1 -0
- package/tools/explore.js +305 -0
- package/tools/explore.ts +250 -0
- package/tools/game/audio.d.ts +19 -0
- package/tools/game/audio.js +143 -0
- package/tools/game/audio.ts +130 -0
- package/tools/game/build.d.ts +46 -0
- package/tools/game/build.js +259 -0
- package/tools/game/build.ts +283 -0
- package/tools/game/defaults.d.ts +26 -0
- package/tools/game/defaults.js +82 -0
- package/tools/game/defaults.ts +83 -0
- package/tools/game/font.d.ts +3 -0
- package/tools/game/font.js +124 -0
- package/tools/game/font.ts +126 -0
- package/tools/game/kit.d.ts +7 -0
- package/tools/game/kit.js +7 -0
- package/tools/game/kit.ts +10 -0
- package/tools/game/new.d.ts +8 -0
- package/tools/game/new.js +39 -0
- package/tools/game/new.ts +38 -0
- package/tools/game/target.d.ts +7 -0
- package/tools/game/target.js +41 -0
- package/tools/game/target.ts +42 -0
- package/tools/game/template/resources.ts.tmpl +43 -0
- package/tools/game/template/rooms/1.room.yaml +9 -0
- package/tools/game/template/rooms/1.yarn +17 -0
- package/tools/game/template/scripts/0.sc +24 -0
- package/tools/game.d.ts +1 -0
- package/tools/game.js +77 -0
- package/tools/game.ts +70 -0
- package/tools/mod-build.d.ts +28 -0
- package/tools/mod-build.js +173 -0
- package/tools/mod-build.ts +179 -0
- package/tools/mod-target.d.ts +16 -0
- package/tools/mod-target.js +26 -0
- package/tools/mod-target.ts +32 -0
- package/tools/mod.d.ts +1 -0
- package/tools/mod.js +47 -0
- package/tools/mod.ts +45 -0
- package/tools/msg.d.ts +1 -0
- package/tools/msg.js +40 -0
- package/tools/msg.ts +36 -0
- package/tools/node-files.d.ts +3 -0
- package/tools/node-files.js +34 -0
- package/tools/node-files.ts +33 -0
- package/tools/patches.d.ts +3 -0
- package/tools/patches.js +13 -0
- package/tools/patches.ts +13 -0
- package/tools/pic-build.d.ts +9 -0
- package/tools/pic-build.js +26 -0
- package/tools/pic-build.ts +27 -0
- package/tools/pic.d.ts +1 -0
- package/tools/pic.js +64 -0
- package/tools/pic.ts +61 -0
- package/tools/play.d.ts +1 -0
- package/tools/play.js +54 -0
- package/tools/play.ts +54 -0
- package/tools/png.d.ts +16 -0
- package/tools/png.js +124 -0
- package/tools/png.ts +123 -0
- package/tools/res.d.ts +1 -0
- package/tools/res.js +48 -0
- package/tools/res.ts +50 -0
- package/tools/scenario.d.ts +1 -0
- package/tools/scenario.js +74 -0
- package/tools/scenario.ts +80 -0
- package/tools/site/build.d.ts +8 -0
- package/tools/site/build.js +110 -0
- package/tools/site/build.ts +116 -0
- package/tools/site/markdown.d.ts +11 -0
- package/tools/site/markdown.js +88 -0
- package/tools/site/markdown.ts +84 -0
- package/tools/view-build.d.ts +34 -0
- package/tools/view-build.js +47 -0
- package/tools/view-build.ts +71 -0
- package/tools/view.d.ts +1 -0
- package/tools/view.js +54 -0
- package/tools/view.ts +52 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCI1.1/SCI2 sound resources and a sequencer for them.
|
|
3
|
+
*
|
|
4
|
+
* Resource layout:
|
|
5
|
+
* [0xF0 priority, 6 unused] optional 8-byte header
|
|
6
|
+
* tracks until 0xFF: u8 device, then 6-byte channel entries until 0xFF:
|
|
7
|
+
* u16 unused, u16 offset, u16 size
|
|
8
|
+
* Devices: 0x00 AdLib, 0x07 General MIDI, 0x0C MT-32 (plus a few others).
|
|
9
|
+
* Each channel stream: u8 channel (low nibble = MIDI channel), u8 priority/voices,
|
|
10
|
+
* then [delta][event]... where delta bytes of 0xF8 each add 240 ticks (1/60 s), and
|
|
11
|
+
* events are MIDI messages with running status. 0xFC ends the stream.
|
|
12
|
+
*
|
|
13
|
+
* Channel 15 is Sierra's control channel: program change N sets the sound object's
|
|
14
|
+
* signal to N (127 marks the loop point instead), controller 0x60 is a "cue".
|
|
15
|
+
*/
|
|
16
|
+
export const SoundDevice = { AdLib: 0x00, GeneralMidi: 0x07, Mt32: 0x0c } as const;
|
|
17
|
+
|
|
18
|
+
const CONTROL_CHANNEL = 15;
|
|
19
|
+
const LOOP_MARKER = 127;
|
|
20
|
+
const CUE_CONTROLLER = 0x60;
|
|
21
|
+
|
|
22
|
+
export interface MidiEvent {
|
|
23
|
+
tick: number;
|
|
24
|
+
/** Raw MIDI bytes, e.g. [0x91, 60, 100]. */
|
|
25
|
+
bytes: number[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SongChannel {
|
|
29
|
+
midiChannel: number;
|
|
30
|
+
/** Voices this channel wants from a voice-limited device (AdLib: 9 in total). */
|
|
31
|
+
voices: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Song {
|
|
35
|
+
priority: number;
|
|
36
|
+
channels: SongChannel[];
|
|
37
|
+
/** All channels merged, sorted by tick. */
|
|
38
|
+
events: MidiEvent[];
|
|
39
|
+
/** Tick where the last event ends. */
|
|
40
|
+
length: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dataBytes = (status: number) => ((status & 0xf0) === 0xc0 || (status & 0xf0) === 0xd0 ? 1 : 2);
|
|
44
|
+
|
|
45
|
+
function readChannel(data: Uint8Array, start: number, end: number, midiChannel: number, out: MidiEvent[]): number {
|
|
46
|
+
let pos = start;
|
|
47
|
+
let tick = 0;
|
|
48
|
+
let running = 0;
|
|
49
|
+
while (pos < end) {
|
|
50
|
+
// Delta time.
|
|
51
|
+
let b = data[pos++]!;
|
|
52
|
+
while (b === 0xf8 && pos < end) {
|
|
53
|
+
tick += 240;
|
|
54
|
+
b = data[pos++]!;
|
|
55
|
+
}
|
|
56
|
+
tick += b;
|
|
57
|
+
if (pos >= end) break;
|
|
58
|
+
|
|
59
|
+
let status = data[pos]!;
|
|
60
|
+
if (status === 0xfc) break; // end of stream
|
|
61
|
+
if (status & 0x80) pos++;
|
|
62
|
+
else status = running; // running status: this byte is data
|
|
63
|
+
if (status === 0xf0) {
|
|
64
|
+
while (pos < end && data[pos] !== 0xf7) pos++; // skip sysex
|
|
65
|
+
pos++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (status === 0xff) {
|
|
69
|
+
pos += 1; // meta type
|
|
70
|
+
const len = data[pos++]!;
|
|
71
|
+
pos += len;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (status < 0x80) break; // corrupt stream
|
|
75
|
+
running = status;
|
|
76
|
+
const n = dataBytes(status);
|
|
77
|
+
const bytes = [(status & 0xf0) | midiChannel];
|
|
78
|
+
for (let i = 0; i < n; i++) bytes.push(data[pos++]! & 0x7f);
|
|
79
|
+
out.push({ tick, bytes });
|
|
80
|
+
}
|
|
81
|
+
return tick;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Parses the track for `device` (falling back to MT-32, then any), merging its channels. */
|
|
85
|
+
export function parseSound(data: Uint8Array, device: number = SoundDevice.GeneralMidi): Song | undefined {
|
|
86
|
+
const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
87
|
+
let pos = 0;
|
|
88
|
+
let priority = 0;
|
|
89
|
+
if (data[0] === 0xf0) {
|
|
90
|
+
priority = data[1]!;
|
|
91
|
+
pos = 8;
|
|
92
|
+
}
|
|
93
|
+
const tracks = new Map<number, { offset: number; size: number }[]>();
|
|
94
|
+
while (pos < data.length && data[pos] !== 0xff) {
|
|
95
|
+
const type = data[pos++]!;
|
|
96
|
+
const channels: { offset: number; size: number }[] = [];
|
|
97
|
+
while (pos + 6 <= data.length && data[pos] !== 0xff) {
|
|
98
|
+
channels.push({ offset: v.getUint16(pos + 2, true), size: v.getUint16(pos + 4, true) });
|
|
99
|
+
pos += 6;
|
|
100
|
+
}
|
|
101
|
+
pos++;
|
|
102
|
+
tracks.set(type, channels);
|
|
103
|
+
}
|
|
104
|
+
const channels = tracks.get(device) ?? tracks.get(SoundDevice.Mt32);
|
|
105
|
+
if (!channels?.length) return undefined;
|
|
106
|
+
|
|
107
|
+
const events: MidiEvent[] = [];
|
|
108
|
+
const songChannels: SongChannel[] = [];
|
|
109
|
+
let length = 0;
|
|
110
|
+
for (const { offset, size } of channels) {
|
|
111
|
+
if (!size || offset + 2 > data.length) continue;
|
|
112
|
+
const midiChannel = data[offset]! & 0x0f;
|
|
113
|
+
if (midiChannel !== CONTROL_CHANNEL) songChannels.push({ midiChannel, voices: data[offset + 1]! & 0x0f });
|
|
114
|
+
length = Math.max(length, readChannel(data, offset + 2, Math.min(data.length, offset + size), midiChannel, events));
|
|
115
|
+
}
|
|
116
|
+
// Stable sort keeps each channel's order for events on the same tick.
|
|
117
|
+
events.sort((a, b) => a.tick - b.tick);
|
|
118
|
+
return { priority, channels: songChannels, events, length };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Where MIDI bytes go: a SoundFont synth or the AdLib driver. */
|
|
122
|
+
export interface MidiOutput {
|
|
123
|
+
send(bytes: number[]): void;
|
|
124
|
+
/** Voice-limited devices: a song claims (or, with a negative count, releases) voices. */
|
|
125
|
+
claimVoices?(channel: number, count: number): void;
|
|
126
|
+
setMasterVolume?(volume: number): void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Signal values scripts see on the sound object. */
|
|
130
|
+
export const SongSignal = { Finished: 0xffff } as const;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Plays one song against a tick clock. Tracks the control channel so the kernel can
|
|
134
|
+
* report cues/signals, loops at the loop marker, and silences its notes when stopped.
|
|
135
|
+
*/
|
|
136
|
+
export class SongPlayer {
|
|
137
|
+
private index = 0;
|
|
138
|
+
private startTick = 0;
|
|
139
|
+
private loopIndex = 0;
|
|
140
|
+
private loopTick = 0;
|
|
141
|
+
private readonly held = new Set<number>(); // channel << 8 | note
|
|
142
|
+
/** Last channel volume (CC 7) the song asked for, to rescale on volume changes. */
|
|
143
|
+
private readonly channelVolume = new Map<number, number>();
|
|
144
|
+
finished = false;
|
|
145
|
+
/** Latest control-channel signal not yet reported (0 = none). */
|
|
146
|
+
signal = 0;
|
|
147
|
+
dataInc = 0;
|
|
148
|
+
private paused: number | undefined;
|
|
149
|
+
|
|
150
|
+
constructor(
|
|
151
|
+
readonly song: Song,
|
|
152
|
+
private readonly output: MidiOutput | undefined,
|
|
153
|
+
public loop: boolean,
|
|
154
|
+
public volume: number, // 0..127
|
|
155
|
+
now: number,
|
|
156
|
+
) {
|
|
157
|
+
this.startTick = now;
|
|
158
|
+
for (const c of song.channels) output?.claimVoices?.(c.midiChannel, c.voices);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Sends every event due by tick `now`. */
|
|
162
|
+
update(now: number): void {
|
|
163
|
+
if (this.finished || this.paused !== undefined) return;
|
|
164
|
+
const pos = now - this.startTick;
|
|
165
|
+
const events = this.song.events;
|
|
166
|
+
while (this.index < events.length && events[this.index]!.tick <= pos) {
|
|
167
|
+
this.dispatch(events[this.index]!, this.index);
|
|
168
|
+
this.index++;
|
|
169
|
+
}
|
|
170
|
+
if (this.index >= events.length && pos >= this.song.length) {
|
|
171
|
+
if (this.loop) {
|
|
172
|
+
this.silence();
|
|
173
|
+
this.index = this.loopIndex;
|
|
174
|
+
this.startTick = now - this.loopTick;
|
|
175
|
+
} else {
|
|
176
|
+
this.stop();
|
|
177
|
+
this.finished = true;
|
|
178
|
+
this.signal = SongSignal.Finished;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private dispatch(e: MidiEvent, index: number): void {
|
|
184
|
+
const [status = 0, a = 0, b = 0] = e.bytes;
|
|
185
|
+
const channel = status & 0x0f;
|
|
186
|
+
const type = status & 0xf0;
|
|
187
|
+
if (channel === CONTROL_CHANNEL) {
|
|
188
|
+
if (type === 0xc0) {
|
|
189
|
+
if (a === LOOP_MARKER) {
|
|
190
|
+
this.loopIndex = index + 1;
|
|
191
|
+
this.loopTick = e.tick;
|
|
192
|
+
} else if (e.tick > 0) this.signal = a;
|
|
193
|
+
} else if (type === 0xb0 && a === CUE_CONTROLLER) {
|
|
194
|
+
this.dataInc++;
|
|
195
|
+
this.signal = this.dataInc + 127;
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (type === 0x90 && b > 0) this.held.add((channel << 8) | a);
|
|
200
|
+
else if (type === 0x80 || type === 0x90) this.held.delete((channel << 8) | a);
|
|
201
|
+
// Scale channel volume by the sound object's volume.
|
|
202
|
+
if (type === 0xb0 && a === 7) {
|
|
203
|
+
this.channelVolume.set(channel, b);
|
|
204
|
+
return this.output?.send([status, a, Math.round((b * this.volume) / 127)]);
|
|
205
|
+
}
|
|
206
|
+
this.output?.send(e.bytes);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Releases every note this song is holding. */
|
|
210
|
+
private silence(): void {
|
|
211
|
+
for (const key of this.held) this.output?.send([0x80 | (key >> 8), key & 0xff, 0]);
|
|
212
|
+
this.held.clear();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private released = false;
|
|
216
|
+
|
|
217
|
+
stop(): void {
|
|
218
|
+
this.silence();
|
|
219
|
+
if (this.released) return;
|
|
220
|
+
this.released = true;
|
|
221
|
+
for (const c of this.song.channels) this.output?.claimVoices?.(c.midiChannel, -c.voices);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Position in ticks. */
|
|
225
|
+
position(now: number): number {
|
|
226
|
+
return (this.paused ?? now) - this.startTick;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Jumps to `pos` ticks without sounding notes: replays program, controller and pitch
|
|
231
|
+
* events so the (possibly new) output is in the right state. Used when switching devices.
|
|
232
|
+
*/
|
|
233
|
+
seek(pos: number, now: number): void {
|
|
234
|
+
const events = this.song.events;
|
|
235
|
+
this.index = 0;
|
|
236
|
+
while (this.index < events.length && events[this.index]!.tick <= pos) {
|
|
237
|
+
const e = events[this.index]!;
|
|
238
|
+
const type = e.bytes[0]! & 0xf0;
|
|
239
|
+
if (type !== 0x90 && type !== 0x80) this.dispatch(e, this.index);
|
|
240
|
+
this.index++;
|
|
241
|
+
}
|
|
242
|
+
this.signal = 0;
|
|
243
|
+
this.startTick = now - pos;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
pause(now: number): void {
|
|
247
|
+
if (this.paused !== undefined) return;
|
|
248
|
+
this.paused = now;
|
|
249
|
+
this.silence();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
resume(now: number): void {
|
|
253
|
+
if (this.paused === undefined) return;
|
|
254
|
+
this.startTick += now - this.paused;
|
|
255
|
+
this.paused = undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setVolume(volume: number): void {
|
|
259
|
+
this.volume = volume;
|
|
260
|
+
for (const [channel, v] of this.channelVolume) this.output?.send([0xb0 | channel, 7, Math.round((v * volume) / 127)]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yamaha YM3812 (OPL2) FM synthesizer: the chip on AdLib and Sound Blaster cards.
|
|
3
|
+
*
|
|
4
|
+
* 9 channels, each with two operators: a *modulator* and a *carrier*. Each operator is a
|
|
5
|
+
* sine-ish oscillator with its own envelope. In FM mode the modulator's output bends the
|
|
6
|
+
* carrier's phase (that's where the metallic/brassy timbres come from); in additive mode
|
|
7
|
+
* both are simply mixed. Sound is shaped entirely through register writes, exactly as the
|
|
8
|
+
* game's AdLib driver did it in 1994.
|
|
9
|
+
*
|
|
10
|
+
* This is a behavioural model, not a gate-level one: it follows the documented register
|
|
11
|
+
* layout and datasheet timings (log-domain attenuation in 0.1875 dB steps, a 10-bit sine
|
|
12
|
+
* table, exponential attack, linear-in-dB decay) closely enough to sound like the chip.
|
|
13
|
+
*/
|
|
14
|
+
export declare const OPL_RATE = 49716;
|
|
15
|
+
declare enum Env {
|
|
16
|
+
Attack = 0,
|
|
17
|
+
Decay = 1,
|
|
18
|
+
Sustain = 2,
|
|
19
|
+
Release = 3,
|
|
20
|
+
Off = 4
|
|
21
|
+
}
|
|
22
|
+
declare class Operator {
|
|
23
|
+
am: boolean;
|
|
24
|
+
vib: boolean;
|
|
25
|
+
sustained: boolean;
|
|
26
|
+
ksr: boolean;
|
|
27
|
+
mult: number;
|
|
28
|
+
ksl: number;
|
|
29
|
+
tl: number;
|
|
30
|
+
ar: number;
|
|
31
|
+
dr: number;
|
|
32
|
+
sl: number;
|
|
33
|
+
rr: number;
|
|
34
|
+
wave: number;
|
|
35
|
+
phase: number;
|
|
36
|
+
inc: number;
|
|
37
|
+
att: number;
|
|
38
|
+
state: Env;
|
|
39
|
+
kslAtt: number;
|
|
40
|
+
keyCode: number;
|
|
41
|
+
attackK: number;
|
|
42
|
+
decayStep: number;
|
|
43
|
+
releaseStep: number;
|
|
44
|
+
private effectiveRate;
|
|
45
|
+
updateRates(): void;
|
|
46
|
+
get sustainLevel(): number;
|
|
47
|
+
keyOn(): void;
|
|
48
|
+
keyOff(): void;
|
|
49
|
+
stepEnvelope(): void;
|
|
50
|
+
/** Output in -1..1 for a phase offset (modulation) in cycles. */
|
|
51
|
+
output(mod: number, tremolo: number, waveEnabled: boolean): number;
|
|
52
|
+
}
|
|
53
|
+
declare class Channel {
|
|
54
|
+
readonly ops: readonly [Operator, Operator];
|
|
55
|
+
fnum: number;
|
|
56
|
+
block: number;
|
|
57
|
+
keyOn: boolean;
|
|
58
|
+
feedback: number;
|
|
59
|
+
additive: boolean;
|
|
60
|
+
fb1: number;
|
|
61
|
+
fb2: number;
|
|
62
|
+
updatePitch(): void;
|
|
63
|
+
}
|
|
64
|
+
export declare class Opl2 {
|
|
65
|
+
readonly channels: Channel[];
|
|
66
|
+
private waveSelect;
|
|
67
|
+
private deepTremolo;
|
|
68
|
+
private deepVibrato;
|
|
69
|
+
private lfoTime;
|
|
70
|
+
write(reg: number, value: number): void;
|
|
71
|
+
/** Renders `count` mono samples at OPL_RATE into `out` starting at `offset`. */
|
|
72
|
+
generate(out: Float32Array, offset?: number, count?: number): void;
|
|
73
|
+
}
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yamaha YM3812 (OPL2) FM synthesizer: the chip on AdLib and Sound Blaster cards.
|
|
3
|
+
*
|
|
4
|
+
* 9 channels, each with two operators: a *modulator* and a *carrier*. Each operator is a
|
|
5
|
+
* sine-ish oscillator with its own envelope. In FM mode the modulator's output bends the
|
|
6
|
+
* carrier's phase (that's where the metallic/brassy timbres come from); in additive mode
|
|
7
|
+
* both are simply mixed. Sound is shaped entirely through register writes, exactly as the
|
|
8
|
+
* game's AdLib driver did it in 1994.
|
|
9
|
+
*
|
|
10
|
+
* This is a behavioural model, not a gate-level one: it follows the documented register
|
|
11
|
+
* layout and datasheet timings (log-domain attenuation in 0.1875 dB steps, a 10-bit sine
|
|
12
|
+
* table, exponential attack, linear-in-dB decay) closely enough to sound like the chip.
|
|
13
|
+
*/
|
|
14
|
+
export const OPL_RATE = 49716; // 3.579545 MHz / 72
|
|
15
|
+
const MULT = [0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 12, 15, 15];
|
|
16
|
+
const KSL_ROM = [0, 32, 40, 45, 48, 51, 53, 55, 56, 58, 59, 60, 61, 62, 63, 64];
|
|
17
|
+
const KSL_SHIFT = [8, 1, 2, 0];
|
|
18
|
+
/** Attenuation units are 0.1875 dB; 511 units ≈ 96 dB = silence. */
|
|
19
|
+
const ATT_MAX = 511;
|
|
20
|
+
const ATT_DB = 0.1875;
|
|
21
|
+
const SINE = new Float32Array(1024).map((_, i) => Math.sin(((i + 0.5) / 1024) * 2 * Math.PI));
|
|
22
|
+
const AMPLITUDE = new Float32Array(2048).map((_, u) => Math.pow(10, (-u * ATT_DB) / 20));
|
|
23
|
+
/** OPL2 waveforms: sine, half-sine, abs-sine, and "quarter" pulses of abs-sine. */
|
|
24
|
+
function wave(form, phase) {
|
|
25
|
+
const i = Math.floor(phase * 1024) & 1023;
|
|
26
|
+
const s = SINE[i];
|
|
27
|
+
switch (form) {
|
|
28
|
+
case 1: return s > 0 ? s : 0;
|
|
29
|
+
case 2: return Math.abs(s);
|
|
30
|
+
case 3: return (i & 256) === 0 ? Math.abs(s) : 0;
|
|
31
|
+
default: return s;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
var Env;
|
|
35
|
+
(function (Env) {
|
|
36
|
+
Env[Env["Attack"] = 0] = "Attack";
|
|
37
|
+
Env[Env["Decay"] = 1] = "Decay";
|
|
38
|
+
Env[Env["Sustain"] = 2] = "Sustain";
|
|
39
|
+
Env[Env["Release"] = 3] = "Release";
|
|
40
|
+
Env[Env["Off"] = 4] = "Off";
|
|
41
|
+
})(Env || (Env = {}));
|
|
42
|
+
/** Datasheet timings (ms) for the slowest rate (effective rate 4); each +4 halves them. */
|
|
43
|
+
const ATTACK_MS = 2826.24;
|
|
44
|
+
const DECAY_MS = 39280.64;
|
|
45
|
+
function rateTime(baseMs, rate) {
|
|
46
|
+
if (rate < 4)
|
|
47
|
+
return Infinity;
|
|
48
|
+
const r = Math.min(rate, 60);
|
|
49
|
+
return (baseMs * 4) / ((4 + (r & 3)) * Math.pow(2, (r >> 2) - 1));
|
|
50
|
+
}
|
|
51
|
+
class Operator {
|
|
52
|
+
// Registers
|
|
53
|
+
am = false;
|
|
54
|
+
vib = false;
|
|
55
|
+
sustained = false; // EG type: hold at sustain level until key-off
|
|
56
|
+
ksr = false;
|
|
57
|
+
mult = 1;
|
|
58
|
+
ksl = 0;
|
|
59
|
+
tl = 0;
|
|
60
|
+
ar = 0;
|
|
61
|
+
dr = 0;
|
|
62
|
+
sl = 0;
|
|
63
|
+
rr = 0;
|
|
64
|
+
wave = 0;
|
|
65
|
+
// State
|
|
66
|
+
phase = 0; // in cycles, 0..1
|
|
67
|
+
inc = 0; // cycles per sample
|
|
68
|
+
att = ATT_MAX; // envelope attenuation, units of 0.1875 dB
|
|
69
|
+
state = Env.Off;
|
|
70
|
+
kslAtt = 0;
|
|
71
|
+
keyCode = 0; // (block << 1) | fnum bit 9, for rate key scaling
|
|
72
|
+
// Per-sample envelope steps, recomputed when rates or pitch change.
|
|
73
|
+
attackK = 0;
|
|
74
|
+
decayStep = 0;
|
|
75
|
+
releaseStep = 0;
|
|
76
|
+
effectiveRate(r) {
|
|
77
|
+
return r === 0 ? 0 : Math.min(63, r * 4 + (this.ksr ? this.keyCode : this.keyCode >> 2));
|
|
78
|
+
}
|
|
79
|
+
updateRates() {
|
|
80
|
+
const at = rateTime(ATTACK_MS, this.effectiveRate(this.ar));
|
|
81
|
+
// Exponential attack: reach ~0 attenuation from full in `at`.
|
|
82
|
+
this.attackK = this.effectiveRate(this.ar) >= 60 ? 1 : at === Infinity ? 0 : 1 - Math.pow(1 / 512, 1000 / (at * OPL_RATE));
|
|
83
|
+
const step = (rate) => {
|
|
84
|
+
const t = rateTime(DECAY_MS, this.effectiveRate(rate));
|
|
85
|
+
return t === Infinity ? 0 : (ATT_MAX * 1000) / (t * OPL_RATE);
|
|
86
|
+
};
|
|
87
|
+
this.decayStep = step(this.dr);
|
|
88
|
+
this.releaseStep = step(this.rr);
|
|
89
|
+
}
|
|
90
|
+
get sustainLevel() {
|
|
91
|
+
return (this.sl === 15 ? 31 : this.sl) * 16; // 3 dB steps; 15 means 93 dB
|
|
92
|
+
}
|
|
93
|
+
keyOn() {
|
|
94
|
+
this.phase = 0;
|
|
95
|
+
this.state = Env.Attack;
|
|
96
|
+
}
|
|
97
|
+
keyOff() {
|
|
98
|
+
if (this.state !== Env.Off)
|
|
99
|
+
this.state = Env.Release;
|
|
100
|
+
}
|
|
101
|
+
stepEnvelope() {
|
|
102
|
+
switch (this.state) {
|
|
103
|
+
case Env.Attack:
|
|
104
|
+
this.att -= (this.att + 1) * this.attackK;
|
|
105
|
+
if (this.att <= 0) {
|
|
106
|
+
this.att = 0;
|
|
107
|
+
this.state = Env.Decay;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case Env.Decay:
|
|
111
|
+
this.att += this.decayStep;
|
|
112
|
+
if (this.att >= this.sustainLevel) {
|
|
113
|
+
this.att = this.sustainLevel;
|
|
114
|
+
this.state = Env.Sustain;
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
case Env.Sustain:
|
|
118
|
+
// Percussive envelopes (EG type 0) keep falling at the release rate.
|
|
119
|
+
if (!this.sustained)
|
|
120
|
+
this.att += this.releaseStep;
|
|
121
|
+
break;
|
|
122
|
+
case Env.Release:
|
|
123
|
+
this.att += this.releaseStep;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
if (this.att >= ATT_MAX && this.state !== Env.Attack) {
|
|
127
|
+
this.att = ATT_MAX;
|
|
128
|
+
if (this.state === Env.Release || this.state === Env.Sustain)
|
|
129
|
+
this.state = Env.Off;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Output in -1..1 for a phase offset (modulation) in cycles. */
|
|
133
|
+
output(mod, tremolo, waveEnabled) {
|
|
134
|
+
if (this.state === Env.Off)
|
|
135
|
+
return 0;
|
|
136
|
+
const att = this.att + this.tl * 4 + this.kslAtt + (this.am ? tremolo : 0);
|
|
137
|
+
if (att >= ATT_MAX)
|
|
138
|
+
return 0;
|
|
139
|
+
return wave(waveEnabled ? this.wave : 0, this.phase + mod) * AMPLITUDE[att | 0];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
class Channel {
|
|
143
|
+
ops = [new Operator(), new Operator()];
|
|
144
|
+
fnum = 0;
|
|
145
|
+
block = 0;
|
|
146
|
+
keyOn = false;
|
|
147
|
+
feedback = 0;
|
|
148
|
+
additive = false;
|
|
149
|
+
fb1 = 0;
|
|
150
|
+
fb2 = 0;
|
|
151
|
+
updatePitch() {
|
|
152
|
+
const keyCode = (this.block << 1) | ((this.fnum >> 9) & 1);
|
|
153
|
+
const kslBase = Math.max(0, KSL_ROM[this.fnum >> 6] * 4 - (8 - this.block) * 32);
|
|
154
|
+
for (const op of this.ops) {
|
|
155
|
+
op.inc = (this.fnum * Math.pow(2, this.block) * MULT[op.mult]) / (1 << 20);
|
|
156
|
+
op.keyCode = keyCode;
|
|
157
|
+
op.kslAtt = kslBase >> KSL_SHIFT[op.ksl];
|
|
158
|
+
op.updateRates();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Operator register offsets 0x00-0x15 → (channel, operator), with gaps at 6-7 and 0x0e-0x0f. */
|
|
163
|
+
function slotFor(offset) {
|
|
164
|
+
const group = offset >> 3;
|
|
165
|
+
const within = offset & 7;
|
|
166
|
+
if (group > 2 || within > 5)
|
|
167
|
+
return undefined;
|
|
168
|
+
return [group * 3 + (within % 3), within >= 3 ? 1 : 0];
|
|
169
|
+
}
|
|
170
|
+
export class Opl2 {
|
|
171
|
+
channels = Array.from({ length: 9 }, () => new Channel());
|
|
172
|
+
waveSelect = false;
|
|
173
|
+
deepTremolo = false;
|
|
174
|
+
deepVibrato = false;
|
|
175
|
+
lfoTime = 0;
|
|
176
|
+
write(reg, value) {
|
|
177
|
+
reg &= 0xff;
|
|
178
|
+
value &= 0xff;
|
|
179
|
+
if (reg === 0x01) {
|
|
180
|
+
this.waveSelect = (value & 0x20) !== 0;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (reg === 0xbd) {
|
|
184
|
+
this.deepTremolo = (value & 0x80) !== 0;
|
|
185
|
+
this.deepVibrato = (value & 0x40) !== 0;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const hi = reg & 0xe0;
|
|
189
|
+
if (hi >= 0x20 && hi <= 0x80 || hi === 0xe0) {
|
|
190
|
+
const slot = slotFor(reg & 0x1f);
|
|
191
|
+
if (!slot)
|
|
192
|
+
return;
|
|
193
|
+
const ch = this.channels[slot[0]];
|
|
194
|
+
const op = ch.ops[slot[1]];
|
|
195
|
+
switch (hi) {
|
|
196
|
+
case 0x20:
|
|
197
|
+
op.am = (value & 0x80) !== 0;
|
|
198
|
+
op.vib = (value & 0x40) !== 0;
|
|
199
|
+
op.sustained = (value & 0x20) !== 0;
|
|
200
|
+
op.ksr = (value & 0x10) !== 0;
|
|
201
|
+
op.mult = value & 0x0f;
|
|
202
|
+
ch.updatePitch();
|
|
203
|
+
break;
|
|
204
|
+
case 0x40:
|
|
205
|
+
op.ksl = value >> 6;
|
|
206
|
+
op.tl = value & 0x3f;
|
|
207
|
+
ch.updatePitch();
|
|
208
|
+
break;
|
|
209
|
+
case 0x60:
|
|
210
|
+
op.ar = value >> 4;
|
|
211
|
+
op.dr = value & 0x0f;
|
|
212
|
+
op.updateRates();
|
|
213
|
+
break;
|
|
214
|
+
case 0x80:
|
|
215
|
+
op.sl = value >> 4;
|
|
216
|
+
op.rr = value & 0x0f;
|
|
217
|
+
op.updateRates();
|
|
218
|
+
break;
|
|
219
|
+
case 0xe0:
|
|
220
|
+
op.wave = value & 3;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const c = reg & 0x0f;
|
|
226
|
+
if (c > 8)
|
|
227
|
+
return;
|
|
228
|
+
const ch = this.channels[c];
|
|
229
|
+
switch (reg & 0xf0) {
|
|
230
|
+
case 0xa0:
|
|
231
|
+
ch.fnum = (ch.fnum & 0x300) | value;
|
|
232
|
+
ch.updatePitch();
|
|
233
|
+
break;
|
|
234
|
+
case 0xb0: {
|
|
235
|
+
ch.fnum = (ch.fnum & 0xff) | ((value & 3) << 8);
|
|
236
|
+
ch.block = (value >> 2) & 7;
|
|
237
|
+
ch.updatePitch();
|
|
238
|
+
const on = (value & 0x20) !== 0;
|
|
239
|
+
if (on && !ch.keyOn)
|
|
240
|
+
for (const op of ch.ops)
|
|
241
|
+
op.keyOn();
|
|
242
|
+
if (!on && ch.keyOn)
|
|
243
|
+
for (const op of ch.ops)
|
|
244
|
+
op.keyOff();
|
|
245
|
+
ch.keyOn = on;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case 0xc0:
|
|
249
|
+
ch.feedback = (value >> 1) & 7;
|
|
250
|
+
ch.additive = (value & 1) !== 0;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Renders `count` mono samples at OPL_RATE into `out` starting at `offset`. */
|
|
255
|
+
generate(out, offset = 0, count = out.length - offset) {
|
|
256
|
+
const tremoloDepth = this.deepTremolo ? 4.8 / ATT_DB : 1 / ATT_DB;
|
|
257
|
+
const vibratoCents = this.deepVibrato ? 14 : 7;
|
|
258
|
+
for (let n = 0; n < count; n++) {
|
|
259
|
+
this.lfoTime += 1 / OPL_RATE;
|
|
260
|
+
const tremolo = tremoloDepth * (0.5 - 0.5 * Math.cos(2 * Math.PI * 3.7 * this.lfoTime));
|
|
261
|
+
const vibrato = Math.pow(2, (vibratoCents * Math.sin(2 * Math.PI * 6.07 * this.lfoTime)) / 1200);
|
|
262
|
+
let mix = 0;
|
|
263
|
+
for (const ch of this.channels) {
|
|
264
|
+
const [mod, car] = ch.ops;
|
|
265
|
+
if (mod.state === Env.Off && car.state === Env.Off)
|
|
266
|
+
continue;
|
|
267
|
+
const fb = ch.feedback ? (ch.fb1 + ch.fb2) * Math.pow(2, ch.feedback - 7) : 0;
|
|
268
|
+
const m = mod.output(fb, tremolo, this.waveSelect);
|
|
269
|
+
ch.fb2 = ch.fb1;
|
|
270
|
+
ch.fb1 = m;
|
|
271
|
+
// Full-scale modulator output shifts the carrier's phase by ±4 cycles, as on the chip.
|
|
272
|
+
const c = car.output(ch.additive ? 0 : m * 4, tremolo, this.waveSelect);
|
|
273
|
+
mix += ch.additive ? m + c : c;
|
|
274
|
+
for (const op of ch.ops) {
|
|
275
|
+
op.phase = (op.phase + op.inc * (op.vib ? vibrato : 1)) % 1;
|
|
276
|
+
op.stepEnvelope();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Each channel peaks at ~1/8 of the DAC's range; the sum can clip, as on real cards.
|
|
280
|
+
out[offset + n] = Math.max(-1, Math.min(1, mix * 0.125 * 2));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|