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.
Files changed (357) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -2
  4. package/apps/viewer/editor.html +43 -0
  5. package/apps/viewer/explore.html +27 -0
  6. package/apps/viewer/index.html +26 -0
  7. package/apps/viewer/package.json +27 -0
  8. package/apps/viewer/play.html +26 -0
  9. package/apps/viewer/plugin.d.ts +18 -0
  10. package/apps/viewer/plugin.js +75 -0
  11. package/apps/viewer/plugin.ts +84 -0
  12. package/apps/viewer/src/adlib.d.ts +20 -0
  13. package/apps/viewer/src/adlib.js +55 -0
  14. package/apps/viewer/src/adlib.ts +62 -0
  15. package/apps/viewer/src/app.d.ts +5 -0
  16. package/apps/viewer/src/app.js +5 -0
  17. package/apps/viewer/src/app.ts +5 -0
  18. package/apps/viewer/src/asset-index.d.ts +22 -0
  19. package/apps/viewer/src/asset-index.js +74 -0
  20. package/apps/viewer/src/asset-index.ts +90 -0
  21. package/apps/viewer/src/audio.d.ts +28 -0
  22. package/apps/viewer/src/audio.js +102 -0
  23. package/apps/viewer/src/audio.ts +110 -0
  24. package/apps/viewer/src/editor-handles.d.ts +63 -0
  25. package/apps/viewer/src/editor-handles.js +332 -0
  26. package/apps/viewer/src/editor-handles.ts +368 -0
  27. package/apps/viewer/src/editor-languages.d.ts +23 -0
  28. package/apps/viewer/src/editor-languages.js +157 -0
  29. package/apps/viewer/src/editor-languages.ts +135 -0
  30. package/apps/viewer/src/editor.css +213 -0
  31. package/apps/viewer/src/editor.d.ts +1 -0
  32. package/apps/viewer/src/editor.js +393 -0
  33. package/apps/viewer/src/editor.ts +412 -0
  34. package/apps/viewer/src/explore/classes.d.ts +7 -0
  35. package/apps/viewer/src/explore/classes.js +63 -0
  36. package/apps/viewer/src/explore/classes.ts +61 -0
  37. package/apps/viewer/src/explore/context.d.ts +33 -0
  38. package/apps/viewer/src/explore/context.js +96 -0
  39. package/apps/viewer/src/explore/context.ts +124 -0
  40. package/apps/viewer/src/explore/explore.css +93 -0
  41. package/apps/viewer/src/explore/explore.d.ts +1 -0
  42. package/apps/viewer/src/explore/explore.js +102 -0
  43. package/apps/viewer/src/explore/explore.ts +104 -0
  44. package/apps/viewer/src/explore/inspect.d.ts +5 -0
  45. package/apps/viewer/src/explore/inspect.js +208 -0
  46. package/apps/viewer/src/explore/inspect.ts +279 -0
  47. package/apps/viewer/src/explore/live.d.ts +22 -0
  48. package/apps/viewer/src/explore/live.js +318 -0
  49. package/apps/viewer/src/explore/live.ts +365 -0
  50. package/apps/viewer/src/explore/map.d.ts +11 -0
  51. package/apps/viewer/src/explore/map.js +182 -0
  52. package/apps/viewer/src/explore/map.ts +193 -0
  53. package/apps/viewer/src/files.d.ts +21 -0
  54. package/apps/viewer/src/files.js +69 -0
  55. package/apps/viewer/src/files.ts +72 -0
  56. package/apps/viewer/src/game-profile.d.ts +19 -0
  57. package/apps/viewer/src/game-profile.js +4 -0
  58. package/apps/viewer/src/game-profile.ts +26 -0
  59. package/apps/viewer/src/main.d.ts +1 -0
  60. package/apps/viewer/src/main.js +473 -0
  61. package/apps/viewer/src/main.ts +527 -0
  62. package/apps/viewer/src/music.d.ts +14 -0
  63. package/apps/viewer/src/music.js +32 -0
  64. package/apps/viewer/src/music.ts +34 -0
  65. package/apps/viewer/src/opl-worklet.d.ts +1 -0
  66. package/apps/viewer/src/opl-worklet.js +47 -0
  67. package/apps/viewer/src/opl-worklet.ts +55 -0
  68. package/apps/viewer/src/overlay.d.ts +31 -0
  69. package/apps/viewer/src/overlay.js +103 -0
  70. package/apps/viewer/src/overlay.ts +123 -0
  71. package/apps/viewer/src/play.css +14 -0
  72. package/apps/viewer/src/play.d.ts +1 -0
  73. package/apps/viewer/src/play.js +179 -0
  74. package/apps/viewer/src/play.ts +177 -0
  75. package/apps/viewer/src/saves.d.ts +10 -0
  76. package/apps/viewer/src/saves.js +50 -0
  77. package/apps/viewer/src/saves.ts +51 -0
  78. package/apps/viewer/src/session.d.ts +91 -0
  79. package/apps/viewer/src/session.js +301 -0
  80. package/apps/viewer/src/session.ts +321 -0
  81. package/apps/viewer/src/sound-preview.d.ts +7 -0
  82. package/apps/viewer/src/sound-preview.js +214 -0
  83. package/apps/viewer/src/sound-preview.ts +228 -0
  84. package/apps/viewer/src/style.css +64 -0
  85. package/apps/viewer/vite.config.ts +25 -0
  86. package/bin/sci-ts.mjs +9 -0
  87. package/lib/994.sc +49 -0
  88. package/lib/995.sc +53 -0
  89. package/lib/996.sc +299 -0
  90. package/lib/997.sc +198 -0
  91. package/lib/998.sc +338 -0
  92. package/lib/999.sc +189 -0
  93. package/lib/resources.d.ts +2 -0
  94. package/lib/resources.js +84 -0
  95. package/lib/resources.ts +88 -0
  96. package/lib/system.sh +62 -0
  97. package/package.json +75 -4
  98. package/packages/content/room.schema.json +267 -0
  99. package/packages/content/src/index.d.ts +3 -0
  100. package/packages/content/src/index.js +3 -0
  101. package/packages/content/src/index.ts +3 -0
  102. package/packages/content/src/room.d.ts +111 -0
  103. package/packages/content/src/room.js +886 -0
  104. package/packages/content/src/room.ts +1011 -0
  105. package/packages/content/src/target.d.ts +80 -0
  106. package/packages/content/src/target.js +1 -0
  107. package/packages/content/src/target.ts +60 -0
  108. package/packages/content/src/yarn.d.ts +90 -0
  109. package/packages/content/src/yarn.js +214 -0
  110. package/packages/content/src/yarn.ts +261 -0
  111. package/packages/sci/src/analysis/game.d.ts +90 -0
  112. package/packages/sci/src/analysis/game.js +221 -0
  113. package/packages/sci/src/analysis/game.ts +305 -0
  114. package/packages/sci/src/analysis/runtime.d.ts +58 -0
  115. package/packages/sci/src/analysis/runtime.js +133 -0
  116. package/packages/sci/src/analysis/runtime.ts +163 -0
  117. package/packages/sci/src/audio/adlib.d.ts +73 -0
  118. package/packages/sci/src/audio/adlib.js +280 -0
  119. package/packages/sci/src/audio/adlib.ts +329 -0
  120. package/packages/sci/src/audio/index.d.ts +43 -0
  121. package/packages/sci/src/audio/index.js +99 -0
  122. package/packages/sci/src/audio/index.ts +122 -0
  123. package/packages/sci/src/audio/midi.d.ts +92 -0
  124. package/packages/sci/src/audio/midi.js +242 -0
  125. package/packages/sci/src/audio/midi.ts +262 -0
  126. package/packages/sci/src/audio/opl2.d.ts +74 -0
  127. package/packages/sci/src/audio/opl2.js +283 -0
  128. package/packages/sci/src/audio/opl2.ts +283 -0
  129. package/packages/sci/src/audio/sol.d.ts +32 -0
  130. package/packages/sci/src/audio/sol.js +81 -0
  131. package/packages/sci/src/audio/sol.ts +100 -0
  132. package/packages/sci/src/audio/write.d.ts +35 -0
  133. package/packages/sci/src/audio/write.js +91 -0
  134. package/packages/sci/src/audio/write.ts +108 -0
  135. package/packages/sci/src/gfx/cel.d.ts +36 -0
  136. package/packages/sci/src/gfx/cel.js +82 -0
  137. package/packages/sci/src/gfx/cel.ts +110 -0
  138. package/packages/sci/src/gfx/palette.d.ts +24 -0
  139. package/packages/sci/src/gfx/palette.js +49 -0
  140. package/packages/sci/src/gfx/palette.ts +56 -0
  141. package/packages/sci/src/gfx/pic-file.d.ts +46 -0
  142. package/packages/sci/src/gfx/pic-file.js +114 -0
  143. package/packages/sci/src/gfx/pic-file.ts +155 -0
  144. package/packages/sci/src/gfx/pic.d.ts +27 -0
  145. package/packages/sci/src/gfx/pic.js +34 -0
  146. package/packages/sci/src/gfx/pic.ts +53 -0
  147. package/packages/sci/src/gfx/quantize.d.ts +42 -0
  148. package/packages/sci/src/gfx/quantize.js +199 -0
  149. package/packages/sci/src/gfx/quantize.ts +218 -0
  150. package/packages/sci/src/gfx/view-file.d.ts +62 -0
  151. package/packages/sci/src/gfx/view-file.js +182 -0
  152. package/packages/sci/src/gfx/view-file.ts +233 -0
  153. package/packages/sci/src/gfx/view.d.ts +28 -0
  154. package/packages/sci/src/gfx/view.js +45 -0
  155. package/packages/sci/src/gfx/view.ts +62 -0
  156. package/packages/sci/src/index.d.ts +38 -0
  157. package/packages/sci/src/index.js +38 -0
  158. package/packages/sci/src/index.ts +38 -0
  159. package/packages/sci/src/motion/pathfinding.d.ts +40 -0
  160. package/packages/sci/src/motion/pathfinding.js +140 -0
  161. package/packages/sci/src/motion/pathfinding.ts +147 -0
  162. package/packages/sci/src/resource/archive.d.ts +16 -0
  163. package/packages/sci/src/resource/archive.js +51 -0
  164. package/packages/sci/src/resource/archive.ts +56 -0
  165. package/packages/sci/src/resource/lzs.d.ts +12 -0
  166. package/packages/sci/src/resource/lzs.js +64 -0
  167. package/packages/sci/src/resource/lzs.ts +66 -0
  168. package/packages/sci/src/resource/manager.d.ts +45 -0
  169. package/packages/sci/src/resource/manager.js +158 -0
  170. package/packages/sci/src/resource/manager.ts +184 -0
  171. package/packages/sci/src/resource/map.d.ts +18 -0
  172. package/packages/sci/src/resource/map.js +34 -0
  173. package/packages/sci/src/resource/map.ts +44 -0
  174. package/packages/sci/src/resource/types.d.ts +37 -0
  175. package/packages/sci/src/resource/types.js +45 -0
  176. package/packages/sci/src/resource/types.ts +57 -0
  177. package/packages/sci/src/script/assembly.d.ts +67 -0
  178. package/packages/sci/src/script/assembly.js +748 -0
  179. package/packages/sci/src/script/assembly.ts +714 -0
  180. package/packages/sci/src/script/compiler.d.ts +75 -0
  181. package/packages/sci/src/script/compiler.js +993 -0
  182. package/packages/sci/src/script/compiler.ts +1040 -0
  183. package/packages/sci/src/script/disasm.d.ts +81 -0
  184. package/packages/sci/src/script/disasm.js +318 -0
  185. package/packages/sci/src/script/disasm.ts +323 -0
  186. package/packages/sci/src/script/kernel-names.d.ts +6 -0
  187. package/packages/sci/src/script/kernel-names.js +165 -0
  188. package/packages/sci/src/script/kernel-names.ts +165 -0
  189. package/packages/sci/src/script/opcodes.d.ts +22 -0
  190. package/packages/sci/src/script/opcodes.js +45 -0
  191. package/packages/sci/src/script/opcodes.ts +72 -0
  192. package/packages/sci/src/script/script.d.ts +56 -0
  193. package/packages/sci/src/script/script.js +63 -0
  194. package/packages/sci/src/script/script.ts +111 -0
  195. package/packages/sci/src/script/sexpr.d.ts +45 -0
  196. package/packages/sci/src/script/sexpr.js +151 -0
  197. package/packages/sci/src/script/sexpr.ts +139 -0
  198. package/packages/sci/src/script/vocab.d.ts +15 -0
  199. package/packages/sci/src/script/vocab.js +44 -0
  200. package/packages/sci/src/script/vocab.ts +48 -0
  201. package/packages/sci/src/script/world.d.ts +25 -0
  202. package/packages/sci/src/script/world.js +65 -0
  203. package/packages/sci/src/script/world.ts +70 -0
  204. package/packages/sci/src/text/font.d.ts +37 -0
  205. package/packages/sci/src/text/font.js +103 -0
  206. package/packages/sci/src/text/font.ts +125 -0
  207. package/packages/sci/src/text/message.d.ts +69 -0
  208. package/packages/sci/src/text/message.js +171 -0
  209. package/packages/sci/src/text/message.ts +205 -0
  210. package/packages/sci/src/vm/kernels/arrays.d.ts +19 -0
  211. package/packages/sci/src/vm/kernels/arrays.js +182 -0
  212. package/packages/sci/src/vm/kernels/arrays.ts +164 -0
  213. package/packages/sci/src/vm/kernels/audio.d.ts +70 -0
  214. package/packages/sci/src/vm/kernels/audio.js +347 -0
  215. package/packages/sci/src/vm/kernels/audio.ts +349 -0
  216. package/packages/sci/src/vm/kernels/core.d.ts +5 -0
  217. package/packages/sci/src/vm/kernels/core.js +87 -0
  218. package/packages/sci/src/vm/kernels/core.ts +91 -0
  219. package/packages/sci/src/vm/kernels/graphics.d.ts +141 -0
  220. package/packages/sci/src/vm/kernels/graphics.js +467 -0
  221. package/packages/sci/src/vm/kernels/graphics.ts +502 -0
  222. package/packages/sci/src/vm/kernels/index.d.ts +8 -0
  223. package/packages/sci/src/vm/kernels/index.js +29 -0
  224. package/packages/sci/src/vm/kernels/index.ts +32 -0
  225. package/packages/sci/src/vm/kernels/lists.d.ts +6 -0
  226. package/packages/sci/src/vm/kernels/lists.js +127 -0
  227. package/packages/sci/src/vm/kernels/lists.ts +111 -0
  228. package/packages/sci/src/vm/kernels/motion.d.ts +7 -0
  229. package/packages/sci/src/vm/kernels/motion.js +204 -0
  230. package/packages/sci/src/vm/kernels/motion.ts +194 -0
  231. package/packages/sci/src/vm/kernels/palette.d.ts +82 -0
  232. package/packages/sci/src/vm/kernels/palette.js +292 -0
  233. package/packages/sci/src/vm/kernels/palette.ts +303 -0
  234. package/packages/sci/src/vm/kernels/saves.d.ts +45 -0
  235. package/packages/sci/src/vm/kernels/saves.js +123 -0
  236. package/packages/sci/src/vm/kernels/saves.ts +149 -0
  237. package/packages/sci/src/vm/kernels/system.d.ts +62 -0
  238. package/packages/sci/src/vm/kernels/system.js +110 -0
  239. package/packages/sci/src/vm/kernels/system.ts +128 -0
  240. package/packages/sci/src/vm/kernels/text.d.ts +13 -0
  241. package/packages/sci/src/vm/kernels/text.js +338 -0
  242. package/packages/sci/src/vm/kernels/text.ts +328 -0
  243. package/packages/sci/src/vm/memory.d.ts +138 -0
  244. package/packages/sci/src/vm/memory.js +168 -0
  245. package/packages/sci/src/vm/memory.ts +266 -0
  246. package/packages/sci/src/vm/replace.d.ts +11 -0
  247. package/packages/sci/src/vm/replace.js +14 -0
  248. package/packages/sci/src/vm/replace.ts +16 -0
  249. package/packages/sci/src/vm/savegame.d.ts +89 -0
  250. package/packages/sci/src/vm/savegame.js +161 -0
  251. package/packages/sci/src/vm/savegame.ts +188 -0
  252. package/packages/sci/src/vm/value.d.ts +17 -0
  253. package/packages/sci/src/vm/value.js +12 -0
  254. package/packages/sci/src/vm/value.ts +25 -0
  255. package/packages/sci/src/vm/vm.d.ts +155 -0
  256. package/packages/sci/src/vm/vm.js +1053 -0
  257. package/packages/sci/src/vm/vm.ts +914 -0
  258. package/tools/adlib-render.d.ts +1 -0
  259. package/tools/adlib-render.js +49 -0
  260. package/tools/adlib-render.ts +42 -0
  261. package/tools/analyze.d.ts +1 -0
  262. package/tools/analyze.js +25 -0
  263. package/tools/analyze.ts +25 -0
  264. package/tools/art/build.d.ts +34 -0
  265. package/tools/art/build.js +175 -0
  266. package/tools/art/build.ts +153 -0
  267. package/tools/art.d.ts +1 -0
  268. package/tools/art.js +31 -0
  269. package/tools/art.ts +29 -0
  270. package/tools/asm.d.ts +1 -0
  271. package/tools/asm.js +61 -0
  272. package/tools/asm.ts +55 -0
  273. package/tools/cli.d.ts +1 -0
  274. package/tools/cli.js +99 -0
  275. package/tools/cli.ts +100 -0
  276. package/tools/editor-server.d.ts +29 -0
  277. package/tools/editor-server.js +162 -0
  278. package/tools/editor-server.ts +176 -0
  279. package/tools/explore.d.ts +1 -0
  280. package/tools/explore.js +305 -0
  281. package/tools/explore.ts +250 -0
  282. package/tools/game/audio.d.ts +19 -0
  283. package/tools/game/audio.js +143 -0
  284. package/tools/game/audio.ts +130 -0
  285. package/tools/game/build.d.ts +46 -0
  286. package/tools/game/build.js +259 -0
  287. package/tools/game/build.ts +283 -0
  288. package/tools/game/defaults.d.ts +26 -0
  289. package/tools/game/defaults.js +82 -0
  290. package/tools/game/defaults.ts +83 -0
  291. package/tools/game/font.d.ts +3 -0
  292. package/tools/game/font.js +124 -0
  293. package/tools/game/font.ts +126 -0
  294. package/tools/game/kit.d.ts +7 -0
  295. package/tools/game/kit.js +7 -0
  296. package/tools/game/kit.ts +10 -0
  297. package/tools/game/new.d.ts +8 -0
  298. package/tools/game/new.js +39 -0
  299. package/tools/game/new.ts +38 -0
  300. package/tools/game/target.d.ts +7 -0
  301. package/tools/game/target.js +41 -0
  302. package/tools/game/target.ts +42 -0
  303. package/tools/game/template/resources.ts.tmpl +43 -0
  304. package/tools/game/template/rooms/1.room.yaml +9 -0
  305. package/tools/game/template/rooms/1.yarn +17 -0
  306. package/tools/game/template/scripts/0.sc +24 -0
  307. package/tools/game.d.ts +1 -0
  308. package/tools/game.js +77 -0
  309. package/tools/game.ts +70 -0
  310. package/tools/mod-build.d.ts +28 -0
  311. package/tools/mod-build.js +173 -0
  312. package/tools/mod-build.ts +179 -0
  313. package/tools/mod-target.d.ts +16 -0
  314. package/tools/mod-target.js +26 -0
  315. package/tools/mod-target.ts +32 -0
  316. package/tools/mod.d.ts +1 -0
  317. package/tools/mod.js +47 -0
  318. package/tools/mod.ts +45 -0
  319. package/tools/msg.d.ts +1 -0
  320. package/tools/msg.js +40 -0
  321. package/tools/msg.ts +36 -0
  322. package/tools/node-files.d.ts +3 -0
  323. package/tools/node-files.js +34 -0
  324. package/tools/node-files.ts +33 -0
  325. package/tools/patches.d.ts +3 -0
  326. package/tools/patches.js +13 -0
  327. package/tools/patches.ts +13 -0
  328. package/tools/pic-build.d.ts +9 -0
  329. package/tools/pic-build.js +26 -0
  330. package/tools/pic-build.ts +27 -0
  331. package/tools/pic.d.ts +1 -0
  332. package/tools/pic.js +64 -0
  333. package/tools/pic.ts +61 -0
  334. package/tools/play.d.ts +1 -0
  335. package/tools/play.js +54 -0
  336. package/tools/play.ts +54 -0
  337. package/tools/png.d.ts +16 -0
  338. package/tools/png.js +124 -0
  339. package/tools/png.ts +123 -0
  340. package/tools/res.d.ts +1 -0
  341. package/tools/res.js +48 -0
  342. package/tools/res.ts +50 -0
  343. package/tools/scenario.d.ts +1 -0
  344. package/tools/scenario.js +74 -0
  345. package/tools/scenario.ts +80 -0
  346. package/tools/site/build.d.ts +8 -0
  347. package/tools/site/build.js +110 -0
  348. package/tools/site/build.ts +116 -0
  349. package/tools/site/markdown.d.ts +11 -0
  350. package/tools/site/markdown.js +88 -0
  351. package/tools/site/markdown.ts +84 -0
  352. package/tools/view-build.d.ts +34 -0
  353. package/tools/view-build.js +47 -0
  354. package/tools/view-build.ts +71 -0
  355. package/tools/view.d.ts +1 -0
  356. package/tools/view.js +54 -0
  357. package/tools/view.ts +52 -0
