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,43 @@
1
+ import type { ResourceManager } from "../resource/manager.ts";
2
+ export type AudioFile = "RESOURCE.AUD" | "RESOURCE.SFX";
3
+ /** Where a clip's bytes live, without having read them. */
4
+ export interface AudioClip {
5
+ file: AudioFile;
6
+ /** Offset of the SOL resource (type byte) in the file. */
7
+ offset: number;
8
+ /** Byte length up to the next clip in the file (an upper bound for the last one). */
9
+ length: number;
10
+ /** Exact duration in 60 Hz ticks, when the header was available at index time. */
11
+ ticks?: number;
12
+ }
13
+ export declare const speechKey: (module: number, noun: number, verb: number, cond: number, seq: number) => string;
14
+ /**
15
+ * Index of every digital clip, built from the audio map resources:
16
+ *
17
+ * Speech maps (one per module): u32 base offset into RESOURCE.AUD, then entries of
18
+ * u32 BIG-endian noun<<24|verb<<16|cond<<8|seq (seq bit 7: u16 lip-sync size follows,
19
+ * u24 delta added to the running offset seq bit 6: u16 extra size follows)
20
+ * terminated by 0xFFFFFFFF. Lip-sync data sits before the SOL audio.
21
+ * Map 65535 (sound effects): u16 number, u24 delta (running offset) into RESOURCE.SFX;
22
+ * 0xFFFF ends it.
23
+ *
24
+ * Lengths come from sorting all offsets per file: each clip runs to the next one.
25
+ */
26
+ export declare class AudioIndex {
27
+ readonly speech: Map<string, AudioClip>;
28
+ readonly effects: Map<number, AudioClip>;
29
+ static build(rm: ResourceManager): AudioIndex;
30
+ /** Estimated duration in 60 Hz ticks, from the clip's byte length. */
31
+ static ticks(clip: AudioClip): number;
32
+ }
33
+ /** What a host (browser, test) provides to actually make noise. */
34
+ export interface AudioOutput {
35
+ play(channel: string, clip: AudioClip, options: {
36
+ loop: boolean;
37
+ volume: number;
38
+ }): void;
39
+ stop(channel: string): void;
40
+ pause(channel: string): void;
41
+ resume(channel: string): void;
42
+ setVolume(channel: string, volume: number): void;
43
+ }
@@ -0,0 +1,99 @@
1
+ import { ResourceType } from "../resource/types.js";
2
+ import { parseSolHeader, solSampleCount } from "./sol.js";
3
+ const SFX_MAP = 65535;
4
+ const SOL_HEADER = 14;
5
+ /** Speech and effects are usually 11,025 Hz, 16-bit DPCM (1 byte per sample). */
6
+ const DEFAULT_RATE = 11025;
7
+ export const speechKey = (module, noun, verb, cond, seq) => `${module}:${noun}:${verb}:${cond}:${seq}`;
8
+ /**
9
+ * Index of every digital clip, built from the audio map resources:
10
+ *
11
+ * Speech maps (one per module): u32 base offset into RESOURCE.AUD, then entries of
12
+ * u32 BIG-endian noun<<24|verb<<16|cond<<8|seq (seq bit 7: u16 lip-sync size follows,
13
+ * u24 delta added to the running offset seq bit 6: u16 extra size follows)
14
+ * terminated by 0xFFFFFFFF. Lip-sync data sits before the SOL audio.
15
+ * Map 65535 (sound effects): u16 number, u24 delta (running offset) into RESOURCE.SFX;
16
+ * 0xFFFF ends it.
17
+ *
18
+ * Lengths come from sorting all offsets per file: each clip runs to the next one.
19
+ */
20
+ export class AudioIndex {
21
+ speech = new Map();
22
+ effects = new Map();
23
+ static build(rm) {
24
+ const index = new AudioIndex();
25
+ const starts = { "RESOURCE.AUD": [], "RESOURCE.SFX": [] };
26
+ const pending = [];
27
+ for (const info of rm.list(ResourceType.Map)) {
28
+ const data = rm.loadSync(info).data;
29
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
30
+ if (info.number === SFX_MAP) {
31
+ let offset = 0;
32
+ for (let p = 0; p + 5 <= data.length; p += 5) {
33
+ const n = v.getUint16(p, true);
34
+ if (n === 0xffff)
35
+ break;
36
+ offset += data[p + 2] | (data[p + 3] << 8) | (data[p + 4] << 16);
37
+ const clip = { file: "RESOURCE.SFX", offset, length: 0 };
38
+ index.effects.set(n, clip);
39
+ starts["RESOURCE.SFX"].push(offset);
40
+ pending.push({ clip, skip: 0 });
41
+ }
42
+ continue;
43
+ }
44
+ let offset = v.getUint32(0, true);
45
+ for (let p = 4; p + 7 <= data.length;) {
46
+ const n = v.getUint32(p, false);
47
+ if (n === 0xffffffff)
48
+ break;
49
+ offset += data[p + 4] | (data[p + 5] << 8) | (data[p + 6] << 16);
50
+ p += 7;
51
+ let skip = 0;
52
+ const seqFlags = n & 0xff;
53
+ starts["RESOURCE.AUD"].push(offset);
54
+ if (seqFlags & 0x80)
55
+ (skip += v.getUint16(p, true)), (p += 2);
56
+ if (seqFlags & 0x40)
57
+ (skip += v.getUint16(p, true)), (p += 2);
58
+ const key = speechKey(info.number, n >>> 24, (n >>> 16) & 0xff, (n >>> 8) & 0xff, seqFlags & 0x3f);
59
+ const clip = { file: "RESOURCE.AUD", offset: offset + skip, length: 0 };
60
+ index.speech.set(key, clip);
61
+ pending.push({ clip, skip });
62
+ }
63
+ }
64
+ // Effects are small and preloaded: take exact durations from their headers.
65
+ const sfx = rm.file("RESOURCE.SFX");
66
+ if (sfx) {
67
+ for (const clip of index.effects.values()) {
68
+ const h = parseSolHeader(sfx.subarray(clip.offset, clip.offset + 16));
69
+ if (h)
70
+ clip.ticks = Math.max(1, Math.round((solSampleCount(h.flags, h.dataSize) / h.rate) * 60));
71
+ }
72
+ }
73
+ for (const file of Object.keys(starts))
74
+ starts[file] = [...new Set(starts[file])].sort((a, b) => a - b);
75
+ for (const { clip, skip } of pending) {
76
+ const list = starts[clip.file];
77
+ const start = clip.offset - skip;
78
+ // Binary search for the next start after this clip.
79
+ let lo = 0, hi = list.length;
80
+ while (lo < hi) {
81
+ const mid = (lo + hi) >> 1;
82
+ if (list[mid] <= start)
83
+ lo = mid + 1;
84
+ else
85
+ hi = mid;
86
+ }
87
+ const next = list[lo] ?? clip.offset + 2_000_000;
88
+ clip.length = Math.max(0, next - clip.offset);
89
+ }
90
+ return index;
91
+ }
92
+ /** Estimated duration in 60 Hz ticks, from the clip's byte length. */
93
+ static ticks(clip) {
94
+ if (clip.ticks)
95
+ return clip.ticks;
96
+ const samples = Math.max(0, clip.length - SOL_HEADER);
97
+ return Math.max(1, Math.round((samples / DEFAULT_RATE) * 60));
98
+ }
99
+ }
@@ -0,0 +1,122 @@
1
+ import type { ResourceManager } from "../resource/manager.ts";
2
+ import { ResourceType } from "../resource/types.ts";
3
+ import { parseSolHeader, solSampleCount } from "./sol.ts";
4
+
5
+ export type AudioFile = "RESOURCE.AUD" | "RESOURCE.SFX";
6
+
7
+ /** Where a clip's bytes live, without having read them. */
8
+ export interface AudioClip {
9
+ file: AudioFile;
10
+ /** Offset of the SOL resource (type byte) in the file. */
11
+ offset: number;
12
+ /** Byte length up to the next clip in the file (an upper bound for the last one). */
13
+ length: number;
14
+ /** Exact duration in 60 Hz ticks, when the header was available at index time. */
15
+ ticks?: number;
16
+ }
17
+
18
+ const SFX_MAP = 65535;
19
+ const SOL_HEADER = 14;
20
+ /** Speech and effects are usually 11,025 Hz, 16-bit DPCM (1 byte per sample). */
21
+ const DEFAULT_RATE = 11025;
22
+
23
+ export const speechKey = (module: number, noun: number, verb: number, cond: number, seq: number) =>
24
+ `${module}:${noun}:${verb}:${cond}:${seq}`;
25
+
26
+ /**
27
+ * Index of every digital clip, built from the audio map resources:
28
+ *
29
+ * Speech maps (one per module): u32 base offset into RESOURCE.AUD, then entries of
30
+ * u32 BIG-endian noun<<24|verb<<16|cond<<8|seq (seq bit 7: u16 lip-sync size follows,
31
+ * u24 delta added to the running offset seq bit 6: u16 extra size follows)
32
+ * terminated by 0xFFFFFFFF. Lip-sync data sits before the SOL audio.
33
+ * Map 65535 (sound effects): u16 number, u24 delta (running offset) into RESOURCE.SFX;
34
+ * 0xFFFF ends it.
35
+ *
36
+ * Lengths come from sorting all offsets per file: each clip runs to the next one.
37
+ */
38
+ export class AudioIndex {
39
+ readonly speech = new Map<string, AudioClip>();
40
+ readonly effects = new Map<number, AudioClip>();
41
+
42
+ static build(rm: ResourceManager): AudioIndex {
43
+ const index = new AudioIndex();
44
+ const starts: Record<AudioFile, number[]> = { "RESOURCE.AUD": [], "RESOURCE.SFX": [] };
45
+ const pending: { clip: AudioClip; skip: number }[] = [];
46
+
47
+ for (const info of rm.list(ResourceType.Map)) {
48
+ const data = rm.loadSync(info).data;
49
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
50
+ if (info.number === SFX_MAP) {
51
+ let offset = 0;
52
+ for (let p = 0; p + 5 <= data.length; p += 5) {
53
+ const n = v.getUint16(p, true);
54
+ if (n === 0xffff) break;
55
+ offset += data[p + 2]! | (data[p + 3]! << 8) | (data[p + 4]! << 16);
56
+ const clip: AudioClip = { file: "RESOURCE.SFX", offset, length: 0 };
57
+ index.effects.set(n, clip);
58
+ starts["RESOURCE.SFX"].push(offset);
59
+ pending.push({ clip, skip: 0 });
60
+ }
61
+ continue;
62
+ }
63
+ let offset = v.getUint32(0, true);
64
+ for (let p = 4; p + 7 <= data.length; ) {
65
+ const n = v.getUint32(p, false);
66
+ if (n === 0xffffffff) break;
67
+ offset += data[p + 4]! | (data[p + 5]! << 8) | (data[p + 6]! << 16);
68
+ p += 7;
69
+ let skip = 0;
70
+ const seqFlags = n & 0xff;
71
+ starts["RESOURCE.AUD"].push(offset);
72
+ if (seqFlags & 0x80) (skip += v.getUint16(p, true)), (p += 2);
73
+ if (seqFlags & 0x40) (skip += v.getUint16(p, true)), (p += 2);
74
+ const key = speechKey(info.number, n >>> 24, (n >>> 16) & 0xff, (n >>> 8) & 0xff, seqFlags & 0x3f);
75
+ const clip: AudioClip = { file: "RESOURCE.AUD", offset: offset + skip, length: 0 };
76
+ index.speech.set(key, clip);
77
+ pending.push({ clip, skip });
78
+ }
79
+ }
80
+
81
+ // Effects are small and preloaded: take exact durations from their headers.
82
+ const sfx = rm.file("RESOURCE.SFX");
83
+ if (sfx) {
84
+ for (const clip of index.effects.values()) {
85
+ const h = parseSolHeader(sfx.subarray(clip.offset, clip.offset + 16));
86
+ if (h) clip.ticks = Math.max(1, Math.round((solSampleCount(h.flags, h.dataSize) / h.rate) * 60));
87
+ }
88
+ }
89
+
90
+ for (const file of Object.keys(starts) as AudioFile[]) starts[file] = [...new Set(starts[file])].sort((a, b) => a - b);
91
+ for (const { clip, skip } of pending) {
92
+ const list = starts[clip.file];
93
+ const start = clip.offset - skip;
94
+ // Binary search for the next start after this clip.
95
+ let lo = 0, hi = list.length;
96
+ while (lo < hi) {
97
+ const mid = (lo + hi) >> 1;
98
+ if (list[mid]! <= start) lo = mid + 1;
99
+ else hi = mid;
100
+ }
101
+ const next = list[lo] ?? clip.offset + 2_000_000;
102
+ clip.length = Math.max(0, next - clip.offset);
103
+ }
104
+ return index;
105
+ }
106
+
107
+ /** Estimated duration in 60 Hz ticks, from the clip's byte length. */
108
+ static ticks(clip: AudioClip): number {
109
+ if (clip.ticks) return clip.ticks;
110
+ const samples = Math.max(0, clip.length - SOL_HEADER);
111
+ return Math.max(1, Math.round((samples / DEFAULT_RATE) * 60));
112
+ }
113
+ }
114
+
115
+ /** What a host (browser, test) provides to actually make noise. */
116
+ export interface AudioOutput {
117
+ play(channel: string, clip: AudioClip, options: { loop: boolean; volume: number }): void;
118
+ stop(channel: string): void;
119
+ pause(channel: string): void;
120
+ resume(channel: string): void;
121
+ setVolume(channel: string, volume: number): void;
122
+ }
@@ -0,0 +1,92 @@
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 declare const SoundDevice: {
17
+ readonly AdLib: 0;
18
+ readonly GeneralMidi: 7;
19
+ readonly Mt32: 12;
20
+ };
21
+ export interface MidiEvent {
22
+ tick: number;
23
+ /** Raw MIDI bytes, e.g. [0x91, 60, 100]. */
24
+ bytes: number[];
25
+ }
26
+ export interface SongChannel {
27
+ midiChannel: number;
28
+ /** Voices this channel wants from a voice-limited device (AdLib: 9 in total). */
29
+ voices: number;
30
+ }
31
+ export interface Song {
32
+ priority: number;
33
+ channels: SongChannel[];
34
+ /** All channels merged, sorted by tick. */
35
+ events: MidiEvent[];
36
+ /** Tick where the last event ends. */
37
+ length: number;
38
+ }
39
+ /** Parses the track for `device` (falling back to MT-32, then any), merging its channels. */
40
+ export declare function parseSound(data: Uint8Array, device?: number): Song | undefined;
41
+ /** Where MIDI bytes go: a SoundFont synth or the AdLib driver. */
42
+ export interface MidiOutput {
43
+ send(bytes: number[]): void;
44
+ /** Voice-limited devices: a song claims (or, with a negative count, releases) voices. */
45
+ claimVoices?(channel: number, count: number): void;
46
+ setMasterVolume?(volume: number): void;
47
+ }
48
+ /** Signal values scripts see on the sound object. */
49
+ export declare const SongSignal: {
50
+ readonly Finished: 65535;
51
+ };
52
+ /**
53
+ * Plays one song against a tick clock. Tracks the control channel so the kernel can
54
+ * report cues/signals, loops at the loop marker, and silences its notes when stopped.
55
+ */
56
+ export declare class SongPlayer {
57
+ readonly song: Song;
58
+ private readonly output;
59
+ loop: boolean;
60
+ volume: number;
61
+ private index;
62
+ private startTick;
63
+ private loopIndex;
64
+ private loopTick;
65
+ private readonly held;
66
+ /** Last channel volume (CC 7) the song asked for, to rescale on volume changes. */
67
+ private readonly channelVolume;
68
+ finished: boolean;
69
+ /** Latest control-channel signal not yet reported (0 = none). */
70
+ signal: number;
71
+ dataInc: number;
72
+ private paused;
73
+ constructor(song: Song, output: MidiOutput | undefined, loop: boolean, volume: number, // 0..127
74
+ now: number);
75
+ /** Sends every event due by tick `now`. */
76
+ update(now: number): void;
77
+ private dispatch;
78
+ /** Releases every note this song is holding. */
79
+ private silence;
80
+ private released;
81
+ stop(): void;
82
+ /** Position in ticks. */
83
+ position(now: number): number;
84
+ /**
85
+ * Jumps to `pos` ticks without sounding notes: replays program, controller and pitch
86
+ * events so the (possibly new) output is in the right state. Used when switching devices.
87
+ */
88
+ seek(pos: number, now: number): void;
89
+ pause(now: number): void;
90
+ resume(now: number): void;
91
+ setVolume(volume: number): void;
92
+ }
@@ -0,0 +1,242 @@
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 };
17
+ const CONTROL_CHANNEL = 15;
18
+ const LOOP_MARKER = 127;
19
+ const CUE_CONTROLLER = 0x60;
20
+ const dataBytes = (status) => ((status & 0xf0) === 0xc0 || (status & 0xf0) === 0xd0 ? 1 : 2);
21
+ function readChannel(data, start, end, midiChannel, out) {
22
+ let pos = start;
23
+ let tick = 0;
24
+ let running = 0;
25
+ while (pos < end) {
26
+ // Delta time.
27
+ let b = data[pos++];
28
+ while (b === 0xf8 && pos < end) {
29
+ tick += 240;
30
+ b = data[pos++];
31
+ }
32
+ tick += b;
33
+ if (pos >= end)
34
+ break;
35
+ let status = data[pos];
36
+ if (status === 0xfc)
37
+ break; // end of stream
38
+ if (status & 0x80)
39
+ pos++;
40
+ else
41
+ status = running; // running status: this byte is data
42
+ if (status === 0xf0) {
43
+ while (pos < end && data[pos] !== 0xf7)
44
+ pos++; // skip sysex
45
+ pos++;
46
+ continue;
47
+ }
48
+ if (status === 0xff) {
49
+ pos += 1; // meta type
50
+ const len = data[pos++];
51
+ pos += len;
52
+ continue;
53
+ }
54
+ if (status < 0x80)
55
+ break; // corrupt stream
56
+ running = status;
57
+ const n = dataBytes(status);
58
+ const bytes = [(status & 0xf0) | midiChannel];
59
+ for (let i = 0; i < n; i++)
60
+ bytes.push(data[pos++] & 0x7f);
61
+ out.push({ tick, bytes });
62
+ }
63
+ return tick;
64
+ }
65
+ /** Parses the track for `device` (falling back to MT-32, then any), merging its channels. */
66
+ export function parseSound(data, device = SoundDevice.GeneralMidi) {
67
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
68
+ let pos = 0;
69
+ let priority = 0;
70
+ if (data[0] === 0xf0) {
71
+ priority = data[1];
72
+ pos = 8;
73
+ }
74
+ const tracks = new Map();
75
+ while (pos < data.length && data[pos] !== 0xff) {
76
+ const type = data[pos++];
77
+ const channels = [];
78
+ while (pos + 6 <= data.length && data[pos] !== 0xff) {
79
+ channels.push({ offset: v.getUint16(pos + 2, true), size: v.getUint16(pos + 4, true) });
80
+ pos += 6;
81
+ }
82
+ pos++;
83
+ tracks.set(type, channels);
84
+ }
85
+ const channels = tracks.get(device) ?? tracks.get(SoundDevice.Mt32);
86
+ if (!channels?.length)
87
+ return undefined;
88
+ const events = [];
89
+ const songChannels = [];
90
+ let length = 0;
91
+ for (const { offset, size } of channels) {
92
+ if (!size || offset + 2 > data.length)
93
+ continue;
94
+ const midiChannel = data[offset] & 0x0f;
95
+ if (midiChannel !== CONTROL_CHANNEL)
96
+ songChannels.push({ midiChannel, voices: data[offset + 1] & 0x0f });
97
+ length = Math.max(length, readChannel(data, offset + 2, Math.min(data.length, offset + size), midiChannel, events));
98
+ }
99
+ // Stable sort keeps each channel's order for events on the same tick.
100
+ events.sort((a, b) => a.tick - b.tick);
101
+ return { priority, channels: songChannels, events, length };
102
+ }
103
+ /** Signal values scripts see on the sound object. */
104
+ export const SongSignal = { Finished: 0xffff };
105
+ /**
106
+ * Plays one song against a tick clock. Tracks the control channel so the kernel can
107
+ * report cues/signals, loops at the loop marker, and silences its notes when stopped.
108
+ */
109
+ export class SongPlayer {
110
+ song;
111
+ output;
112
+ loop;
113
+ volume;
114
+ index = 0;
115
+ startTick = 0;
116
+ loopIndex = 0;
117
+ loopTick = 0;
118
+ held = new Set(); // channel << 8 | note
119
+ /** Last channel volume (CC 7) the song asked for, to rescale on volume changes. */
120
+ channelVolume = new Map();
121
+ finished = false;
122
+ /** Latest control-channel signal not yet reported (0 = none). */
123
+ signal = 0;
124
+ dataInc = 0;
125
+ paused;
126
+ constructor(song, output, loop, volume, // 0..127
127
+ now) {
128
+ this.song = song;
129
+ this.output = output;
130
+ this.loop = loop;
131
+ this.volume = volume;
132
+ this.startTick = now;
133
+ for (const c of song.channels)
134
+ output?.claimVoices?.(c.midiChannel, c.voices);
135
+ }
136
+ /** Sends every event due by tick `now`. */
137
+ update(now) {
138
+ if (this.finished || this.paused !== undefined)
139
+ return;
140
+ const pos = now - this.startTick;
141
+ const events = this.song.events;
142
+ while (this.index < events.length && events[this.index].tick <= pos) {
143
+ this.dispatch(events[this.index], this.index);
144
+ this.index++;
145
+ }
146
+ if (this.index >= events.length && pos >= this.song.length) {
147
+ if (this.loop) {
148
+ this.silence();
149
+ this.index = this.loopIndex;
150
+ this.startTick = now - this.loopTick;
151
+ }
152
+ else {
153
+ this.stop();
154
+ this.finished = true;
155
+ this.signal = SongSignal.Finished;
156
+ }
157
+ }
158
+ }
159
+ dispatch(e, index) {
160
+ const [status = 0, a = 0, b = 0] = e.bytes;
161
+ const channel = status & 0x0f;
162
+ const type = status & 0xf0;
163
+ if (channel === CONTROL_CHANNEL) {
164
+ if (type === 0xc0) {
165
+ if (a === LOOP_MARKER) {
166
+ this.loopIndex = index + 1;
167
+ this.loopTick = e.tick;
168
+ }
169
+ else if (e.tick > 0)
170
+ this.signal = a;
171
+ }
172
+ else if (type === 0xb0 && a === CUE_CONTROLLER) {
173
+ this.dataInc++;
174
+ this.signal = this.dataInc + 127;
175
+ }
176
+ return;
177
+ }
178
+ if (type === 0x90 && b > 0)
179
+ this.held.add((channel << 8) | a);
180
+ else if (type === 0x80 || type === 0x90)
181
+ this.held.delete((channel << 8) | a);
182
+ // Scale channel volume by the sound object's volume.
183
+ if (type === 0xb0 && a === 7) {
184
+ this.channelVolume.set(channel, b);
185
+ return this.output?.send([status, a, Math.round((b * this.volume) / 127)]);
186
+ }
187
+ this.output?.send(e.bytes);
188
+ }
189
+ /** Releases every note this song is holding. */
190
+ silence() {
191
+ for (const key of this.held)
192
+ this.output?.send([0x80 | (key >> 8), key & 0xff, 0]);
193
+ this.held.clear();
194
+ }
195
+ released = false;
196
+ stop() {
197
+ this.silence();
198
+ if (this.released)
199
+ return;
200
+ this.released = true;
201
+ for (const c of this.song.channels)
202
+ this.output?.claimVoices?.(c.midiChannel, -c.voices);
203
+ }
204
+ /** Position in ticks. */
205
+ position(now) {
206
+ return (this.paused ?? now) - this.startTick;
207
+ }
208
+ /**
209
+ * Jumps to `pos` ticks without sounding notes: replays program, controller and pitch
210
+ * events so the (possibly new) output is in the right state. Used when switching devices.
211
+ */
212
+ seek(pos, now) {
213
+ const events = this.song.events;
214
+ this.index = 0;
215
+ while (this.index < events.length && events[this.index].tick <= pos) {
216
+ const e = events[this.index];
217
+ const type = e.bytes[0] & 0xf0;
218
+ if (type !== 0x90 && type !== 0x80)
219
+ this.dispatch(e, this.index);
220
+ this.index++;
221
+ }
222
+ this.signal = 0;
223
+ this.startTick = now - pos;
224
+ }
225
+ pause(now) {
226
+ if (this.paused !== undefined)
227
+ return;
228
+ this.paused = now;
229
+ this.silence();
230
+ }
231
+ resume(now) {
232
+ if (this.paused === undefined)
233
+ return;
234
+ this.startTick += now - this.paused;
235
+ this.paused = undefined;
236
+ }
237
+ setVolume(volume) {
238
+ this.volume = volume;
239
+ for (const [channel, v] of this.channelVolume)
240
+ this.output?.send([0xb0 | channel, 7, Math.round((v * volume) / 127)]);
241
+ }
242
+ }