sci2-ts 0.0.0-stage → 0.2.1
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 +72 -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,73 @@
|
|
|
1
|
+
import type { MidiOutput } from "./midi.ts";
|
|
2
|
+
/** Anything that accepts OPL register writes: the emulator, or a queue to a worklet. */
|
|
3
|
+
export interface OplChip {
|
|
4
|
+
write(reg: number, value: number): void;
|
|
5
|
+
}
|
|
6
|
+
interface OperatorPatch {
|
|
7
|
+
ksl: number;
|
|
8
|
+
mult: number;
|
|
9
|
+
attack: number;
|
|
10
|
+
sustainLevel: number;
|
|
11
|
+
sustained: number;
|
|
12
|
+
decay: number;
|
|
13
|
+
release: number;
|
|
14
|
+
level: number;
|
|
15
|
+
am: number;
|
|
16
|
+
vibrato: number;
|
|
17
|
+
ksr: number;
|
|
18
|
+
wave: number;
|
|
19
|
+
}
|
|
20
|
+
export interface AdLibInstrument {
|
|
21
|
+
mod: OperatorPatch;
|
|
22
|
+
car: OperatorPatch;
|
|
23
|
+
feedback: number;
|
|
24
|
+
fm: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface AdLibBank {
|
|
27
|
+
instruments: AdLibInstrument[];
|
|
28
|
+
/** Channel 9 note → pitch to play its percussion instrument at (notes 27..88). */
|
|
29
|
+
rhythmKeyMap: Uint8Array | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parses the AdLib instrument bank (patch resource 3). SCI1.1/SCI2 banks are 190 × 28-byte
|
|
33
|
+
* instruments followed by a 62-byte rhythm key map (5,382 bytes); older ones have 48 or 96.
|
|
34
|
+
*
|
|
35
|
+
* Instrument: modulator (13 bytes) + carrier (13 bytes) + 2 waveform bytes. Operator bytes:
|
|
36
|
+
* KSL, MULT, FB, AR, SL, EGT, DR, RR, TL, AM, VIB, KSR, CON. FB and CON are only meaningful
|
|
37
|
+
* on the modulator; CON = 1 means FM (the chip's register bit is the inverse).
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseAdLibBank(data: Uint8Array): AdLibBank;
|
|
40
|
+
/**
|
|
41
|
+
* Sierra's AdLib driver, reimplemented: turns MIDI into OPL2 register writes.
|
|
42
|
+
* The chip has 9 voices; Sierra's AdLib arrangements ask for a fixed number per channel
|
|
43
|
+
* (1 each in the scores we tested), and a channel steals its own oldest voice when it runs out.
|
|
44
|
+
*/
|
|
45
|
+
export declare class AdLibDriver implements MidiOutput {
|
|
46
|
+
private readonly chip;
|
|
47
|
+
private readonly bank;
|
|
48
|
+
private readonly voices;
|
|
49
|
+
private readonly channels;
|
|
50
|
+
private masterVolume;
|
|
51
|
+
constructor(chip: OplChip, bank: AdLibBank);
|
|
52
|
+
setMasterVolume(volume: number): void;
|
|
53
|
+
/** Voice mapping: give `channel` `count` more voices (negative releases them). */
|
|
54
|
+
claimVoices(channel: number, count: number): void;
|
|
55
|
+
private assignFreeVoices;
|
|
56
|
+
send(bytes: number[]): void;
|
|
57
|
+
private noteOn;
|
|
58
|
+
private noteOff;
|
|
59
|
+
/** A free voice owned by the channel, else its oldest; else any unowned voice. */
|
|
60
|
+
private findVoice;
|
|
61
|
+
private voiceOn;
|
|
62
|
+
private voiceOff;
|
|
63
|
+
private readonly lastB0;
|
|
64
|
+
private setNote;
|
|
65
|
+
private setPatch;
|
|
66
|
+
/**
|
|
67
|
+
* Output level = master × note (or instrument) level × channel volume. In FM mode only the
|
|
68
|
+
* carrier's level changes loudness (the modulator's sets the timbre); additive voices
|
|
69
|
+
* scale both operators.
|
|
70
|
+
*/
|
|
71
|
+
private updateLevels;
|
|
72
|
+
}
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { OPL_RATE } from "./opl2.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parses the AdLib instrument bank (patch resource 3). SCI1.1/SCI2 banks are 190 × 28-byte
|
|
4
|
+
* instruments followed by a 62-byte rhythm key map (5,382 bytes); older ones have 48 or 96.
|
|
5
|
+
*
|
|
6
|
+
* Instrument: modulator (13 bytes) + carrier (13 bytes) + 2 waveform bytes. Operator bytes:
|
|
7
|
+
* KSL, MULT, FB, AR, SL, EGT, DR, RR, TL, AM, VIB, KSR, CON. FB and CON are only meaningful
|
|
8
|
+
* on the modulator; CON = 1 means FM (the chip's register bit is the inverse).
|
|
9
|
+
*/
|
|
10
|
+
export function parseAdLibBank(data) {
|
|
11
|
+
const count = Math.floor(Math.min(data.length, 190 * 28) / 28);
|
|
12
|
+
const op = (b, wave) => ({
|
|
13
|
+
ksl: b[0] & 3,
|
|
14
|
+
mult: b[1] & 0x0f,
|
|
15
|
+
attack: b[3] & 0x0f,
|
|
16
|
+
sustainLevel: b[4] & 0x0f,
|
|
17
|
+
sustained: b[5] ? 1 : 0,
|
|
18
|
+
decay: b[6] & 0x0f,
|
|
19
|
+
release: b[7] & 0x0f,
|
|
20
|
+
level: b[8] & 0x3f,
|
|
21
|
+
am: b[9] ? 1 : 0,
|
|
22
|
+
vibrato: b[10] ? 1 : 0,
|
|
23
|
+
ksr: b[11] ? 1 : 0,
|
|
24
|
+
wave: wave & 3,
|
|
25
|
+
});
|
|
26
|
+
const instruments = Array.from({ length: count }, (_, i) => {
|
|
27
|
+
const b = data.subarray(i * 28, i * 28 + 28);
|
|
28
|
+
return { mod: op(b.subarray(0, 13), b[26]), car: op(b.subarray(13, 26), b[27]), feedback: b[2] & 7, fm: b[12] === 1 };
|
|
29
|
+
});
|
|
30
|
+
return { instruments, rhythmKeyMap: data.length >= 190 * 28 + 62 ? data.slice(190 * 28, 190 * 28 + 62) : undefined };
|
|
31
|
+
}
|
|
32
|
+
/** Maps MIDI note velocity (0..63) onto the driver's level curve (from Sierra's SCI1.1 driver). */
|
|
33
|
+
const VELOCITY_MAP = [
|
|
34
|
+
0x00, 0x0c, 0x0d, 0x0e, 0x0f, 0x11, 0x12, 0x13, 0x14, 0x16, 0x17, 0x18, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
|
|
35
|
+
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x2f, 0x30, 0x31, 0x32,
|
|
36
|
+
0x32, 0x33, 0x34, 0x34, 0x35, 0x36, 0x36, 0x37, 0x38, 0x38, 0x39, 0x3a, 0x3a, 0x3b, 0x3b, 0x3c, 0x3c, 0x3c, 0x3d,
|
|
37
|
+
0x3d, 0x3d, 0x3e, 0x3e, 0x3e, 0x3e, 0x3f,
|
|
38
|
+
];
|
|
39
|
+
/** Operator register offsets of each of the 9 voices' modulators (carrier = +3). */
|
|
40
|
+
const VOICE_OFFSET = [0x00, 0x01, 0x02, 0x08, 0x09, 0x0a, 0x10, 0x11, 0x12];
|
|
41
|
+
const VOICES = 9;
|
|
42
|
+
const RHYTHM_CHANNEL = 9;
|
|
43
|
+
const BEND_RANGE = 2; // semitones
|
|
44
|
+
/**
|
|
45
|
+
* Sierra's AdLib driver, reimplemented: turns MIDI into OPL2 register writes.
|
|
46
|
+
* The chip has 9 voices; Sierra's AdLib arrangements ask for a fixed number per channel
|
|
47
|
+
* (1 each in the scores we tested), and a channel steals its own oldest voice when it runs out.
|
|
48
|
+
*/
|
|
49
|
+
export class AdLibDriver {
|
|
50
|
+
chip;
|
|
51
|
+
bank;
|
|
52
|
+
voices = Array.from({ length: VOICES }, () => ({ channel: -1, note: -1, patch: -1, velocity: 0, age: 0, sustained: false, keyOn: false }));
|
|
53
|
+
channels = Array.from({ length: 16 }, () => ({
|
|
54
|
+
patch: 0, volume: 63, pan: 64, hold: false, pitchWheel: 8192, lastVoice: 0, velocityEnabled: false, wanted: 0,
|
|
55
|
+
}));
|
|
56
|
+
masterVolume = 15;
|
|
57
|
+
constructor(chip, bank) {
|
|
58
|
+
this.chip = chip;
|
|
59
|
+
this.bank = bank;
|
|
60
|
+
chip.write(0x01, 0x20); // enable waveform select
|
|
61
|
+
chip.write(0xbd, 0x00); // melodic mode, shallow tremolo/vibrato
|
|
62
|
+
for (let v = 0; v < VOICES; v++)
|
|
63
|
+
chip.write(0xb0 + v, 0);
|
|
64
|
+
}
|
|
65
|
+
setMasterVolume(volume) {
|
|
66
|
+
this.masterVolume = volume & 0x0f;
|
|
67
|
+
for (let v = 0; v < VOICES; v++)
|
|
68
|
+
if (this.voices[v].note !== -1)
|
|
69
|
+
this.updateLevels(v);
|
|
70
|
+
}
|
|
71
|
+
/** Voice mapping: give `channel` `count` more voices (negative releases them). */
|
|
72
|
+
claimVoices(channel, count) {
|
|
73
|
+
const ch = this.channels[channel];
|
|
74
|
+
if (count < 0) {
|
|
75
|
+
let release = -count;
|
|
76
|
+
for (let v = VOICES - 1; v >= 0 && release > 0; v--) {
|
|
77
|
+
if (this.voices[v].channel === channel) {
|
|
78
|
+
this.voiceOff(v);
|
|
79
|
+
this.voices[v].channel = -1;
|
|
80
|
+
release--;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
ch.wanted = Math.max(0, ch.wanted - release);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
ch.wanted += count;
|
|
87
|
+
this.assignFreeVoices();
|
|
88
|
+
}
|
|
89
|
+
assignFreeVoices() {
|
|
90
|
+
for (let c = 0; c < 16; c++) {
|
|
91
|
+
const ch = this.channels[c];
|
|
92
|
+
for (let v = 0; v < VOICES && ch.wanted > 0; v++) {
|
|
93
|
+
if (this.voices[v].channel === -1) {
|
|
94
|
+
this.voices[v].channel = c;
|
|
95
|
+
ch.wanted--;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
send(bytes) {
|
|
101
|
+
const [status = 0, a = 0, b = 0] = bytes;
|
|
102
|
+
const channel = status & 0x0f;
|
|
103
|
+
const ch = this.channels[channel];
|
|
104
|
+
switch (status & 0xf0) {
|
|
105
|
+
case 0x80: return this.noteOff(channel, a);
|
|
106
|
+
case 0x90: return b === 0 ? this.noteOff(channel, a) : this.noteOn(channel, a, b);
|
|
107
|
+
case 0xc0:
|
|
108
|
+
ch.patch = a;
|
|
109
|
+
return;
|
|
110
|
+
case 0xe0:
|
|
111
|
+
ch.pitchWheel = (b << 7) | a;
|
|
112
|
+
for (let v = 0; v < VOICES; v++) {
|
|
113
|
+
const voice = this.voices[v];
|
|
114
|
+
if (voice.channel === channel && voice.note !== -1)
|
|
115
|
+
this.setNote(v, voice.note, voice.keyOn);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
case 0xb0:
|
|
119
|
+
switch (a) {
|
|
120
|
+
case 7: // volume
|
|
121
|
+
ch.volume = b;
|
|
122
|
+
for (let v = 0; v < VOICES; v++)
|
|
123
|
+
if (this.voices[v].channel === channel && this.voices[v].note !== -1)
|
|
124
|
+
this.updateLevels(v);
|
|
125
|
+
return;
|
|
126
|
+
case 10:
|
|
127
|
+
ch.pan = b;
|
|
128
|
+
return;
|
|
129
|
+
case 64: // hold pedal
|
|
130
|
+
ch.hold = b !== 0;
|
|
131
|
+
if (!ch.hold) {
|
|
132
|
+
for (let v = 0; v < VOICES; v++) {
|
|
133
|
+
const voice = this.voices[v];
|
|
134
|
+
if (voice.channel === channel && voice.sustained)
|
|
135
|
+
(voice.sustained = false), this.voiceOff(v);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
case 0x4e: // velocity sensitivity on/off
|
|
140
|
+
ch.velocityEnabled = b !== 0;
|
|
141
|
+
return;
|
|
142
|
+
case 123: // all notes off
|
|
143
|
+
for (let v = 0; v < VOICES; v++)
|
|
144
|
+
if (this.voices[v].channel === channel)
|
|
145
|
+
this.voiceOff(v);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
noteOn(channel, note, velocity) {
|
|
151
|
+
const vel = VELOCITY_MAP[velocity >> 1];
|
|
152
|
+
// Retrigger if this note is already sounding on the channel.
|
|
153
|
+
for (let v = 0; v < VOICES; v++) {
|
|
154
|
+
const voice = this.voices[v];
|
|
155
|
+
if (voice.channel === channel && voice.note === note) {
|
|
156
|
+
this.voiceOff(v);
|
|
157
|
+
return this.voiceOn(v, note, vel);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const v = this.findVoice(channel);
|
|
161
|
+
if (v !== -1)
|
|
162
|
+
this.voiceOn(v, note, vel);
|
|
163
|
+
}
|
|
164
|
+
noteOff(channel, note) {
|
|
165
|
+
for (let v = 0; v < VOICES; v++) {
|
|
166
|
+
const voice = this.voices[v];
|
|
167
|
+
if (voice.channel !== channel || voice.note !== note)
|
|
168
|
+
continue;
|
|
169
|
+
if (this.channels[channel].hold)
|
|
170
|
+
voice.sustained = true;
|
|
171
|
+
else
|
|
172
|
+
this.voiceOff(v);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** A free voice owned by the channel, else its oldest; else any unowned voice. */
|
|
176
|
+
findVoice(channel) {
|
|
177
|
+
const ch = this.channels[channel];
|
|
178
|
+
let oldest = -1;
|
|
179
|
+
for (let i = 0; i < VOICES; i++) {
|
|
180
|
+
const v = (ch.lastVoice + i + 1) % VOICES;
|
|
181
|
+
const voice = this.voices[v];
|
|
182
|
+
if (voice.channel !== channel)
|
|
183
|
+
continue;
|
|
184
|
+
if (voice.note === -1)
|
|
185
|
+
return (ch.lastVoice = v);
|
|
186
|
+
if (oldest === -1 || voice.age > this.voices[oldest].age)
|
|
187
|
+
oldest = v;
|
|
188
|
+
}
|
|
189
|
+
if (oldest !== -1) {
|
|
190
|
+
this.voiceOff(oldest);
|
|
191
|
+
return (ch.lastVoice = oldest);
|
|
192
|
+
}
|
|
193
|
+
// Channel has no voices of its own (e.g. never claimed): borrow an idle unowned one.
|
|
194
|
+
const idle = this.voices.findIndex((voice) => voice.channel === -1 && voice.note === -1);
|
|
195
|
+
if (idle !== -1)
|
|
196
|
+
this.voices[idle].channel = channel;
|
|
197
|
+
return idle;
|
|
198
|
+
}
|
|
199
|
+
voiceOn(v, note, velocity) {
|
|
200
|
+
const voice = this.voices[v];
|
|
201
|
+
const channel = voice.channel;
|
|
202
|
+
for (const other of this.voices)
|
|
203
|
+
other.age++;
|
|
204
|
+
voice.age = 0;
|
|
205
|
+
const rhythm = channel === RHYTHM_CHANNEL && this.bank.rhythmKeyMap;
|
|
206
|
+
const patch = rhythm ? Math.min(Math.max(note, 27), 88) + 101 : this.channels[channel].patch;
|
|
207
|
+
if (patch !== voice.patch)
|
|
208
|
+
this.setPatch(v, patch);
|
|
209
|
+
voice.velocity = velocity;
|
|
210
|
+
voice.note = note;
|
|
211
|
+
this.updateLevels(v);
|
|
212
|
+
this.setNote(v, note, true);
|
|
213
|
+
}
|
|
214
|
+
voiceOff(v) {
|
|
215
|
+
const voice = this.voices[v];
|
|
216
|
+
voice.note = -1;
|
|
217
|
+
voice.sustained = false;
|
|
218
|
+
if (voice.keyOn) {
|
|
219
|
+
voice.keyOn = false;
|
|
220
|
+
this.chip.write(0xb0 + v, this.lastB0[v] & ~0x20);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
lastB0 = new Array(VOICES).fill(0);
|
|
224
|
+
setNote(v, note, keyOn) {
|
|
225
|
+
const voice = this.voices[v];
|
|
226
|
+
const channel = voice.channel;
|
|
227
|
+
let pitch = note;
|
|
228
|
+
const map = this.bank.rhythmKeyMap;
|
|
229
|
+
if (channel === RHYTHM_CHANNEL && map)
|
|
230
|
+
pitch = map[Math.min(Math.max(note, 27), 88) - 27];
|
|
231
|
+
const bend = ((this.channels[channel].pitchWheel - 8192) / 8192) * BEND_RANGE;
|
|
232
|
+
const freq = 440 * Math.pow(2, (pitch + bend - 69) / 12);
|
|
233
|
+
let block = Math.min(7, Math.max(0, Math.floor(pitch / 12) - 1));
|
|
234
|
+
let fnum = Math.round((freq * (1 << 20)) / (OPL_RATE * (1 << block)));
|
|
235
|
+
while (fnum > 1023 && block < 7)
|
|
236
|
+
fnum = Math.round((freq * (1 << 20)) / (OPL_RATE * (1 << ++block)));
|
|
237
|
+
fnum = Math.min(fnum, 1023);
|
|
238
|
+
this.chip.write(0xa0 + v, fnum & 0xff);
|
|
239
|
+
const b0 = (keyOn ? 0x20 : 0) | (block << 2) | (fnum >> 8);
|
|
240
|
+
this.lastB0[v] = b0;
|
|
241
|
+
this.chip.write(0xb0 + v, b0);
|
|
242
|
+
voice.keyOn = keyOn;
|
|
243
|
+
}
|
|
244
|
+
setPatch(v, patch) {
|
|
245
|
+
const ins = this.bank.instruments[patch] ?? this.bank.instruments[0];
|
|
246
|
+
if (!ins)
|
|
247
|
+
return;
|
|
248
|
+
this.voices[v].patch = patch;
|
|
249
|
+
const base = VOICE_OFFSET[v];
|
|
250
|
+
for (const [offset, op] of [[base, ins.mod], [base + 3, ins.car]]) {
|
|
251
|
+
this.chip.write(0x20 + offset, (op.am << 7) | (op.vibrato << 6) | (op.sustained << 5) | (op.ksr << 4) | op.mult);
|
|
252
|
+
this.chip.write(0x40 + offset, (op.ksl << 6) | op.level);
|
|
253
|
+
this.chip.write(0x60 + offset, (op.attack << 4) | op.decay);
|
|
254
|
+
this.chip.write(0x80 + offset, (op.sustainLevel << 4) | op.release);
|
|
255
|
+
this.chip.write(0xe0 + offset, op.wave);
|
|
256
|
+
}
|
|
257
|
+
this.chip.write(0xc0 + v, (ins.feedback << 1) | (ins.fm ? 0 : 1));
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Output level = master × note (or instrument) level × channel volume. In FM mode only the
|
|
261
|
+
* carrier's level changes loudness (the modulator's sets the timbre); additive voices
|
|
262
|
+
* scale both operators.
|
|
263
|
+
*/
|
|
264
|
+
updateLevels(v) {
|
|
265
|
+
const voice = this.voices[v];
|
|
266
|
+
const ins = this.bank.instruments[voice.patch];
|
|
267
|
+
if (!ins)
|
|
268
|
+
return;
|
|
269
|
+
const ch = this.channels[voice.channel];
|
|
270
|
+
const master = this.masterVolume > 0 ? Math.min(this.masterVolume + 3, 15) : 0;
|
|
271
|
+
const level = (op) => {
|
|
272
|
+
const base = ch.velocityEnabled ? voice.velocity : 63 - op.level;
|
|
273
|
+
return 63 - ((master * base * (ch.volume + 1)) >> 11);
|
|
274
|
+
};
|
|
275
|
+
const offset = VOICE_OFFSET[v];
|
|
276
|
+
this.chip.write(0x40 + offset + 3, (ins.car.ksl << 6) | level(ins.car));
|
|
277
|
+
if (!ins.fm)
|
|
278
|
+
this.chip.write(0x40 + offset, (ins.mod.ksl << 6) | level(ins.mod));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type { MidiOutput } from "./midi.ts";
|
|
2
|
+
import { OPL_RATE } from "./opl2.ts";
|
|
3
|
+
|
|
4
|
+
/** Anything that accepts OPL register writes: the emulator, or a queue to a worklet. */
|
|
5
|
+
export interface OplChip {
|
|
6
|
+
write(reg: number, value: number): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface OperatorPatch {
|
|
10
|
+
ksl: number;
|
|
11
|
+
mult: number;
|
|
12
|
+
attack: number;
|
|
13
|
+
sustainLevel: number;
|
|
14
|
+
sustained: number;
|
|
15
|
+
decay: number;
|
|
16
|
+
release: number;
|
|
17
|
+
level: number; // total level: 0 = loudest, 63 = silent
|
|
18
|
+
am: number;
|
|
19
|
+
vibrato: number;
|
|
20
|
+
ksr: number;
|
|
21
|
+
wave: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AdLibInstrument {
|
|
25
|
+
mod: OperatorPatch;
|
|
26
|
+
car: OperatorPatch;
|
|
27
|
+
feedback: number;
|
|
28
|
+
fm: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AdLibBank {
|
|
32
|
+
instruments: AdLibInstrument[];
|
|
33
|
+
/** Channel 9 note → pitch to play its percussion instrument at (notes 27..88). */
|
|
34
|
+
rhythmKeyMap: Uint8Array | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parses the AdLib instrument bank (patch resource 3). SCI1.1/SCI2 banks are 190 × 28-byte
|
|
39
|
+
* instruments followed by a 62-byte rhythm key map (5,382 bytes); older ones have 48 or 96.
|
|
40
|
+
*
|
|
41
|
+
* Instrument: modulator (13 bytes) + carrier (13 bytes) + 2 waveform bytes. Operator bytes:
|
|
42
|
+
* KSL, MULT, FB, AR, SL, EGT, DR, RR, TL, AM, VIB, KSR, CON. FB and CON are only meaningful
|
|
43
|
+
* on the modulator; CON = 1 means FM (the chip's register bit is the inverse).
|
|
44
|
+
*/
|
|
45
|
+
export function parseAdLibBank(data: Uint8Array): AdLibBank {
|
|
46
|
+
const count = Math.floor(Math.min(data.length, 190 * 28) / 28);
|
|
47
|
+
const op = (b: Uint8Array, wave: number): OperatorPatch => ({
|
|
48
|
+
ksl: b[0]! & 3,
|
|
49
|
+
mult: b[1]! & 0x0f,
|
|
50
|
+
attack: b[3]! & 0x0f,
|
|
51
|
+
sustainLevel: b[4]! & 0x0f,
|
|
52
|
+
sustained: b[5]! ? 1 : 0,
|
|
53
|
+
decay: b[6]! & 0x0f,
|
|
54
|
+
release: b[7]! & 0x0f,
|
|
55
|
+
level: b[8]! & 0x3f,
|
|
56
|
+
am: b[9]! ? 1 : 0,
|
|
57
|
+
vibrato: b[10]! ? 1 : 0,
|
|
58
|
+
ksr: b[11]! ? 1 : 0,
|
|
59
|
+
wave: wave & 3,
|
|
60
|
+
});
|
|
61
|
+
const instruments = Array.from({ length: count }, (_, i) => {
|
|
62
|
+
const b = data.subarray(i * 28, i * 28 + 28);
|
|
63
|
+
return { mod: op(b.subarray(0, 13), b[26]!), car: op(b.subarray(13, 26), b[27]!), feedback: b[2]! & 7, fm: b[12] === 1 };
|
|
64
|
+
});
|
|
65
|
+
return { instruments, rhythmKeyMap: data.length >= 190 * 28 + 62 ? data.slice(190 * 28, 190 * 28 + 62) : undefined };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Maps MIDI note velocity (0..63) onto the driver's level curve (from Sierra's SCI1.1 driver). */
|
|
69
|
+
const VELOCITY_MAP = [
|
|
70
|
+
0x00, 0x0c, 0x0d, 0x0e, 0x0f, 0x11, 0x12, 0x13, 0x14, 0x16, 0x17, 0x18, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
|
|
71
|
+
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x2f, 0x30, 0x31, 0x32,
|
|
72
|
+
0x32, 0x33, 0x34, 0x34, 0x35, 0x36, 0x36, 0x37, 0x38, 0x38, 0x39, 0x3a, 0x3a, 0x3b, 0x3b, 0x3c, 0x3c, 0x3c, 0x3d,
|
|
73
|
+
0x3d, 0x3d, 0x3e, 0x3e, 0x3e, 0x3e, 0x3f,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/** Operator register offsets of each of the 9 voices' modulators (carrier = +3). */
|
|
77
|
+
const VOICE_OFFSET = [0x00, 0x01, 0x02, 0x08, 0x09, 0x0a, 0x10, 0x11, 0x12];
|
|
78
|
+
const VOICES = 9;
|
|
79
|
+
const RHYTHM_CHANNEL = 9;
|
|
80
|
+
const BEND_RANGE = 2; // semitones
|
|
81
|
+
|
|
82
|
+
interface Voice {
|
|
83
|
+
channel: number; // -1 = unassigned
|
|
84
|
+
note: number; // -1 = free
|
|
85
|
+
patch: number;
|
|
86
|
+
velocity: number;
|
|
87
|
+
age: number;
|
|
88
|
+
sustained: boolean;
|
|
89
|
+
keyOn: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface ChannelState {
|
|
93
|
+
patch: number;
|
|
94
|
+
volume: number;
|
|
95
|
+
pan: number;
|
|
96
|
+
hold: boolean;
|
|
97
|
+
pitchWheel: number;
|
|
98
|
+
lastVoice: number;
|
|
99
|
+
velocityEnabled: boolean;
|
|
100
|
+
/** Voices claimed by songs but not yet available. */
|
|
101
|
+
wanted: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sierra's AdLib driver, reimplemented: turns MIDI into OPL2 register writes.
|
|
106
|
+
* The chip has 9 voices; Sierra's AdLib arrangements ask for a fixed number per channel
|
|
107
|
+
* (1 each in the scores we tested), and a channel steals its own oldest voice when it runs out.
|
|
108
|
+
*/
|
|
109
|
+
export class AdLibDriver implements MidiOutput {
|
|
110
|
+
private readonly voices: Voice[] = Array.from({ length: VOICES }, () => ({ channel: -1, note: -1, patch: -1, velocity: 0, age: 0, sustained: false, keyOn: false }));
|
|
111
|
+
private readonly channels: ChannelState[] = Array.from({ length: 16 }, () => ({
|
|
112
|
+
patch: 0, volume: 63, pan: 64, hold: false, pitchWheel: 8192, lastVoice: 0, velocityEnabled: false, wanted: 0,
|
|
113
|
+
}));
|
|
114
|
+
private masterVolume = 15;
|
|
115
|
+
|
|
116
|
+
constructor(
|
|
117
|
+
private readonly chip: OplChip,
|
|
118
|
+
private readonly bank: AdLibBank,
|
|
119
|
+
) {
|
|
120
|
+
chip.write(0x01, 0x20); // enable waveform select
|
|
121
|
+
chip.write(0xbd, 0x00); // melodic mode, shallow tremolo/vibrato
|
|
122
|
+
for (let v = 0; v < VOICES; v++) chip.write(0xb0 + v, 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setMasterVolume(volume: number): void {
|
|
126
|
+
this.masterVolume = volume & 0x0f;
|
|
127
|
+
for (let v = 0; v < VOICES; v++) if (this.voices[v]!.note !== -1) this.updateLevels(v);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Voice mapping: give `channel` `count` more voices (negative releases them). */
|
|
131
|
+
claimVoices(channel: number, count: number): void {
|
|
132
|
+
const ch = this.channels[channel]!;
|
|
133
|
+
if (count < 0) {
|
|
134
|
+
let release = -count;
|
|
135
|
+
for (let v = VOICES - 1; v >= 0 && release > 0; v--) {
|
|
136
|
+
if (this.voices[v]!.channel === channel) {
|
|
137
|
+
this.voiceOff(v);
|
|
138
|
+
this.voices[v]!.channel = -1;
|
|
139
|
+
release--;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
ch.wanted = Math.max(0, ch.wanted - release);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
ch.wanted += count;
|
|
146
|
+
this.assignFreeVoices();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private assignFreeVoices(): void {
|
|
150
|
+
for (let c = 0; c < 16; c++) {
|
|
151
|
+
const ch = this.channels[c]!;
|
|
152
|
+
for (let v = 0; v < VOICES && ch.wanted > 0; v++) {
|
|
153
|
+
if (this.voices[v]!.channel === -1) {
|
|
154
|
+
this.voices[v]!.channel = c;
|
|
155
|
+
ch.wanted--;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
send(bytes: number[]): void {
|
|
162
|
+
const [status = 0, a = 0, b = 0] = bytes;
|
|
163
|
+
const channel = status & 0x0f;
|
|
164
|
+
const ch = this.channels[channel]!;
|
|
165
|
+
switch (status & 0xf0) {
|
|
166
|
+
case 0x80: return this.noteOff(channel, a);
|
|
167
|
+
case 0x90: return b === 0 ? this.noteOff(channel, a) : this.noteOn(channel, a, b);
|
|
168
|
+
case 0xc0:
|
|
169
|
+
ch.patch = a;
|
|
170
|
+
return;
|
|
171
|
+
case 0xe0:
|
|
172
|
+
ch.pitchWheel = (b << 7) | a;
|
|
173
|
+
for (let v = 0; v < VOICES; v++) {
|
|
174
|
+
const voice = this.voices[v]!;
|
|
175
|
+
if (voice.channel === channel && voice.note !== -1) this.setNote(v, voice.note, voice.keyOn);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
case 0xb0:
|
|
179
|
+
switch (a) {
|
|
180
|
+
case 7: // volume
|
|
181
|
+
ch.volume = b;
|
|
182
|
+
for (let v = 0; v < VOICES; v++) if (this.voices[v]!.channel === channel && this.voices[v]!.note !== -1) this.updateLevels(v);
|
|
183
|
+
return;
|
|
184
|
+
case 10:
|
|
185
|
+
ch.pan = b;
|
|
186
|
+
return;
|
|
187
|
+
case 64: // hold pedal
|
|
188
|
+
ch.hold = b !== 0;
|
|
189
|
+
if (!ch.hold) {
|
|
190
|
+
for (let v = 0; v < VOICES; v++) {
|
|
191
|
+
const voice = this.voices[v]!;
|
|
192
|
+
if (voice.channel === channel && voice.sustained) (voice.sustained = false), this.voiceOff(v);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
case 0x4e: // velocity sensitivity on/off
|
|
197
|
+
ch.velocityEnabled = b !== 0;
|
|
198
|
+
return;
|
|
199
|
+
case 123: // all notes off
|
|
200
|
+
for (let v = 0; v < VOICES; v++) if (this.voices[v]!.channel === channel) this.voiceOff(v);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private noteOn(channel: number, note: number, velocity: number): void {
|
|
207
|
+
const vel = VELOCITY_MAP[velocity >> 1]!;
|
|
208
|
+
// Retrigger if this note is already sounding on the channel.
|
|
209
|
+
for (let v = 0; v < VOICES; v++) {
|
|
210
|
+
const voice = this.voices[v]!;
|
|
211
|
+
if (voice.channel === channel && voice.note === note) {
|
|
212
|
+
this.voiceOff(v);
|
|
213
|
+
return this.voiceOn(v, note, vel);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const v = this.findVoice(channel);
|
|
217
|
+
if (v !== -1) this.voiceOn(v, note, vel);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private noteOff(channel: number, note: number): void {
|
|
221
|
+
for (let v = 0; v < VOICES; v++) {
|
|
222
|
+
const voice = this.voices[v]!;
|
|
223
|
+
if (voice.channel !== channel || voice.note !== note) continue;
|
|
224
|
+
if (this.channels[channel]!.hold) voice.sustained = true;
|
|
225
|
+
else this.voiceOff(v);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** A free voice owned by the channel, else its oldest; else any unowned voice. */
|
|
230
|
+
private findVoice(channel: number): number {
|
|
231
|
+
const ch = this.channels[channel]!;
|
|
232
|
+
let oldest = -1;
|
|
233
|
+
for (let i = 0; i < VOICES; i++) {
|
|
234
|
+
const v = (ch.lastVoice + i + 1) % VOICES;
|
|
235
|
+
const voice = this.voices[v]!;
|
|
236
|
+
if (voice.channel !== channel) continue;
|
|
237
|
+
if (voice.note === -1) return (ch.lastVoice = v);
|
|
238
|
+
if (oldest === -1 || voice.age > this.voices[oldest]!.age) oldest = v;
|
|
239
|
+
}
|
|
240
|
+
if (oldest !== -1) {
|
|
241
|
+
this.voiceOff(oldest);
|
|
242
|
+
return (ch.lastVoice = oldest);
|
|
243
|
+
}
|
|
244
|
+
// Channel has no voices of its own (e.g. never claimed): borrow an idle unowned one.
|
|
245
|
+
const idle = this.voices.findIndex((voice) => voice.channel === -1 && voice.note === -1);
|
|
246
|
+
if (idle !== -1) this.voices[idle]!.channel = channel;
|
|
247
|
+
return idle;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private voiceOn(v: number, note: number, velocity: number): void {
|
|
251
|
+
const voice = this.voices[v]!;
|
|
252
|
+
const channel = voice.channel;
|
|
253
|
+
for (const other of this.voices) other.age++;
|
|
254
|
+
voice.age = 0;
|
|
255
|
+
const rhythm = channel === RHYTHM_CHANNEL && this.bank.rhythmKeyMap;
|
|
256
|
+
const patch = rhythm ? Math.min(Math.max(note, 27), 88) + 101 : this.channels[channel]!.patch;
|
|
257
|
+
if (patch !== voice.patch) this.setPatch(v, patch);
|
|
258
|
+
voice.velocity = velocity;
|
|
259
|
+
voice.note = note;
|
|
260
|
+
this.updateLevels(v);
|
|
261
|
+
this.setNote(v, note, true);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private voiceOff(v: number): void {
|
|
265
|
+
const voice = this.voices[v]!;
|
|
266
|
+
voice.note = -1;
|
|
267
|
+
voice.sustained = false;
|
|
268
|
+
if (voice.keyOn) {
|
|
269
|
+
voice.keyOn = false;
|
|
270
|
+
this.chip.write(0xb0 + v, this.lastB0[v]! & ~0x20);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private readonly lastB0 = new Array<number>(VOICES).fill(0);
|
|
275
|
+
|
|
276
|
+
private setNote(v: number, note: number, keyOn: boolean): void {
|
|
277
|
+
const voice = this.voices[v]!;
|
|
278
|
+
const channel = voice.channel;
|
|
279
|
+
let pitch = note;
|
|
280
|
+
const map = this.bank.rhythmKeyMap;
|
|
281
|
+
if (channel === RHYTHM_CHANNEL && map) pitch = map[Math.min(Math.max(note, 27), 88) - 27]!;
|
|
282
|
+
const bend = ((this.channels[channel]!.pitchWheel - 8192) / 8192) * BEND_RANGE;
|
|
283
|
+
const freq = 440 * Math.pow(2, (pitch + bend - 69) / 12);
|
|
284
|
+
let block = Math.min(7, Math.max(0, Math.floor(pitch / 12) - 1));
|
|
285
|
+
let fnum = Math.round((freq * (1 << 20)) / (OPL_RATE * (1 << block)));
|
|
286
|
+
while (fnum > 1023 && block < 7) fnum = Math.round((freq * (1 << 20)) / (OPL_RATE * (1 << ++block)));
|
|
287
|
+
fnum = Math.min(fnum, 1023);
|
|
288
|
+
this.chip.write(0xa0 + v, fnum & 0xff);
|
|
289
|
+
const b0 = (keyOn ? 0x20 : 0) | (block << 2) | (fnum >> 8);
|
|
290
|
+
this.lastB0[v] = b0;
|
|
291
|
+
this.chip.write(0xb0 + v, b0);
|
|
292
|
+
voice.keyOn = keyOn;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private setPatch(v: number, patch: number): void {
|
|
296
|
+
const ins = this.bank.instruments[patch] ?? this.bank.instruments[0];
|
|
297
|
+
if (!ins) return;
|
|
298
|
+
this.voices[v]!.patch = patch;
|
|
299
|
+
const base = VOICE_OFFSET[v]!;
|
|
300
|
+
for (const [offset, op] of [[base, ins.mod], [base + 3, ins.car]] as const) {
|
|
301
|
+
this.chip.write(0x20 + offset, (op.am << 7) | (op.vibrato << 6) | (op.sustained << 5) | (op.ksr << 4) | op.mult);
|
|
302
|
+
this.chip.write(0x40 + offset, (op.ksl << 6) | op.level);
|
|
303
|
+
this.chip.write(0x60 + offset, (op.attack << 4) | op.decay);
|
|
304
|
+
this.chip.write(0x80 + offset, (op.sustainLevel << 4) | op.release);
|
|
305
|
+
this.chip.write(0xe0 + offset, op.wave);
|
|
306
|
+
}
|
|
307
|
+
this.chip.write(0xc0 + v, (ins.feedback << 1) | (ins.fm ? 0 : 1));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Output level = master × note (or instrument) level × channel volume. In FM mode only the
|
|
312
|
+
* carrier's level changes loudness (the modulator's sets the timbre); additive voices
|
|
313
|
+
* scale both operators.
|
|
314
|
+
*/
|
|
315
|
+
private updateLevels(v: number): void {
|
|
316
|
+
const voice = this.voices[v]!;
|
|
317
|
+
const ins = this.bank.instruments[voice.patch];
|
|
318
|
+
if (!ins) return;
|
|
319
|
+
const ch = this.channels[voice.channel]!;
|
|
320
|
+
const master = this.masterVolume > 0 ? Math.min(this.masterVolume + 3, 15) : 0;
|
|
321
|
+
const level = (op: OperatorPatch) => {
|
|
322
|
+
const base = ch.velocityEnabled ? voice.velocity : 63 - op.level;
|
|
323
|
+
return 63 - ((master * base * (ch.volume + 1)) >> 11);
|
|
324
|
+
};
|
|
325
|
+
const offset = VOICE_OFFSET[v]!;
|
|
326
|
+
this.chip.write(0x40 + offset + 3, (ins.car.ksl << 6) | level(ins.car));
|
|
327
|
+
if (!ins.fm) this.chip.write(0x40 + offset, (ins.mod.ksl << 6) | level(ins.mod));
|
|
328
|
+
}
|
|
329
|
+
}
|