@datamoc/mw_games 0.42.0 → 0.44.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 (333) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +84 -1
  3. package/REFERENCE.md +134 -6
  4. package/api-surface.json +1814 -116
  5. package/data/microgm-essential.PROVENANCE.md +13 -0
  6. package/data/microgm-essential.sf2 +0 -0
  7. package/data/soundfont-catalog.json +8 -0
  8. package/dist/actors/Resource.d.ts.map +1 -1
  9. package/dist/actors/Resource.js +2 -3
  10. package/dist/actors/Resource.js.map +1 -1
  11. package/dist/audio/AdaptiveMusic.d.ts +85 -0
  12. package/dist/audio/AdaptiveMusic.d.ts.map +1 -0
  13. package/dist/audio/AdaptiveMusic.js +230 -0
  14. package/dist/audio/AdaptiveMusic.js.map +1 -0
  15. package/dist/audio/AudioLoader.d.ts +33 -0
  16. package/dist/audio/AudioLoader.d.ts.map +1 -0
  17. package/dist/audio/AudioLoader.js +58 -0
  18. package/dist/audio/AudioLoader.js.map +1 -0
  19. package/dist/audio/AudioViews.d.ts +70 -0
  20. package/dist/audio/AudioViews.d.ts.map +1 -0
  21. package/dist/audio/AudioViews.js +119 -0
  22. package/dist/audio/AudioViews.js.map +1 -0
  23. package/dist/audio/BusBudget.d.ts +89 -0
  24. package/dist/audio/BusBudget.d.ts.map +1 -0
  25. package/dist/audio/BusBudget.js +120 -0
  26. package/dist/audio/BusBudget.js.map +1 -0
  27. package/dist/audio/Channels.d.ts +66 -1
  28. package/dist/audio/Channels.d.ts.map +1 -1
  29. package/dist/audio/Channels.js +119 -31
  30. package/dist/audio/Channels.js.map +1 -1
  31. package/dist/audio/CoreHost.d.ts +114 -0
  32. package/dist/audio/CoreHost.d.ts.map +1 -0
  33. package/dist/audio/CoreHost.js +335 -0
  34. package/dist/audio/CoreHost.js.map +1 -0
  35. package/dist/audio/DecoderGuard.d.ts +139 -0
  36. package/dist/audio/DecoderGuard.d.ts.map +1 -0
  37. package/dist/audio/DecoderGuard.js +208 -0
  38. package/dist/audio/DecoderGuard.js.map +1 -0
  39. package/dist/audio/Fft.d.ts +3 -0
  40. package/dist/audio/Fft.d.ts.map +1 -0
  41. package/dist/audio/Fft.js +45 -0
  42. package/dist/audio/Fft.js.map +1 -0
  43. package/dist/audio/LoopTags.d.ts +18 -0
  44. package/dist/audio/LoopTags.d.ts.map +1 -1
  45. package/dist/audio/LoopTags.js +20 -0
  46. package/dist/audio/LoopTags.js.map +1 -1
  47. package/dist/audio/Midi.d.ts +35 -0
  48. package/dist/audio/Midi.d.ts.map +1 -1
  49. package/dist/audio/Midi.js +71 -1
  50. package/dist/audio/Midi.js.map +1 -1
  51. package/dist/audio/MidiRender.d.ts +47 -0
  52. package/dist/audio/MidiRender.d.ts.map +1 -1
  53. package/dist/audio/MidiRender.js +100 -2
  54. package/dist/audio/MidiRender.js.map +1 -1
  55. package/dist/audio/MidiSynth.d.ts +147 -0
  56. package/dist/audio/MidiSynth.d.ts.map +1 -0
  57. package/dist/audio/MidiSynth.js +205 -0
  58. package/dist/audio/MidiSynth.js.map +1 -0
  59. package/dist/audio/MusicPlayer.d.ts +80 -0
  60. package/dist/audio/MusicPlayer.d.ts.map +1 -0
  61. package/dist/audio/MusicPlayer.js +147 -0
  62. package/dist/audio/MusicPlayer.js.map +1 -0
  63. package/dist/audio/Playable.d.ts +7 -0
  64. package/dist/audio/Playable.d.ts.map +1 -1
  65. package/dist/audio/Playable.js.map +1 -1
  66. package/dist/audio/Positional.d.ts +12 -4
  67. package/dist/audio/Positional.d.ts.map +1 -1
  68. package/dist/audio/Positional.js +5 -2
  69. package/dist/audio/Positional.js.map +1 -1
  70. package/dist/audio/Resample.d.ts +61 -0
  71. package/dist/audio/Resample.d.ts.map +1 -0
  72. package/dist/audio/Resample.js +97 -0
  73. package/dist/audio/Resample.js.map +1 -0
  74. package/dist/audio/SampleDecoder.d.ts +16 -2
  75. package/dist/audio/SampleDecoder.d.ts.map +1 -1
  76. package/dist/audio/SampleDecoder.js +10 -6
  77. package/dist/audio/SampleDecoder.js.map +1 -1
  78. package/dist/audio/Settings.d.ts +99 -0
  79. package/dist/audio/Settings.d.ts.map +1 -0
  80. package/dist/audio/Settings.js +134 -0
  81. package/dist/audio/Settings.js.map +1 -0
  82. package/dist/audio/Sfx.d.ts +148 -0
  83. package/dist/audio/Sfx.d.ts.map +1 -0
  84. package/dist/audio/Sfx.js +213 -0
  85. package/dist/audio/Sfx.js.map +1 -0
  86. package/dist/audio/Sound.d.ts +2 -0
  87. package/dist/audio/Sound.d.ts.map +1 -1
  88. package/dist/audio/Sound.js +3 -0
  89. package/dist/audio/Sound.js.map +1 -1
  90. package/dist/audio/SoundFont.d.ts +5 -3
  91. package/dist/audio/SoundFont.d.ts.map +1 -1
  92. package/dist/audio/SoundFont.js +10 -8
  93. package/dist/audio/SoundFont.js.map +1 -1
  94. package/dist/audio/SpectralDistance.d.ts +34 -0
  95. package/dist/audio/SpectralDistance.d.ts.map +1 -0
  96. package/dist/audio/SpectralDistance.js +64 -0
  97. package/dist/audio/SpectralDistance.js.map +1 -0
  98. package/dist/audio/StreamChannel.d.ts +150 -0
  99. package/dist/audio/StreamChannel.d.ts.map +1 -0
  100. package/dist/audio/StreamChannel.js +390 -0
  101. package/dist/audio/StreamChannel.js.map +1 -0
  102. package/dist/audio/StreamPlayable.d.ts +73 -0
  103. package/dist/audio/StreamPlayable.d.ts.map +1 -0
  104. package/dist/audio/StreamPlayable.js +130 -0
  105. package/dist/audio/StreamPlayable.js.map +1 -0
  106. package/dist/audio/Synth.d.ts +1 -1
  107. package/dist/audio/Synth.d.ts.map +1 -1
  108. package/dist/audio/Synth.js +4 -0
  109. package/dist/audio/Synth.js.map +1 -1
  110. package/dist/audio/SyxBank.d.ts +73 -0
  111. package/dist/audio/SyxBank.d.ts.map +1 -0
  112. package/dist/audio/SyxBank.js +130 -0
  113. package/dist/audio/SyxBank.js.map +1 -0
  114. package/dist/audio/Underrun.d.ts +25 -0
  115. package/dist/audio/Underrun.d.ts.map +1 -0
  116. package/dist/audio/Underrun.js +18 -0
  117. package/dist/audio/Underrun.js.map +1 -0
  118. package/dist/audio/Wav.d.ts +56 -0
  119. package/dist/audio/Wav.d.ts.map +1 -0
  120. package/dist/audio/Wav.js +164 -0
  121. package/dist/audio/Wav.js.map +1 -0
  122. package/dist/audio/WebMidi.d.ts +58 -0
  123. package/dist/audio/WebMidi.d.ts.map +1 -0
  124. package/dist/audio/WebMidi.js +79 -0
  125. package/dist/audio/WebMidi.js.map +1 -0
  126. package/dist/audio/humanize/Onsets.d.ts.map +1 -1
  127. package/dist/audio/humanize/Onsets.js +1 -44
  128. package/dist/audio/humanize/Onsets.js.map +1 -1
  129. package/dist/audio/index.d.ts +43 -7
  130. package/dist/audio/index.d.ts.map +1 -1
  131. package/dist/audio/index.js +21 -3
  132. package/dist/audio/index.js.map +1 -1
  133. package/dist/battle/AttackPreview.d.ts.map +1 -1
  134. package/dist/battle/AttackPreview.js +8 -8
  135. package/dist/battle/AttackPreview.js.map +1 -1
  136. package/dist/battle/StatStages.d.ts.map +1 -1
  137. package/dist/battle/StatStages.js +2 -1
  138. package/dist/battle/StatStages.js.map +1 -1
  139. package/dist/board/Engine.d.ts +2 -0
  140. package/dist/board/Engine.d.ts.map +1 -1
  141. package/dist/board/Engine.js +1 -1
  142. package/dist/board/Engine.js.map +1 -1
  143. package/dist/board/HexSkirmish.d.ts.map +1 -1
  144. package/dist/board/HexSkirmish.js +11 -72
  145. package/dist/board/HexSkirmish.js.map +1 -1
  146. package/dist/board/Tactics.d.ts.map +1 -1
  147. package/dist/board/Tactics.js +3 -5
  148. package/dist/board/Tactics.js.map +1 -1
  149. package/dist/board/tourney.d.ts.map +1 -1
  150. package/dist/board/tourney.js +2 -19
  151. package/dist/board/tourney.js.map +1 -1
  152. package/dist/core/Handles.js +7 -7
  153. package/dist/core/Handles.js.map +1 -1
  154. package/dist/mw_games.global.js +25 -22
  155. package/dist/mwl/conditions.d.ts +12 -0
  156. package/dist/mwl/conditions.d.ts.map +1 -1
  157. package/dist/mwl/conditions.js +49 -8
  158. package/dist/mwl/conditions.js.map +1 -1
  159. package/dist/mwl/index.d.ts +13 -0
  160. package/dist/mwl/index.d.ts.map +1 -1
  161. package/dist/mwl/index.js +7 -0
  162. package/dist/mwl/index.js.map +1 -1
  163. package/dist/mwl/objects.d.ts +31 -0
  164. package/dist/mwl/objects.d.ts.map +1 -0
  165. package/dist/mwl/objects.js +83 -0
  166. package/dist/mwl/objects.js.map +1 -0
  167. package/dist/mwl/package-container.d.ts +185 -0
  168. package/dist/mwl/package-container.d.ts.map +1 -0
  169. package/dist/mwl/package-container.js +446 -0
  170. package/dist/mwl/package-container.js.map +1 -0
  171. package/dist/mwl/package-document.d.ts +217 -0
  172. package/dist/mwl/package-document.d.ts.map +1 -0
  173. package/dist/mwl/package-document.js +639 -0
  174. package/dist/mwl/package-document.js.map +1 -0
  175. package/dist/mwl/package-rom.d.ts +37 -0
  176. package/dist/mwl/package-rom.d.ts.map +1 -0
  177. package/dist/mwl/package-rom.js +96 -0
  178. package/dist/mwl/package-rom.js.map +1 -0
  179. package/dist/mwl/package-verify.d.ts +34 -0
  180. package/dist/mwl/package-verify.d.ts.map +1 -0
  181. package/dist/mwl/package-verify.js +111 -0
  182. package/dist/mwl/package-verify.js.map +1 -0
  183. package/dist/mwl/package.d.ts +184 -0
  184. package/dist/mwl/package.d.ts.map +1 -0
  185. package/dist/mwl/package.js +382 -0
  186. package/dist/mwl/package.js.map +1 -0
  187. package/dist/mwl/schema.d.ts +1 -1
  188. package/dist/mwl/schema.d.ts.map +1 -1
  189. package/dist/mwl/tables.d.ts +2 -0
  190. package/dist/mwl/tables.d.ts.map +1 -1
  191. package/dist/mwl/tables.js +13 -0
  192. package/dist/mwl/tables.js.map +1 -1
  193. package/dist/physics/PhysicsWorld.d.ts.map +1 -1
  194. package/dist/physics/PhysicsWorld.js +4 -3
  195. package/dist/physics/PhysicsWorld.js.map +1 -1
  196. package/dist/physics/XpbdWorld.d.ts +23 -5
  197. package/dist/physics/XpbdWorld.d.ts.map +1 -1
  198. package/dist/physics/XpbdWorld.js +91 -6
  199. package/dist/physics/XpbdWorld.js.map +1 -1
  200. package/dist/testing/FakeAudio.d.ts +149 -0
  201. package/dist/testing/FakeAudio.d.ts.map +1 -0
  202. package/dist/testing/FakeAudio.js +219 -0
  203. package/dist/testing/FakeAudio.js.map +1 -0
  204. package/dist/testing/index.d.ts +18 -0
  205. package/dist/testing/index.d.ts.map +1 -1
  206. package/dist/testing/index.js +22 -0
  207. package/dist/testing/index.js.map +1 -1
  208. package/dist/threads/index.d.ts +2 -2
  209. package/dist/threads/index.d.ts.map +1 -1
  210. package/dist/threads/index.js +1 -1
  211. package/dist/threads/index.js.map +1 -1
  212. package/dist/threads/spawn.d.ts +32 -0
  213. package/dist/threads/spawn.d.ts.map +1 -1
  214. package/dist/threads/spawn.js +35 -11
  215. package/dist/threads/spawn.js.map +1 -1
  216. package/dist/three-d/Collision3D.d.ts +46 -2
  217. package/dist/three-d/Collision3D.d.ts.map +1 -1
  218. package/dist/three-d/Collision3D.js +46 -2
  219. package/dist/three-d/Collision3D.js.map +1 -1
  220. package/dist/three-d/Grid.d.ts +12 -1
  221. package/dist/three-d/Grid.d.ts.map +1 -1
  222. package/dist/three-d/Grid.js +12 -1
  223. package/dist/three-d/Grid.js.map +1 -1
  224. package/dist/three-d/Vox.d.ts +13 -1
  225. package/dist/three-d/Vox.d.ts.map +1 -1
  226. package/dist/three-d/Vox.js +13 -1
  227. package/dist/three-d/Vox.js.map +1 -1
  228. package/dist/two-d/Game.d.ts +32 -1
  229. package/dist/two-d/Game.d.ts.map +1 -1
  230. package/dist/two-d/Game.js +105 -14
  231. package/dist/two-d/Game.js.map +1 -1
  232. package/dist/two-d/Scene2D.d.ts +24 -0
  233. package/dist/two-d/Scene2D.d.ts.map +1 -1
  234. package/dist/two-d/Scene2D.js +30 -0
  235. package/dist/two-d/Scene2D.js.map +1 -1
  236. package/dist/two-d/render/HazeLayer.d.ts.map +1 -1
  237. package/dist/two-d/render/HazeLayer.js +2 -1
  238. package/dist/two-d/render/HazeLayer.js.map +1 -1
  239. package/dist/two-d/render/ImageModifiers.d.ts +4 -2
  240. package/dist/two-d/render/ImageModifiers.d.ts.map +1 -1
  241. package/dist/two-d/render/ImageModifiers.js +13 -8
  242. package/dist/two-d/render/ImageModifiers.js.map +1 -1
  243. package/dist/two-d/render/NeonGlow.d.ts.map +1 -1
  244. package/dist/two-d/render/NeonGlow.js +2 -1
  245. package/dist/two-d/render/NeonGlow.js.map +1 -1
  246. package/dist/two-d/render/Particles.d.ts.map +1 -1
  247. package/dist/two-d/render/Particles.js +2 -1
  248. package/dist/two-d/render/Particles.js.map +1 -1
  249. package/dist/two-d/render/SceneTransition.d.ts +301 -0
  250. package/dist/two-d/render/SceneTransition.d.ts.map +1 -0
  251. package/dist/two-d/render/SceneTransition.js +458 -0
  252. package/dist/two-d/render/SceneTransition.js.map +1 -0
  253. package/dist/two-d/render/ScreenEffects.d.ts.map +1 -1
  254. package/dist/two-d/render/ScreenEffects.js +2 -1
  255. package/dist/two-d/render/ScreenEffects.js.map +1 -1
  256. package/dist/two-d/render/SkeletalSprite.d.ts.map +1 -1
  257. package/dist/two-d/render/SkeletalSprite.js +2 -1
  258. package/dist/two-d/render/SkeletalSprite.js.map +1 -1
  259. package/dist/two-d/render/Types2D.d.ts +2 -2
  260. package/dist/two-d/render/Types2D.d.ts.map +1 -1
  261. package/dist/two-d/render/Types2D.js +2 -2
  262. package/dist/two-d/render/Types2D.js.map +1 -1
  263. package/dist/two-d/render/index.d.ts +2 -0
  264. package/dist/two-d/render/index.d.ts.map +1 -1
  265. package/dist/two-d/render/index.js +1 -0
  266. package/dist/two-d/render/index.js.map +1 -1
  267. package/dist/two-d/ui/Bar.d.ts.map +1 -1
  268. package/dist/two-d/ui/Bar.js +3 -5
  269. package/dist/two-d/ui/Bar.js.map +1 -1
  270. package/dist/two-d/ui/Button.d.ts +23 -0
  271. package/dist/two-d/ui/Button.d.ts.map +1 -1
  272. package/dist/two-d/ui/Button.js +37 -0
  273. package/dist/two-d/ui/Button.js.map +1 -1
  274. package/dist/two-d/ui/Dropdown.d.ts +5 -1
  275. package/dist/two-d/ui/Dropdown.d.ts.map +1 -1
  276. package/dist/two-d/ui/Dropdown.js +8 -1
  277. package/dist/two-d/ui/Dropdown.js.map +1 -1
  278. package/dist/two-d/ui/DropdownView.d.ts +63 -3
  279. package/dist/two-d/ui/DropdownView.d.ts.map +1 -1
  280. package/dist/two-d/ui/DropdownView.js +168 -9
  281. package/dist/two-d/ui/DropdownView.js.map +1 -1
  282. package/dist/two-d/ui/FloatingText.d.ts.map +1 -1
  283. package/dist/two-d/ui/FloatingText.js +2 -1
  284. package/dist/two-d/ui/FloatingText.js.map +1 -1
  285. package/dist/two-d/ui/IconGrid.d.ts.map +1 -1
  286. package/dist/two-d/ui/IconGrid.js +2 -1
  287. package/dist/two-d/ui/IconGrid.js.map +1 -1
  288. package/dist/two-d/ui/ListView.d.ts.map +1 -1
  289. package/dist/two-d/ui/ListView.js +2 -1
  290. package/dist/two-d/ui/ListView.js.map +1 -1
  291. package/dist/two-d/ui/ScrollBox.d.ts +96 -0
  292. package/dist/two-d/ui/ScrollBox.d.ts.map +1 -1
  293. package/dist/two-d/ui/ScrollBox.js +218 -5
  294. package/dist/two-d/ui/ScrollBox.js.map +1 -1
  295. package/dist/two-d/ui/ScrollText.d.ts +131 -0
  296. package/dist/two-d/ui/ScrollText.d.ts.map +1 -0
  297. package/dist/two-d/ui/ScrollText.js +218 -0
  298. package/dist/two-d/ui/ScrollText.js.map +1 -0
  299. package/dist/two-d/ui/Slider.d.ts.map +1 -1
  300. package/dist/two-d/ui/Slider.js +4 -4
  301. package/dist/two-d/ui/Slider.js.map +1 -1
  302. package/dist/two-d/ui/Tooltip.d.ts +70 -1
  303. package/dist/two-d/ui/Tooltip.d.ts.map +1 -1
  304. package/dist/two-d/ui/Tooltip.js +115 -3
  305. package/dist/two-d/ui/Tooltip.js.map +1 -1
  306. package/dist/two-d/ui/Window.d.ts +35 -2
  307. package/dist/two-d/ui/Window.d.ts.map +1 -1
  308. package/dist/two-d/ui/Window.js +58 -3
  309. package/dist/two-d/ui/Window.js.map +1 -1
  310. package/dist/two-d/ui/index.d.ts +7 -5
  311. package/dist/two-d/ui/index.d.ts.map +1 -1
  312. package/dist/two-d/ui/index.js +3 -2
  313. package/dist/two-d/ui/index.js.map +1 -1
  314. package/dist/version.d.ts +1 -1
  315. package/dist/version.js +1 -1
  316. package/package.json +14 -3
  317. package/sbom.cdx.json +19 -260
  318. package/tools/audio.d.mts +75 -0
  319. package/tools/audio.mjs +437 -0
  320. package/tools/browser-driver.mjs +3 -3
  321. package/tools/browser-smoke.mjs +19 -2
  322. package/tools/build-tablebases.mjs +7 -3
  323. package/tools/compile-openings.mjs +7 -3
  324. package/tools/compile-resources.d.mts +2 -0
  325. package/tools/compile-resources.mjs +13 -1
  326. package/tools/emit-page.d.mts +6 -0
  327. package/tools/emit-page.mjs +43 -3
  328. package/tools/find-chrome.mjs +4 -2
  329. package/tools/init.mjs +11 -3
  330. package/tools/mwl.mjs +144 -2
  331. package/tools/play-uci.mjs +9 -5
  332. package/tools/vite.mjs +9 -1
  333. package/tools.md +2 -1