@@ -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
+
16
+ const MULT = [0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 12, 15, 15];
17
+ const KSL_ROM = [0, 32, 40, 45, 48, 51, 53, 55, 56, 58, 59, 60, 61, 62, 63, 64];
18
+ const KSL_SHIFT = [8, 1, 2, 0];
19
+ /** Attenuation units are 0.1875 dB; 511 units ≈ 96 dB = silence. */
20
+ const ATT_MAX = 511;
21
+ const ATT_DB = 0.1875;
22
+
23
+ const SINE = new Float32Array(1024).map((_, i) => Math.sin(((i + 0.5) / 1024) * 2 * Math.PI));
24
+ const AMPLITUDE = new Float32Array(2048).map((_, u) => Math.pow(10, (-u * ATT_DB) / 20));
25
+
26
+ /** OPL2 waveforms: sine, half-sine, abs-sine, and "quarter" pulses of abs-sine. */
27
+ function wave(form: number, phase: number): number {
28
+ const i = Math.floor(phase * 1024) & 1023;
29
+ const s = SINE[i]!;
30
+ switch (form) {
31
+ case 1: return s > 0 ? s : 0;
32
+ case 2: return Math.abs(s);
33
+ case 3: return (i & 256) === 0 ? Math.abs(s) : 0;
34
+ default: return s;
35
+ }
36
+ }
37
+
38
+ enum Env { Attack, Decay, Sustain, Release, Off }
39
+
40
+ /** Datasheet timings (ms) for the slowest rate (effective rate 4); each +4 halves them. */
41
+ const ATTACK_MS = 2826.24;
42
+ const DECAY_MS = 39280.64;
43
+
44
+ function rateTime(baseMs: number, rate: number): number {
45
+ if (rate < 4) return Infinity;
46
+ const r = Math.min(rate, 60);
47
+ return (baseMs * 4) / ((4 + (r & 3)) * Math.pow(2, (r >> 2) - 1));
48
+ }
49
+
50
+ class Operator {
51
+ // Registers
52
+ am = false;
53
+ vib = false;
54
+ sustained = false; // EG type: hold at sustain level until key-off
55
+ ksr = false;
56
+ mult = 1;
57
+ ksl = 0;
58
+ tl = 0;
59
+ ar = 0;
60
+ dr = 0;
61
+ sl = 0;
62
+ rr = 0;
63
+ wave = 0;
64
+
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
+
77
+ private effectiveRate(r: number): number {
78
+ return r === 0 ? 0 : Math.min(63, r * 4 + (this.ksr ? this.keyCode : this.keyCode >> 2));
79
+ }
80
+
81
+ updateRates(): void {
82
+ const at = rateTime(ATTACK_MS, this.effectiveRate(this.ar));
83
+ // Exponential attack: reach ~0 attenuation from full in `at`.
84
+ this.attackK = this.effectiveRate(this.ar) >= 60 ? 1 : at === Infinity ? 0 : 1 - Math.pow(1 / 512, 1000 / (at * OPL_RATE));
85
+ const step = (rate: number) => {
86
+ const t = rateTime(DECAY_MS, this.effectiveRate(rate));
87
+ return t === Infinity ? 0 : (ATT_MAX * 1000) / (t * OPL_RATE);
88
+ };
89
+ this.decayStep = step(this.dr);
90
+ this.releaseStep = step(this.rr);
91
+ }
92
+
93
+ get sustainLevel(): number {
94
+ return (this.sl === 15 ? 31 : this.sl) * 16; // 3 dB steps; 15 means 93 dB
95
+ }
96
+
97
+ keyOn(): void {
98
+ this.phase = 0;
99
+ this.state = Env.Attack;
100
+ }
101
+
102
+ keyOff(): void {
103
+ if (this.state !== Env.Off) this.state = Env.Release;
104
+ }
105
+
106
+ stepEnvelope(): void {
107
+ switch (this.state) {
108
+ case Env.Attack:
109
+ this.att -= (this.att + 1) * this.attackK;
110
+ if (this.att <= 0) {
111
+ this.att = 0;
112
+ this.state = Env.Decay;
113
+ }
114
+ break;
115
+ case Env.Decay:
116
+ this.att += this.decayStep;
117
+ if (this.att >= this.sustainLevel) {
118
+ this.att = this.sustainLevel;
119
+ this.state = Env.Sustain;
120
+ }
121
+ break;
122
+ case Env.Sustain:
123
+ // Percussive envelopes (EG type 0) keep falling at the release rate.
124
+ if (!this.sustained) this.att += this.releaseStep;
125
+ break;
126
+ case Env.Release:
127
+ this.att += this.releaseStep;
128
+ break;
129
+ }
130
+ if (this.att >= ATT_MAX && this.state !== Env.Attack) {
131
+ this.att = ATT_MAX;
132
+ if (this.state === Env.Release || this.state === Env.Sustain) this.state = Env.Off;
133
+ }
134
+ }
135
+
136
+ /** Output in -1..1 for a phase offset (modulation) in cycles. */
137
+ output(mod: number, tremolo: number, waveEnabled: boolean): number {
138
+ if (this.state === Env.Off) return 0;
139
+ const att = this.att + this.tl * 4 + this.kslAtt + (this.am ? tremolo : 0);
140
+ if (att >= ATT_MAX) return 0;
141
+ return wave(waveEnabled ? this.wave : 0, this.phase + mod) * AMPLITUDE[att | 0]!;
142
+ }
143
+ }
144
+
145
+ class Channel {
146
+ readonly ops = [new Operator(), new Operator()] as const;
147
+ fnum = 0;
148
+ block = 0;
149
+ keyOn = false;
150
+ feedback = 0;
151
+ additive = false;
152
+ fb1 = 0;
153
+ fb2 = 0;
154
+
155
+ updatePitch(): void {
156
+ const keyCode = (this.block << 1) | ((this.fnum >> 9) & 1);
157
+ const kslBase = Math.max(0, KSL_ROM[this.fnum >> 6]! * 4 - (8 - this.block) * 32);
158
+ for (const op of this.ops) {
159
+ op.inc = (this.fnum * Math.pow(2, this.block) * MULT[op.mult]!) / (1 << 20);
160
+ op.keyCode = keyCode;
161
+ op.kslAtt = kslBase >> KSL_SHIFT[op.ksl]!;
162
+ op.updateRates();
163
+ }
164
+ }
165
+ }
166
+
167
+ /** Operator register offsets 0x00-0x15 → (channel, operator), with gaps at 6-7 and 0x0e-0x0f. */
168
+ function slotFor(offset: number): [channel: number, op: 0 | 1] | undefined {
169
+ const group = offset >> 3;
170
+ const within = offset & 7;
171
+ if (group > 2 || within > 5) return undefined;
172
+ return [group * 3 + (within % 3), within >= 3 ? 1 : 0];
173
+ }
174
+
175
+ export class Opl2 {
176
+ readonly channels = Array.from({ length: 9 }, () => new Channel());
177
+ private waveSelect = false;
178
+ private deepTremolo = false;
179
+ private deepVibrato = false;
180
+ private lfoTime = 0;
181
+
182
+ write(reg: number, value: number): void {
183
+ reg &= 0xff;
184
+ value &= 0xff;
185
+ if (reg === 0x01) {
186
+ this.waveSelect = (value & 0x20) !== 0;
187
+ return;
188
+ }
189
+ if (reg === 0xbd) {
190
+ this.deepTremolo = (value & 0x80) !== 0;
191
+ this.deepVibrato = (value & 0x40) !== 0;
192
+ return;
193
+ }
194
+ const hi = reg & 0xe0;
195
+ if (hi >= 0x20 && hi <= 0x80 || hi === 0xe0) {
196
+ const slot = slotFor(reg & 0x1f);
197
+ if (!slot) return;
198
+ const ch = this.channels[slot[0]]!;
199
+ const op = ch.ops[slot[1]];
200
+ switch (hi) {
201
+ case 0x20:
202
+ op.am = (value & 0x80) !== 0;
203
+ op.vib = (value & 0x40) !== 0;
204
+ op.sustained = (value & 0x20) !== 0;
205
+ op.ksr = (value & 0x10) !== 0;
206
+ op.mult = value & 0x0f;
207
+ ch.updatePitch();
208
+ break;
209
+ case 0x40:
210
+ op.ksl = value >> 6;
211
+ op.tl = value & 0x3f;
212
+ ch.updatePitch();
213
+ break;
214
+ case 0x60:
215
+ op.ar = value >> 4;
216
+ op.dr = value & 0x0f;
217
+ op.updateRates();
218
+ break;
219
+ case 0x80:
220
+ op.sl = value >> 4;
221
+ op.rr = value & 0x0f;
222
+ op.updateRates();
223
+ break;
224
+ case 0xe0:
225
+ op.wave = value & 3;
226
+ break;
227
+ }
228
+ return;
229
+ }
230
+ const c = reg & 0x0f;
231
+ if (c > 8) return;
232
+ const ch = this.channels[c]!;
233
+ switch (reg & 0xf0) {
234
+ case 0xa0:
235
+ ch.fnum = (ch.fnum & 0x300) | value;
236
+ ch.updatePitch();
237
+ break;
238
+ case 0xb0: {
239
+ ch.fnum = (ch.fnum & 0xff) | ((value & 3) << 8);
240
+ ch.block = (value >> 2) & 7;
241
+ ch.updatePitch();
242
+ const on = (value & 0x20) !== 0;
243
+ if (on && !ch.keyOn) for (const op of ch.ops) op.keyOn();
244
+ if (!on && ch.keyOn) for (const op of ch.ops) 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
+
255
+ /** Renders `count` mono samples at OPL_RATE into `out` starting at `offset`. */
256
+ generate(out: Float32Array, offset = 0, count = out.length - offset): void {
257
+ const tremoloDepth = this.deepTremolo ? 4.8 / ATT_DB : 1 / ATT_DB;
258
+ const vibratoCents = this.deepVibrato ? 14 : 7;
259
+ for (let n = 0; n < count; n++) {
260
+ this.lfoTime += 1 / OPL_RATE;
261
+ const tremolo = tremoloDepth * (0.5 - 0.5 * Math.cos(2 * Math.PI * 3.7 * this.lfoTime));
262
+ const vibrato = Math.pow(2, (vibratoCents * Math.sin(2 * Math.PI * 6.07 * this.lfoTime)) / 1200);
263
+ let mix = 0;
264
+ for (const ch of this.channels) {
265
+ const [mod, car] = ch.ops;
266
+ if (mod.state === Env.Off && car.state === Env.Off) 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
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Sierra SOL audio, used for speech (RESOURCE.AUD) and digital effects (RESOURCE.SFX).
3
+ *
4
+ * +0 u8 resource type (0x8D = audio) +1 u8 header size (data starts at 2 + this)
5
+ * +2 "SOL\0" +6 u16 sample rate +8 u8 flags +9 u32 data size
6
+ *
7
+ * Flags: 1 = DPCM compressed, 4 = 16-bit, 8 = signed, 0x10 = stereo.
8
+ * DPCM stores one delta per byte (16-bit) or per nibble (8-bit); tables and the x86-style
9
+ * 16-bit wraparound follow the original interpreter (as documented by ScummVM).
10
+ */
11
+ export interface SolAudio {
12
+ rate: number;
13
+ channels: number;
14
+ /** Samples in -1..1, interleaved if stereo. */
15
+ samples: Float32Array;
16
+ }
17
+ export declare const SolFlag: {
18
+ readonly Compressed: 1;
19
+ readonly Bits16: 4;
20
+ readonly Signed: 8;
21
+ readonly Stereo: 16;
22
+ };
23
+ export interface SolHeader {
24
+ rate: number;
25
+ flags: number;
26
+ dataOffset: number;
27
+ dataSize: number;
28
+ }
29
+ export declare function parseSolHeader(data: Uint8Array): SolHeader | undefined;
30
+ /** Samples a SOL clip of `dataBytes` bytes decodes to, per channel. */
31
+ export declare function solSampleCount(flags: number, dataBytes: number): number;
32
+ export declare function decodeSol(data: Uint8Array): SolAudio | undefined;
@@ -0,0 +1,81 @@
1
+ export const SolFlag = { Compressed: 1, Bits16: 4, Signed: 8, Stereo: 0x10 };
2
+ export function parseSolHeader(data) {
3
+ if (data[2] !== 0x53 || data[3] !== 0x4f || data[4] !== 0x4c)
4
+ return undefined; // "SOL"
5
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
6
+ const headerSize = data[1];
7
+ return {
8
+ rate: v.getUint16(6, true),
9
+ flags: data[8],
10
+ dataOffset: 2 + headerSize,
11
+ dataSize: headerSize >= 11 ? v.getUint32(9, true) : data.length - 2 - headerSize,
12
+ };
13
+ }
14
+ /** Samples a SOL clip of `dataBytes` bytes decodes to, per channel. */
15
+ export function solSampleCount(flags, dataBytes) {
16
+ const channels = flags & SolFlag.Stereo ? 2 : 1;
17
+ const bytesPerSample = flags & SolFlag.Compressed ? (flags & SolFlag.Bits16 ? 1 : 0.5) : flags & SolFlag.Bits16 ? 2 : 1;
18
+ return Math.floor(dataBytes / bytesPerSample / channels);
19
+ }
20
+ const DPCM16 = [
21
+ 0x0000, 0x0008, 0x0010, 0x0020, 0x0030, 0x0040, 0x0050, 0x0060, 0x0070, 0x0080, 0x0090, 0x00a0, 0x00b0, 0x00c0,
22
+ 0x00d0, 0x00e0, 0x00f0, 0x0100, 0x0110, 0x0120, 0x0130, 0x0140, 0x0150, 0x0160, 0x0170, 0x0180, 0x0190, 0x01a0,
23
+ 0x01b0, 0x01c0, 0x01d0, 0x01e0, 0x01f0, 0x0200, 0x0208, 0x0210, 0x0218, 0x0220, 0x0228, 0x0230, 0x0238, 0x0240,
24
+ 0x0248, 0x0250, 0x0258, 0x0260, 0x0268, 0x0270, 0x0278, 0x0280, 0x0288, 0x0290, 0x0298, 0x02a0, 0x02a8, 0x02b0,
25
+ 0x02b8, 0x02c0, 0x02c8, 0x02d0, 0x02d8, 0x02e0, 0x02e8, 0x02f0, 0x02f8, 0x0300, 0x0308, 0x0310, 0x0318, 0x0320,
26
+ 0x0328, 0x0330, 0x0338, 0x0340, 0x0348, 0x0350, 0x0358, 0x0360, 0x0368, 0x0370, 0x0378, 0x0380, 0x0388, 0x0390,
27
+ 0x0398, 0x03a0, 0x03a8, 0x03b0, 0x03b8, 0x03c0, 0x03c8, 0x03d0, 0x03d8, 0x03e0, 0x03e8, 0x03f0, 0x03f8, 0x0400,
28
+ 0x0440, 0x0480, 0x04c0, 0x0500, 0x0540, 0x0580, 0x05c0, 0x0600, 0x0640, 0x0680, 0x06c0, 0x0700, 0x0740, 0x0780,
29
+ 0x07c0, 0x0800, 0x0900, 0x0a00, 0x0b00, 0x0c00, 0x0d00, 0x0e00, 0x0f00, 0x1000, 0x1400, 0x1800, 0x1c00, 0x2000,
30
+ 0x3000, 0x4000,
31
+ ];
32
+ const DPCM8 = [0, 1, 2, 3, 6, 10, 15, 21];
33
+ export function decodeSol(data) {
34
+ const h = parseSolHeader(data);
35
+ if (!h)
36
+ return undefined;
37
+ const channels = h.flags & SolFlag.Stereo ? 2 : 1;
38
+ const body = data.subarray(h.dataOffset, Math.min(data.length, h.dataOffset + h.dataSize));
39
+ const out = new Float32Array(solSampleCount(h.flags, body.length) * channels);
40
+ if (h.flags & SolFlag.Compressed) {
41
+ if (h.flags & SolFlag.Bits16) {
42
+ const last = [0, 0];
43
+ for (let i = 0; i < out.length; i++) {
44
+ const ch = i % channels;
45
+ const delta = body[i];
46
+ let s = last[ch] + (delta & 0x80 ? -DPCM16[delta & 0x7f] : DPCM16[delta]);
47
+ if (s > 32767)
48
+ s -= 65536; // x86 16-bit register wraparound
49
+ else if (s < -32768)
50
+ s += 65536;
51
+ last[ch] = s;
52
+ out[i] = s / 32768;
53
+ }
54
+ }
55
+ else {
56
+ let sample = 0x80;
57
+ let o = 0;
58
+ const nibble = (d) => {
59
+ const prev = sample;
60
+ sample = Math.max(0, Math.min(255, sample + (d & 8 ? -DPCM8[d & 7] : DPCM8[d & 7])));
61
+ out[o++] = ((prev + sample) / 2 - 128) / 128;
62
+ };
63
+ for (let i = 0; i < body.length && o < out.length; i++) {
64
+ nibble(body[i] >> 4);
65
+ if (o < out.length)
66
+ nibble(body[i] & 0xf);
67
+ }
68
+ }
69
+ }
70
+ else if (h.flags & SolFlag.Bits16) {
71
+ const v = new DataView(body.buffer, body.byteOffset, body.byteLength);
72
+ for (let i = 0; i < out.length; i++)
73
+ out[i] = v.getInt16(i * 2, true) / 32768;
74
+ }
75
+ else {
76
+ const signed = (h.flags & SolFlag.Signed) !== 0;
77
+ for (let i = 0; i < out.length; i++)
78
+ out[i] = signed ? ((body[i] << 24) >> 24) / 128 : (body[i] - 128) / 128;
79
+ }
80
+ return { rate: h.rate, channels, samples: out };
81
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Sierra SOL audio, used for speech (RESOURCE.AUD) and digital effects (RESOURCE.SFX).
3
+ *
4
+ * +0 u8 resource type (0x8D = audio) +1 u8 header size (data starts at 2 + this)
5
+ * +2 "SOL\0" +6 u16 sample rate +8 u8 flags +9 u32 data size
6
+ *
7
+ * Flags: 1 = DPCM compressed, 4 = 16-bit, 8 = signed, 0x10 = stereo.
8
+ * DPCM stores one delta per byte (16-bit) or per nibble (8-bit); tables and the x86-style
9
+ * 16-bit wraparound follow the original interpreter (as documented by ScummVM).
10
+ */
11
+ export interface SolAudio {
12
+ rate: number;
13
+ channels: number;
14
+ /** Samples in -1..1, interleaved if stereo. */
15
+ samples: Float32Array;
16
+ }
17
+
18
+ export const SolFlag = { Compressed: 1, Bits16: 4, Signed: 8, Stereo: 0x10 } as const;
19
+
20
+ export interface SolHeader {
21
+ rate: number;
22
+ flags: number;
23
+ dataOffset: number;
24
+ dataSize: number;
25
+ }
26
+
27
+ export function parseSolHeader(data: Uint8Array): SolHeader | undefined {
28
+ if (data[2] !== 0x53 || data[3] !== 0x4f || data[4] !== 0x4c) return undefined; // "SOL"
29
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
30
+ const headerSize = data[1]!;
31
+ return {
32
+ rate: v.getUint16(6, true),
33
+ flags: data[8]!,
34
+ dataOffset: 2 + headerSize,
35
+ dataSize: headerSize >= 11 ? v.getUint32(9, true) : data.length - 2 - headerSize,
36
+ };
37
+ }
38
+
39
+ /** Samples a SOL clip of `dataBytes` bytes decodes to, per channel. */
40
+ export function solSampleCount(flags: number, dataBytes: number): number {
41
+ const channels = flags & SolFlag.Stereo ? 2 : 1;
42
+ const bytesPerSample = flags & SolFlag.Compressed ? (flags & SolFlag.Bits16 ? 1 : 0.5) : flags & SolFlag.Bits16 ? 2 : 1;
43
+ return Math.floor(dataBytes / bytesPerSample / channels);
44
+ }
45
+
46
+ const DPCM16 = [
47
+ 0x0000, 0x0008, 0x0010, 0x0020, 0x0030, 0x0040, 0x0050, 0x0060, 0x0070, 0x0080, 0x0090, 0x00a0, 0x00b0, 0x00c0,
48
+ 0x00d0, 0x00e0, 0x00f0, 0x0100, 0x0110, 0x0120, 0x0130, 0x0140, 0x0150, 0x0160, 0x0170, 0x0180, 0x0190, 0x01a0,
49
+ 0x01b0, 0x01c0, 0x01d0, 0x01e0, 0x01f0, 0x0200, 0x0208, 0x0210, 0x0218, 0x0220, 0x0228, 0x0230, 0x0238, 0x0240,
50
+ 0x0248, 0x0250, 0x0258, 0x0260, 0x0268, 0x0270, 0x0278, 0x0280, 0x0288, 0x0290, 0x0298, 0x02a0, 0x02a8, 0x02b0,
51
+ 0x02b8, 0x02c0, 0x02c8, 0x02d0, 0x02d8, 0x02e0, 0x02e8, 0x02f0, 0x02f8, 0x0300, 0x0308, 0x0310, 0x0318, 0x0320,
52
+ 0x0328, 0x0330, 0x0338, 0x0340, 0x0348, 0x0350, 0x0358, 0x0360, 0x0368, 0x0370, 0x0378, 0x0380, 0x0388, 0x0390,
53
+ 0x0398, 0x03a0, 0x03a8, 0x03b0, 0x03b8, 0x03c0, 0x03c8, 0x03d0, 0x03d8, 0x03e0, 0x03e8, 0x03f0, 0x03f8, 0x0400,
54
+ 0x0440, 0x0480, 0x04c0, 0x0500, 0x0540, 0x0580, 0x05c0, 0x0600, 0x0640, 0x0680, 0x06c0, 0x0700, 0x0740, 0x0780,
55
+ 0x07c0, 0x0800, 0x0900, 0x0a00, 0x0b00, 0x0c00, 0x0d00, 0x0e00, 0x0f00, 0x1000, 0x1400, 0x1800, 0x1c00, 0x2000,
56
+ 0x3000, 0x4000,
57
+ ];
58
+ const DPCM8 = [0, 1, 2, 3, 6, 10, 15, 21];
59
+
60
+ export function decodeSol(data: Uint8Array): SolAudio | undefined {
61
+ const h = parseSolHeader(data);
62
+ if (!h) return undefined;
63
+ const channels = h.flags & SolFlag.Stereo ? 2 : 1;
64
+ const body = data.subarray(h.dataOffset, Math.min(data.length, h.dataOffset + h.dataSize));
65
+ const out = new Float32Array(solSampleCount(h.flags, body.length) * channels);
66
+
67
+ if (h.flags & SolFlag.Compressed) {
68
+ if (h.flags & SolFlag.Bits16) {
69
+ const last = [0, 0];
70
+ for (let i = 0; i < out.length; i++) {
71
+ const ch = i % channels;
72
+ const delta = body[i]!;
73
+ let s = last[ch]! + (delta & 0x80 ? -DPCM16[delta & 0x7f]! : DPCM16[delta]!);
74
+ if (s > 32767) s -= 65536; // x86 16-bit register wraparound
75
+ else if (s < -32768) s += 65536;
76
+ last[ch] = s;
77
+ out[i] = s / 32768;
78
+ }
79
+ } else {
80
+ let sample = 0x80;
81
+ let o = 0;
82
+ const nibble = (d: number) => {
83
+ const prev = sample;
84
+ sample = Math.max(0, Math.min(255, sample + (d & 8 ? -DPCM8[d & 7]! : DPCM8[d & 7]!)));
85
+ out[o++] = ((prev + sample) / 2 - 128) / 128;
86
+ };
87
+ for (let i = 0; i < body.length && o < out.length; i++) {
88
+ nibble(body[i]! >> 4);
89
+ if (o < out.length) nibble(body[i]! & 0xf);
90
+ }
91
+ }
92
+ } else if (h.flags & SolFlag.Bits16) {
93
+ const v = new DataView(body.buffer, body.byteOffset, body.byteLength);
94
+ for (let i = 0; i < out.length; i++) out[i] = v.getInt16(i * 2, true) / 32768;
95
+ } else {
96
+ const signed = (h.flags & SolFlag.Signed) !== 0;
97
+ for (let i = 0; i < out.length; i++) out[i] = signed ? ((body[i]! << 24) >> 24) / 128 : (body[i]! - 128) / 128;
98
+ }
99
+ return { rate: h.rate, channels, samples: out };
100
+ }
@@ -0,0 +1,35 @@
1
+ import type { MidiEvent } from "./midi.ts";
2
+ /**
3
+ * Writers for sound: sound resources (music, in the layout parseSound reads), SOL clips, and
4
+ * the effects file RESOURCE.SFX with its index, map 65535 (the layout AudioIndex reads).
5
+ */
6
+ export interface SoundChannel {
7
+ /** 0-15; 15 is the control channel (signals and cues to the scripts), not music. */
8
+ midiChannel: number;
9
+ /** Voices it wants from a voice-limited device (AdLib); others ignore it. */
10
+ voices: number;
11
+ /** Absolute ticks (1/60 s); bytes are MIDI messages (their channel nibble is replaced). */
12
+ events: MidiEvent[];
13
+ }
14
+ export interface SoundSpec {
15
+ priority?: number;
16
+ /** One per sound device (SoundDevice): each device plays its own arrangement. */
17
+ tracks: {
18
+ device: number;
19
+ channels: SoundChannel[];
20
+ }[];
21
+ }
22
+ export declare function writeSound(spec: SoundSpec): Uint8Array;
23
+ /** A SOL clip: 16-bit samples at `rate`, uncompressed. */
24
+ export declare function writeSol(samples: Int16Array, rate: number): Uint8Array;
25
+ /**
26
+ * RESOURCE.SFX (the SOL clips, one after another) and its index, map 65535: per clip, u16
27
+ * sound number and u24 offset from the previous clip; 0xFFFF ends it.
28
+ */
29
+ export declare function writeEffects(clips: readonly {
30
+ number: number;
31
+ sol: Uint8Array;
32
+ }[]): {
33
+ sfx: Uint8Array;
34
+ map: Uint8Array;
35
+ };
@@ -0,0 +1,91 @@
1
+ import { SolFlag } from "./sol.js";
2
+ const dataBytes = (status) => ((status & 0xf0) === 0xc0 || (status & 0xf0) === 0xd0 ? 1 : 2);
3
+ /** A channel stream: channel, voices, then delta-timed events with running status, then 0xFC. */
4
+ function channelStream(c) {
5
+ if (c.midiChannel < 0 || c.midiChannel > 15)
6
+ throw new Error(`MIDI channel ${c.midiChannel} isn't 0-15`);
7
+ const out = [c.midiChannel, c.voices & 0x0f];
8
+ const events = [...c.events].sort((a, b) => a.tick - b.tick);
9
+ let tick = 0, running = -1;
10
+ for (const e of events) {
11
+ if (e.tick < tick)
12
+ throw new Error(`event at tick ${e.tick} before ${tick}`);
13
+ let delta = Math.round(e.tick - tick);
14
+ tick += delta;
15
+ // A delta of 240 or more is 0xF8 bytes of 240 each, then the rest.
16
+ for (; delta >= 240; delta -= 240)
17
+ out.push(0xf8);
18
+ out.push(delta);
19
+ const status = (e.bytes[0] & 0xf0) | c.midiChannel;
20
+ if (status < 0x80 || status >= 0xf0)
21
+ throw new Error(`not a channel message: ${e.bytes.map((b) => b.toString(16)).join(" ")}`);
22
+ if (status !== running)
23
+ out.push(status);
24
+ running = status;
25
+ const n = dataBytes(status);
26
+ for (let i = 1; i <= n; i++)
27
+ out.push((e.bytes[i] ?? 0) & 0x7f);
28
+ }
29
+ out.push(0, 0xfc);
30
+ return out;
31
+ }
32
+ export function writeSound(spec) {
33
+ const header = [0xf0, spec.priority ?? 0, 0, 0, 0, 0, 0, 0];
34
+ const streams = spec.tracks.map((t) => t.channels.map(channelStream));
35
+ // Track table: device, 6-byte entries (u16 0, u16 offset, u16 size), 0xFF; then 0xFF.
36
+ const tableSize = spec.tracks.reduce((n, t) => n + 1 + t.channels.length * 6 + 1, 0) + 1;
37
+ let at = header.length + tableSize;
38
+ const table = [];
39
+ const body = [];
40
+ spec.tracks.forEach((t, i) => {
41
+ table.push(t.device);
42
+ for (const s of streams[i]) {
43
+ if (at + s.length > 0xffff)
44
+ throw new Error("sound too large: offsets are 16-bit (64 KB)");
45
+ table.push(0, 0, at & 0xff, at >> 8, s.length & 0xff, s.length >> 8);
46
+ body.push(...s);
47
+ at += s.length;
48
+ }
49
+ table.push(0xff);
50
+ });
51
+ table.push(0xff);
52
+ return Uint8Array.from([...header, ...table, ...body]);
53
+ }
54
+ /** A SOL clip: 16-bit samples at `rate`, uncompressed. */
55
+ export function writeSol(samples, rate) {
56
+ const out = new Uint8Array(13 + samples.length * 2);
57
+ const v = new DataView(out.buffer);
58
+ out[0] = 0x8d; // audio
59
+ out[1] = 11; // header bytes after these two
60
+ out.set([0x53, 0x4f, 0x4c, 0], 2); // "SOL\0"
61
+ v.setUint16(6, rate, true);
62
+ out[8] = SolFlag.Bits16 | SolFlag.Signed;
63
+ v.setUint32(9, samples.length * 2, true);
64
+ samples.forEach((s, i) => v.setInt16(13 + i * 2, s, true));
65
+ return out;
66
+ }
67
+ /**
68
+ * RESOURCE.SFX (the SOL clips, one after another) and its index, map 65535: per clip, u16
69
+ * sound number and u24 offset from the previous clip; 0xFFFF ends it.
70
+ */
71
+ export function writeEffects(clips) {
72
+ const sorted = [...clips].sort((a, b) => a.number - b.number);
73
+ const sfx = new Uint8Array(sorted.reduce((n, c) => n + c.sol.length, 0));
74
+ const map = new Uint8Array(sorted.length * 5 + 5);
75
+ const v = new DataView(map.buffer);
76
+ let at = 0, prev = 0;
77
+ sorted.forEach((c, i) => {
78
+ if (i && c.number === sorted[i - 1].number)
79
+ throw new Error(`sound effect ${c.number} is there twice`);
80
+ if (at - prev > 0xffffff)
81
+ throw new Error("sound effect too large");
82
+ sfx.set(c.sol, at);
83
+ v.setUint16(i * 5, c.number, true);
84
+ const delta = at - prev;
85
+ map.set([delta & 0xff, (delta >> 8) & 0xff, delta >> 16], i * 5 + 2);
86
+ prev = at;
87
+ at += c.sol.length;
88
+ });
89
+ v.setUint16(sorted.length * 5, 0xffff, true);
90
+ return { sfx, map };
91
+ }