@@ -0,0 +1,99 @@
1
+ import type { AudioBus, BusKind } from './Channels.ts';
2
+ /** the television standard a retro machine ran at: it sets the clock its music was timed by */
3
+ export type AudioRegion = 'pal' | 'ntsc';
4
+ /**
5
+ * How a machine profile's output stage is treated: `faithful` keeps what the hardware did
6
+ * (DC offset, aliasing and all), `clean` removes the DC and softens the top end.
7
+ */
8
+ export type OutputStage = 'faithful' | 'clean';
9
+ /** player-facing audio settings as plain data, the shape a save or a settings file holds */
10
+ export interface AudioSettingsData {
11
+ /** volume per bus kind, 0 to 100 like every other volume in the bus API */
12
+ busVolumes: Record<BusKind, number>;
13
+ /** the MIDI engine name content is voiced by first, or null for the internal path */
14
+ preferredEngine: string | null;
15
+ region: AudioRegion;
16
+ outputStage: OutputStage;
17
+ /** whether caption text is shown for captioned sounds */
18
+ captions: boolean;
19
+ /** dynamics control, 0 (untouched) to 1 (a soft limiter at full), for loudness comfort */
20
+ dynamics: number;
21
+ /** softening of bright or broadband sounds, 0 (untouched) to 1 (most softened) */
22
+ soften: number;
23
+ }
24
+ /**
25
+ * The playback rate a tune authored for one region runs at on the other: a PAL machine's
26
+ * 50 Hz clock against NTSC's 60 Hz makes the same file slower and lower, which is the
27
+ * difference a player who grew up with the other standard hears first.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { regionPlaybackRate } from '@datamoc/mw_games/audio';
32
+ *
33
+ * console.log(regionPlaybackRate('pal', 'ntsc')); // 0.8333..., slower on PAL
34
+ * console.log(regionPlaybackRate('ntsc', 'ntsc')); // 1, its own region
35
+ * ```
36
+ */
37
+ export declare function regionPlaybackRate(region: AudioRegion, authored?: AudioRegion): number;
38
+ /**
39
+ * The low-pass cutoff, in hertz, that a `soften` amount asks an output stage for: nothing
40
+ * at 0, down to a muffled 3.5 kHz at 1, never above what the sample rate can represent.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { softenCutoff } from '@datamoc/mw_games/audio';
45
+ *
46
+ * console.log(softenCutoff(0, 44100)); // 18000, effectively open
47
+ * console.log(softenCutoff(1, 44100) < softenCutoff(0.5, 44100)); // true, more soften, lower cutoff
48
+ * ```
49
+ */
50
+ export declare function softenCutoff(soften: number, sampleRate: number): number;
51
+ /**
52
+ * One sample through the dynamics control: at 0 the sample is untouched, and as the amount
53
+ * rises a soft knee pulls peaks toward full scale without ever passing it, so a loud game
54
+ * gets quieter peaks rather than clipped ones.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { dynamicsLimit } from '@datamoc/mw_games/audio';
59
+ *
60
+ * console.log(dynamicsLimit(0.5, 0)); // 0.5, untouched
61
+ * console.log(dynamicsLimit(4, 1) < 1); // true, a hot sample is pulled under full scale
62
+ * ```
63
+ */
64
+ export declare function dynamicsLimit(sample: number, dynamics: number): number;
65
+ /**
66
+ * The player's audio settings in one place, over the pieces that already exist: bus
67
+ * volumes apply to an `AudioBus` (`applyToBus`), the preferred engine names what
68
+ * `resolveMidiSynth` is asked for first, the region and output stage describe the machine
69
+ * profile a retro package renders for, and captions, dynamics and soften are the
70
+ * accessibility dials. Everything clamps on the way in, and `snapshot`/`restore` round-trip
71
+ * the plain-data shape a settings screen persists.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { AudioSettings } from '@datamoc/mw_games/audio';
76
+ *
77
+ * const settings = new AudioSettings({ preferredEngine: 'retro:mt32', soften: 0.4 });
78
+ * settings.setBusVolume('se', 130);
79
+ * console.log(settings.snapshot().busVolumes.se); // 100, clamped
80
+ * ```
81
+ */
82
+ export declare class AudioSettings {
83
+ private data;
84
+ constructor(initial?: Partial<AudioSettingsData>);
85
+ /** a copy of the current settings, safe to store */
86
+ snapshot(): AudioSettingsData;
87
+ /** takes settings in, clamping every dial; a partial update keeps the rest */
88
+ restore(data: Partial<AudioSettingsData>): void;
89
+ setBusVolume(kind: BusKind, volume: number): void;
90
+ /** pushes the per-bus volumes onto a bus's mixer, the settings' one live connection */
91
+ applyToBus(bus: AudioBus): void;
92
+ /** the rate content authored for `authored` should play at under this region */
93
+ playbackRate(authored?: AudioRegion): number;
94
+ /** the cutoff the current soften amount (and a clean stage's extra softening) asks for */
95
+ cutoff(sampleRate: number): number;
96
+ /** one sample through the current dynamics amount */
97
+ limit(sample: number): number;
98
+ }
99
+ //# sourceMappingURL=Settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../src/audio/Settings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAEvD,+FAA+F;AAC/F,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,CAAC;AAEzC;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;AAE/C,4FAA4F;AAC5F,MAAM,WAAW,iBAAiB;IACjC,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACpC,qFAAqF;IACrF,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,WAAW,CAAC;IACzB,yDAAyD;IACzD,QAAQ,EAAE,OAAO,CAAC;IAClB,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAE,WAAoB,GAAG,MAAM,CAG9F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAIvE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKtE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,IAAI,CAAoB;IAEhC,YAAY,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,EAWnD;IAED,oDAAoD;IACpD,QAAQ,IAAI,iBAAiB,CAE5B;IAED,8EAA8E;IAC9E,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAY9C;IAED,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAEhD;IAED,uFAAuF;IACvF,UAAU,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAI9B;IAED,gFAAgF;IAChF,YAAY,CAAC,QAAQ,GAAE,WAAoB,GAAG,MAAM,CAEnD;IAED,0FAA0F;IAC1F,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGjC;IAED,qDAAqD;IACrD,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5B;CACD"}
@@ -0,0 +1,134 @@
1
+ import { clamp } from '../core/Math.js';
2
+ /**
3
+ * The playback rate a tune authored for one region runs at on the other: a PAL machine's
4
+ * 50 Hz clock against NTSC's 60 Hz makes the same file slower and lower, which is the
5
+ * difference a player who grew up with the other standard hears first.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { regionPlaybackRate } from '@datamoc/mw_games/audio';
10
+ *
11
+ * console.log(regionPlaybackRate('pal', 'ntsc')); // 0.8333..., slower on PAL
12
+ * console.log(regionPlaybackRate('ntsc', 'ntsc')); // 1, its own region
13
+ * ```
14
+ */
15
+ export function regionPlaybackRate(region, authored = 'ntsc') {
16
+ if (region === authored)
17
+ return 1;
18
+ return region === 'pal' ? 50 / 60 : 60 / 50;
19
+ }
20
+ /**
21
+ * The low-pass cutoff, in hertz, that a `soften` amount asks an output stage for: nothing
22
+ * at 0, down to a muffled 3.5 kHz at 1, never above what the sample rate can represent.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { softenCutoff } from '@datamoc/mw_games/audio';
27
+ *
28
+ * console.log(softenCutoff(0, 44100)); // 18000, effectively open
29
+ * console.log(softenCutoff(1, 44100) < softenCutoff(0.5, 44100)); // true, more soften, lower cutoff
30
+ * ```
31
+ */
32
+ export function softenCutoff(soften, sampleRate) {
33
+ const amount = clamp(soften, 0, 1);
34
+ const cutoff = 18000 * (3500 / 18000) ** amount;
35
+ return Math.min(cutoff, sampleRate / 2);
36
+ }
37
+ /**
38
+ * One sample through the dynamics control: at 0 the sample is untouched, and as the amount
39
+ * rises a soft knee pulls peaks toward full scale without ever passing it, so a loud game
40
+ * gets quieter peaks rather than clipped ones.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { dynamicsLimit } from '@datamoc/mw_games/audio';
45
+ *
46
+ * console.log(dynamicsLimit(0.5, 0)); // 0.5, untouched
47
+ * console.log(dynamicsLimit(4, 1) < 1); // true, a hot sample is pulled under full scale
48
+ * ```
49
+ */
50
+ export function dynamicsLimit(sample, dynamics) {
51
+ const amount = clamp(dynamics, 0, 1);
52
+ if (amount === 0)
53
+ return sample;
54
+ const limited = Math.tanh(sample);
55
+ return sample * (1 - amount) + limited * amount;
56
+ }
57
+ /**
58
+ * The player's audio settings in one place, over the pieces that already exist: bus
59
+ * volumes apply to an `AudioBus` (`applyToBus`), the preferred engine names what
60
+ * `resolveMidiSynth` is asked for first, the region and output stage describe the machine
61
+ * profile a retro package renders for, and captions, dynamics and soften are the
62
+ * accessibility dials. Everything clamps on the way in, and `snapshot`/`restore` round-trip
63
+ * the plain-data shape a settings screen persists.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * import { AudioSettings } from '@datamoc/mw_games/audio';
68
+ *
69
+ * const settings = new AudioSettings({ preferredEngine: 'retro:mt32', soften: 0.4 });
70
+ * settings.setBusVolume('se', 130);
71
+ * console.log(settings.snapshot().busVolumes.se); // 100, clamped
72
+ * ```
73
+ */
74
+ export class AudioSettings {
75
+ constructor(initial = {}) {
76
+ this.data = {
77
+ busVolumes: { bgm: 90, bgs: 90, me: 90, se: 90 },
78
+ preferredEngine: null,
79
+ region: 'ntsc',
80
+ outputStage: 'faithful',
81
+ captions: false,
82
+ dynamics: 0,
83
+ soften: 0,
84
+ };
85
+ this.restore(initial);
86
+ }
87
+ /** a copy of the current settings, safe to store */
88
+ snapshot() {
89
+ return { ...this.data, busVolumes: { ...this.data.busVolumes } };
90
+ }
91
+ /** takes settings in, clamping every dial; a partial update keeps the rest */
92
+ restore(data) {
93
+ if (data.busVolumes) {
94
+ for (const kind of Object.keys(data.busVolumes)) {
95
+ this.data.busVolumes[kind] = clamp(data.busVolumes[kind], 0, 100);
96
+ }
97
+ }
98
+ if (data.preferredEngine !== undefined)
99
+ this.data.preferredEngine = data.preferredEngine;
100
+ if (data.region !== undefined)
101
+ this.data.region = data.region;
102
+ if (data.outputStage !== undefined)
103
+ this.data.outputStage = data.outputStage;
104
+ if (data.captions !== undefined)
105
+ this.data.captions = data.captions;
106
+ if (data.dynamics !== undefined)
107
+ this.data.dynamics = clamp(data.dynamics, 0, 1);
108
+ if (data.soften !== undefined)
109
+ this.data.soften = clamp(data.soften, 0, 1);
110
+ }
111
+ setBusVolume(kind, volume) {
112
+ this.data.busVolumes[kind] = clamp(volume, 0, 100);
113
+ }
114
+ /** pushes the per-bus volumes onto a bus's mixer, the settings' one live connection */
115
+ applyToBus(bus) {
116
+ for (const kind of Object.keys(this.data.busVolumes)) {
117
+ bus.setMixer(kind, { volume: this.data.busVolumes[kind] / 100 });
118
+ }
119
+ }
120
+ /** the rate content authored for `authored` should play at under this region */
121
+ playbackRate(authored = 'ntsc') {
122
+ return regionPlaybackRate(this.data.region, authored);
123
+ }
124
+ /** the cutoff the current soften amount (and a clean stage's extra softening) asks for */
125
+ cutoff(sampleRate) {
126
+ const extra = this.data.outputStage === 'clean' ? 0.25 : 0;
127
+ return softenCutoff(clamp(this.data.soften + extra, 0, 1), sampleRate);
128
+ }
129
+ /** one sample through the current dynamics amount */
130
+ limit(sample) {
131
+ return dynamicsLimit(sample, this.data.dynamics);
132
+ }
133
+ }
134
+ //# sourceMappingURL=Settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Settings.js","sourceRoot":"","sources":["../../src/audio/Settings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * A sound effect described as data and voiced by a synthesis backend instead of read from a
3
+ * file. mwg owns the shape the game and the mixer need (an id, a priority, what a second
4
+ * trigger does, a caption); whatever else a backend wants (chip, voices, curves) rides along
5
+ * untouched, so a retro-instrument package can carry its own `SfxDefinition` through unchanged.
6
+ */
7
+ export interface SfxDefinition {
8
+ id: string;
9
+ /** higher cuts lower when the mixer is full; 0 when omitted */
10
+ priority?: number;
11
+ /** what a second trigger does while the first still sounds: `'restart'` (the default), `'ignore'`, or `'layer'` */
12
+ retrigger?: 'restart' | 'ignore' | 'layer';
13
+ /** shown by a captioning overlay each time the effect plays */
14
+ caption?: string;
15
+ [backendData: string]: unknown;
16
+ }
17
+ /** what a play carries into the backend: the mix mwg computed and a seed that makes any randomness reproducible */
18
+ export interface SfxPlayOptions {
19
+ /** 0 to 1, after distance and the sound's own volume */
20
+ gain: number;
21
+ /** -1 (left) to 1 (right) */
22
+ pan: number;
23
+ /** playback ratio, 1 unchanged */
24
+ pitch: number;
25
+ /** the only source of chance an effect may use */
26
+ seed: number;
27
+ }
28
+ /** one effect sounding on a backend */
29
+ export interface SfxVoice {
30
+ stop(): void;
31
+ /** false once it has finished by itself or been stopped */
32
+ readonly active: boolean;
33
+ }
34
+ /** the synthesis side: a chip engine that can voice an effect, wherever it keeps its voices */
35
+ export interface SfxBackend {
36
+ /** returns null when it cannot voice the effect (no free channel, unknown chip), which is not an error */
37
+ play(definition: SfxDefinition, options: SfxPlayOptions): SfxVoice | null;
38
+ }
39
+ /** effects by id, so a sound can be made from an id the content named */
40
+ export declare class SfxBank {
41
+ private readonly definitions;
42
+ constructor(definitions?: Iterable<SfxDefinition>);
43
+ define(definition: SfxDefinition): void;
44
+ has(id: string): boolean;
45
+ get(id: string): SfxDefinition;
46
+ ids(): string[];
47
+ }
48
+ /** a granted slot in the arbiter; `release` frees it */
49
+ export interface SfxGrant {
50
+ release(): void;
51
+ }
52
+ /**
53
+ * The shared voice limit between chip effects and the bus's `'se'` jingles: both ask here before
54
+ * they sound, and when the limit is reached the lowest-priority voice (the oldest among equals)
55
+ * is cut for a new one of equal or higher priority; a new one below everything sounding is
56
+ * refused. One arbiter handed to a bus and to every `SfxSound` makes them a single pool.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { SfxArbiter } from '@datamoc/mw_games/audio';
61
+ *
62
+ * const arbiter = new SfxArbiter({ maxVoices: 2 });
63
+ * const a = arbiter.request(1, () => console.log('a cut'));
64
+ * arbiter.request(1, () => console.log('b cut'));
65
+ * arbiter.request(5, () => console.log('c cut')); // cuts a, the oldest of the lowest priority
66
+ * a?.release();
67
+ * ```
68
+ */
69
+ export declare class SfxArbiter {
70
+ private readonly maxVoices;
71
+ private readonly voices;
72
+ constructor(options?: {
73
+ maxVoices?: number;
74
+ });
75
+ get active(): number;
76
+ /**
77
+ * Asks for a slot. `stop` cuts the voice when a better one needs its place; `active` says
78
+ * whether it still sounds, so a voice that ended by itself frees its slot for any asker, not
79
+ * only for the sound that owned it.
80
+ */
81
+ request(priority: number, stop: () => void, active?: () => boolean): SfxGrant | null;
82
+ }
83
+ /**
84
+ * A reproducible seed for an effect: the same id on the same game frame (and the same ordinal
85
+ * within that frame) always gives the same number, so a replay or a lockstep peer hears exactly
86
+ * what the original run heard, and audio never perturbs the simulation.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * import { sfxSeed } from '@datamoc/mw_games/audio';
91
+ *
92
+ * sfxSeed('arrow', 120) === sfxSeed('arrow', 120); // true
93
+ * ```
94
+ */
95
+ export declare function sfxSeed(id: string, frame: number, ordinal?: number): number;
96
+ export interface SfxSoundOptions {
97
+ /** where an effect given by id is looked up */
98
+ bank?: SfxBank;
99
+ /** the shared voice limit; none means a voice is never cut for another */
100
+ arbiter?: SfxArbiter;
101
+ /** the game's own frame counter, never the wall clock, so seeds repeat across runs; 0 when omitted */
102
+ frame?: () => number;
103
+ /** shown with each play when the definition has no caption of its own */
104
+ caption?: string;
105
+ /** level, 0 to 1; 1 when omitted */
106
+ volume?: number;
107
+ }
108
+ /**
109
+ * The data-driven counterpart of `Sound`: it plays an `SfxDefinition` (or the id of one in a
110
+ * bank) through a backend, with the same `play(gain, pitch)` shape so a `SoundSource` can
111
+ * attenuate it, and a pan on top. It applies the definition's retrigger rule, asks the shared
112
+ * arbiter for a voice, fires the caption, and hands the backend a deterministic seed.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { SfxArbiter, SfxBank, SfxSound, SoundSource, AudioListener, type SfxBackend } from '@datamoc/mw_games/audio';
117
+ *
118
+ * declare const chip: SfxBackend;
119
+ *
120
+ * const bank = new SfxBank([{ id: 'arrow', priority: 5, caption: 'an arrow flies' }]);
121
+ * const arrow = new SfxSound(chip, 'arrow', { bank, arbiter: new SfxArbiter(), frame: () => 0 });
122
+ * new SoundSource(arrow, { x: 3, y: 4 }).playFor(new AudioListener());
123
+ * ```
124
+ */
125
+ export declare class SfxSound {
126
+ volume: number;
127
+ /** how many plays reached the backend, for a test to assert on without a speaker */
128
+ plays: number;
129
+ private readonly backend;
130
+ private readonly definition;
131
+ private readonly options;
132
+ private readonly live;
133
+ private suspended_;
134
+ private lastFrame;
135
+ private ordinal;
136
+ constructor(backend: SfxBackend, effect: SfxDefinition | string, options?: SfxSoundOptions);
137
+ get isSuspended(): boolean;
138
+ /** voices that still sound */
139
+ get activeVoices(): number;
140
+ /** plays the effect; returns false when it was ignored, refused by the arbiter or the backend had no voice */
141
+ play(gain?: number, pitch?: number, pan?: number): boolean;
142
+ stopAll(): void;
143
+ suspend(): void;
144
+ resume(): void;
145
+ /** drops voices that finished by themselves, returning their arbiter slots */
146
+ private prune;
147
+ }
148
+ //# sourceMappingURL=Sfx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sfx.d.ts","sourceRoot":"","sources":["../../src/audio/Sfx.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,213 @@
1
+ import { onCaption } from './Captions.js';
2
+ /** effects by id, so a sound can be made from an id the content named */
3
+ export class SfxBank {
4
+ constructor(definitions = []) {
5
+ this.definitions = new Map();
6
+ for (const definition of definitions)
7
+ this.define(definition);
8
+ }
9
+ define(definition) {
10
+ if (!definition.id)
11
+ throw new Error('a sound effect needs an id');
12
+ this.definitions.set(definition.id, definition);
13
+ }
14
+ has(id) {
15
+ return this.definitions.has(id);
16
+ }
17
+ get(id) {
18
+ const definition = this.definitions.get(id);
19
+ if (!definition)
20
+ throw new Error(`no sound effect "${id}" in the bank`);
21
+ return definition;
22
+ }
23
+ ids() {
24
+ return [...this.definitions.keys()];
25
+ }
26
+ }
27
+ /**
28
+ * The shared voice limit between chip effects and the bus's `'se'` jingles: both ask here before
29
+ * they sound, and when the limit is reached the lowest-priority voice (the oldest among equals)
30
+ * is cut for a new one of equal or higher priority; a new one below everything sounding is
31
+ * refused. One arbiter handed to a bus and to every `SfxSound` makes them a single pool.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { SfxArbiter } from '@datamoc/mw_games/audio';
36
+ *
37
+ * const arbiter = new SfxArbiter({ maxVoices: 2 });
38
+ * const a = arbiter.request(1, () => console.log('a cut'));
39
+ * arbiter.request(1, () => console.log('b cut'));
40
+ * arbiter.request(5, () => console.log('c cut')); // cuts a, the oldest of the lowest priority
41
+ * a?.release();
42
+ * ```
43
+ */
44
+ export class SfxArbiter {
45
+ constructor(options = {}) {
46
+ this.voices = [];
47
+ this.maxVoices = Math.max(1, Math.floor(options.maxVoices ?? 8));
48
+ }
49
+ get active() {
50
+ return this.voices.length;
51
+ }
52
+ /**
53
+ * Asks for a slot. `stop` cuts the voice when a better one needs its place; `active` says
54
+ * whether it still sounds, so a voice that ended by itself frees its slot for any asker, not
55
+ * only for the sound that owned it.
56
+ */
57
+ request(priority, stop, active = () => true) {
58
+ for (let i = this.voices.length - 1; i >= 0; i--)
59
+ if (!this.voices[i].active())
60
+ this.voices.splice(i, 1);
61
+ if (this.voices.length >= this.maxVoices) {
62
+ let victim = 0;
63
+ for (let i = 1; i < this.voices.length; i++) {
64
+ if (this.voices[i].priority < this.voices[victim].priority)
65
+ victim = i;
66
+ }
67
+ if (this.voices[victim].priority > priority)
68
+ return null;
69
+ const [cut] = this.voices.splice(victim, 1);
70
+ cut.stop();
71
+ }
72
+ const entry = { priority, stop, active };
73
+ this.voices.push(entry);
74
+ return {
75
+ release: () => {
76
+ const at = this.voices.indexOf(entry);
77
+ if (at !== -1)
78
+ this.voices.splice(at, 1);
79
+ },
80
+ };
81
+ }
82
+ }
83
+ /**
84
+ * A reproducible seed for an effect: the same id on the same game frame (and the same ordinal
85
+ * within that frame) always gives the same number, so a replay or a lockstep peer hears exactly
86
+ * what the original run heard, and audio never perturbs the simulation.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * import { sfxSeed } from '@datamoc/mw_games/audio';
91
+ *
92
+ * sfxSeed('arrow', 120) === sfxSeed('arrow', 120); // true
93
+ * ```
94
+ */
95
+ export function sfxSeed(id, frame, ordinal = 0) {
96
+ let hash = 0x811c9dc5;
97
+ const mix = (value) => {
98
+ hash = Math.imul(hash ^ (value & 0xff), 0x01000193);
99
+ hash = Math.imul(hash ^ ((value >>> 8) & 0xff), 0x01000193);
100
+ hash = Math.imul(hash ^ ((value >>> 16) & 0xff), 0x01000193);
101
+ hash = Math.imul(hash ^ ((value >>> 24) & 0xff), 0x01000193);
102
+ };
103
+ for (let i = 0; i < id.length; i++)
104
+ mix(id.charCodeAt(i));
105
+ mix(Math.floor(frame));
106
+ mix(ordinal);
107
+ return hash >>> 0;
108
+ }
109
+ /**
110
+ * The data-driven counterpart of `Sound`: it plays an `SfxDefinition` (or the id of one in a
111
+ * bank) through a backend, with the same `play(gain, pitch)` shape so a `SoundSource` can
112
+ * attenuate it, and a pan on top. It applies the definition's retrigger rule, asks the shared
113
+ * arbiter for a voice, fires the caption, and hands the backend a deterministic seed.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * import { SfxArbiter, SfxBank, SfxSound, SoundSource, AudioListener, type SfxBackend } from '@datamoc/mw_games/audio';
118
+ *
119
+ * declare const chip: SfxBackend;
120
+ *
121
+ * const bank = new SfxBank([{ id: 'arrow', priority: 5, caption: 'an arrow flies' }]);
122
+ * const arrow = new SfxSound(chip, 'arrow', { bank, arbiter: new SfxArbiter(), frame: () => 0 });
123
+ * new SoundSource(arrow, { x: 3, y: 4 }).playFor(new AudioListener());
124
+ * ```
125
+ */
126
+ export class SfxSound {
127
+ constructor(backend, effect, options = {}) {
128
+ /** how many plays reached the backend, for a test to assert on without a speaker */
129
+ this.plays = 0;
130
+ this.live = [];
131
+ this.suspended_ = false;
132
+ this.lastFrame = Number.NaN;
133
+ this.ordinal = 0;
134
+ this.backend = backend;
135
+ this.options = options;
136
+ this.volume = options.volume ?? 1;
137
+ if (typeof effect === 'string') {
138
+ if (!options.bank)
139
+ throw new Error(`a sound effect given by id ("${effect}") needs a bank`);
140
+ this.definition = options.bank.get(effect);
141
+ }
142
+ else {
143
+ this.definition = effect;
144
+ }
145
+ }
146
+ get isSuspended() {
147
+ return this.suspended_;
148
+ }
149
+ /** voices that still sound */
150
+ get activeVoices() {
151
+ this.prune();
152
+ return this.live.length;
153
+ }
154
+ /** plays the effect; returns false when it was ignored, refused by the arbiter or the backend had no voice */
155
+ play(gain = 1, pitch = 1, pan = 0) {
156
+ if (this.suspended_)
157
+ return false;
158
+ this.prune();
159
+ const rule = this.definition.retrigger ?? 'restart';
160
+ if (this.live.length > 0 && rule === 'ignore')
161
+ return false;
162
+ if (this.live.length > 0 && rule === 'restart')
163
+ this.stopAll();
164
+ const frame = this.options.frame?.() ?? 0;
165
+ this.ordinal = frame === this.lastFrame ? this.ordinal + 1 : 0;
166
+ this.lastFrame = frame;
167
+ let voice = null;
168
+ const grant = this.options.arbiter?.request(this.definition.priority ?? 0, () => {
169
+ voice?.stop();
170
+ }, () => voice?.active ?? true) ?? null;
171
+ if (this.options.arbiter && !grant)
172
+ return false;
173
+ voice = this.backend.play(this.definition, {
174
+ gain: Math.max(0, Math.min(1, gain * this.volume)),
175
+ pan: Math.max(-1, Math.min(1, pan)),
176
+ pitch,
177
+ seed: sfxSeed(this.definition.id, frame, this.ordinal),
178
+ });
179
+ if (!voice) {
180
+ grant?.release();
181
+ return false;
182
+ }
183
+ this.live.push({ voice, grant });
184
+ this.plays++;
185
+ const caption = this.definition.caption ?? this.options.caption;
186
+ if (caption)
187
+ onCaption.dispatch({ text: caption });
188
+ return true;
189
+ }
190
+ stopAll() {
191
+ for (const { voice, grant } of this.live.splice(0)) {
192
+ voice.stop();
193
+ grant?.release();
194
+ }
195
+ }
196
+ suspend() {
197
+ this.suspended_ = true;
198
+ this.stopAll();
199
+ }
200
+ resume() {
201
+ this.suspended_ = false;
202
+ }
203
+ /** drops voices that finished by themselves, returning their arbiter slots */
204
+ prune() {
205
+ for (let i = this.live.length - 1; i >= 0; i--) {
206
+ if (!this.live[i].voice.active) {
207
+ this.live[i].grant?.release();
208
+ this.live.splice(i, 1);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ //# sourceMappingURL=Sfx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sfx.js","sourceRoot":"","sources":["../../src/audio/Sfx.ts"],"names":[],"mappings":"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"}
@@ -28,6 +28,8 @@ export declare class Sound {
28
28
  private caption?;
29
29
  private suspended_;
30
30
  volume: number;
31
+ /** how many times `play` actually started a clip (not counting calls made while suspended), for a test to assert on without a microphone */
32
+ plays: number;
31
33
  constructor(path: string, options?: SoundOptions);
32
34
  /**
33
35
  * Plays one instance from the pool.
@@ -1 +1 @@
1
- {"version":3,"file":"Sound.d.ts","sourceRoot":"","sources":["../../src/audio/Sound.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG3D,MAAM,WAAW,YAAY;IAC5B,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,QAAQ,CAAC;IAEpC,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,KAAK;IACjB,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,MAAM,EAAE,MAAM,CAAC;IAEf,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,EAMnD;IAED;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,IAAI,SAAI,EAAE,KAAK,SAAI,GAAG,IAAI,CAc9B;IAED,OAAO,IAAI,IAAI,CAKd;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAGd;IAED,wCAAwC;IACxC,MAAM,IAAI,IAAI,CAEb;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;CACD"}
1
+ {"version":3,"file":"Sound.d.ts","sourceRoot":"","sources":["../../src/audio/Sound.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG3D,MAAM,WAAW,YAAY;IAC5B,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,QAAQ,CAAC;IAEpC,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,KAAK;IACjB,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,4IAA4I;IAC5I,KAAK,SAAK;IAEV,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,EAMnD;IAED;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,IAAI,SAAI,EAAE,KAAK,SAAI,GAAG,IAAI,CAe9B;IAED,OAAO,IAAI,IAAI,CAKd;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAGd;IAED,wCAAwC;IACxC,MAAM,IAAI,IAAI,CAEb;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;CACD"}
@@ -18,6 +18,8 @@ export class Sound {
18
18
  constructor(path, options = {}) {
19
19
  this.next = 0;
20
20
  this.suspended_ = false;
21
+ /** how many times `play` actually started a clip (not counting calls made while suspended), for a test to assert on without a microphone */
22
+ this.plays = 0;
21
23
  const size = Math.max(1, options.poolSize ?? 4);
22
24
  const create = options.create ?? createAudio;
23
25
  this.volume = options.volume ?? 1;
@@ -39,6 +41,7 @@ export class Sound {
39
41
  play(gain = 1, pitch = 1) {
40
42
  if (this.suspended_)
41
43
  return;
44
+ this.plays++;
42
45
  const audio = this.pool[this.next];
43
46
  this.next = (this.next + 1) % this.pool.length;
44
47
  audio.volume = this.volume * gain;