@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.
- package/CHANGELOG.md +126 -0
- package/README.md +84 -1
- package/REFERENCE.md +134 -6
- package/api-surface.json +1814 -116
- package/data/microgm-essential.PROVENANCE.md +13 -0
- package/data/microgm-essential.sf2 +0 -0
- package/data/soundfont-catalog.json +8 -0
- package/dist/actors/Resource.d.ts.map +1 -1
- package/dist/actors/Resource.js +2 -3
- package/dist/actors/Resource.js.map +1 -1
- package/dist/audio/AdaptiveMusic.d.ts +85 -0
- package/dist/audio/AdaptiveMusic.d.ts.map +1 -0
- package/dist/audio/AdaptiveMusic.js +230 -0
- package/dist/audio/AdaptiveMusic.js.map +1 -0
- package/dist/audio/AudioLoader.d.ts +33 -0
- package/dist/audio/AudioLoader.d.ts.map +1 -0
- package/dist/audio/AudioLoader.js +58 -0
- package/dist/audio/AudioLoader.js.map +1 -0
- package/dist/audio/AudioViews.d.ts +70 -0
- package/dist/audio/AudioViews.d.ts.map +1 -0
- package/dist/audio/AudioViews.js +119 -0
- package/dist/audio/AudioViews.js.map +1 -0
- package/dist/audio/BusBudget.d.ts +89 -0
- package/dist/audio/BusBudget.d.ts.map +1 -0
- package/dist/audio/BusBudget.js +120 -0
- package/dist/audio/BusBudget.js.map +1 -0
- package/dist/audio/Channels.d.ts +66 -1
- package/dist/audio/Channels.d.ts.map +1 -1
- package/dist/audio/Channels.js +119 -31
- package/dist/audio/Channels.js.map +1 -1
- package/dist/audio/CoreHost.d.ts +114 -0
- package/dist/audio/CoreHost.d.ts.map +1 -0
- package/dist/audio/CoreHost.js +335 -0
- package/dist/audio/CoreHost.js.map +1 -0
- package/dist/audio/DecoderGuard.d.ts +139 -0
- package/dist/audio/DecoderGuard.d.ts.map +1 -0
- package/dist/audio/DecoderGuard.js +208 -0
- package/dist/audio/DecoderGuard.js.map +1 -0
- package/dist/audio/Fft.d.ts +3 -0
- package/dist/audio/Fft.d.ts.map +1 -0
- package/dist/audio/Fft.js +45 -0
- package/dist/audio/Fft.js.map +1 -0
- package/dist/audio/LoopTags.d.ts +18 -0
- package/dist/audio/LoopTags.d.ts.map +1 -1
- package/dist/audio/LoopTags.js +20 -0
- package/dist/audio/LoopTags.js.map +1 -1
- package/dist/audio/Midi.d.ts +35 -0
- package/dist/audio/Midi.d.ts.map +1 -1
- package/dist/audio/Midi.js +71 -1
- package/dist/audio/Midi.js.map +1 -1
- package/dist/audio/MidiRender.d.ts +47 -0
- package/dist/audio/MidiRender.d.ts.map +1 -1
- package/dist/audio/MidiRender.js +100 -2
- package/dist/audio/MidiRender.js.map +1 -1
- package/dist/audio/MidiSynth.d.ts +147 -0
- package/dist/audio/MidiSynth.d.ts.map +1 -0
- package/dist/audio/MidiSynth.js +205 -0
- package/dist/audio/MidiSynth.js.map +1 -0
- package/dist/audio/MusicPlayer.d.ts +80 -0
- package/dist/audio/MusicPlayer.d.ts.map +1 -0
- package/dist/audio/MusicPlayer.js +147 -0
- package/dist/audio/MusicPlayer.js.map +1 -0
- package/dist/audio/Playable.d.ts +7 -0
- package/dist/audio/Playable.d.ts.map +1 -1
- package/dist/audio/Playable.js.map +1 -1
- package/dist/audio/Positional.d.ts +12 -4
- package/dist/audio/Positional.d.ts.map +1 -1
- package/dist/audio/Positional.js +5 -2
- package/dist/audio/Positional.js.map +1 -1
- package/dist/audio/Resample.d.ts +61 -0
- package/dist/audio/Resample.d.ts.map +1 -0
- package/dist/audio/Resample.js +97 -0
- package/dist/audio/Resample.js.map +1 -0
- package/dist/audio/SampleDecoder.d.ts +16 -2
- package/dist/audio/SampleDecoder.d.ts.map +1 -1
- package/dist/audio/SampleDecoder.js +10 -6
- package/dist/audio/SampleDecoder.js.map +1 -1
- package/dist/audio/Settings.d.ts +99 -0
- package/dist/audio/Settings.d.ts.map +1 -0
- package/dist/audio/Settings.js +134 -0
- package/dist/audio/Settings.js.map +1 -0
- package/dist/audio/Sfx.d.ts +148 -0
- package/dist/audio/Sfx.d.ts.map +1 -0
- package/dist/audio/Sfx.js +213 -0
- package/dist/audio/Sfx.js.map +1 -0
- package/dist/audio/Sound.d.ts +2 -0
- package/dist/audio/Sound.d.ts.map +1 -1
- package/dist/audio/Sound.js +3 -0
- package/dist/audio/Sound.js.map +1 -1
- package/dist/audio/SoundFont.d.ts +5 -3
- package/dist/audio/SoundFont.d.ts.map +1 -1
- package/dist/audio/SoundFont.js +10 -8
- package/dist/audio/SoundFont.js.map +1 -1
- package/dist/audio/SpectralDistance.d.ts +34 -0
- package/dist/audio/SpectralDistance.d.ts.map +1 -0
- package/dist/audio/SpectralDistance.js +64 -0
- package/dist/audio/SpectralDistance.js.map +1 -0
- package/dist/audio/StreamChannel.d.ts +150 -0
- package/dist/audio/StreamChannel.d.ts.map +1 -0
- package/dist/audio/StreamChannel.js +390 -0
- package/dist/audio/StreamChannel.js.map +1 -0
- package/dist/audio/StreamPlayable.d.ts +73 -0
- package/dist/audio/StreamPlayable.d.ts.map +1 -0
- package/dist/audio/StreamPlayable.js +130 -0
- package/dist/audio/StreamPlayable.js.map +1 -0
- package/dist/audio/Synth.d.ts +1 -1
- package/dist/audio/Synth.d.ts.map +1 -1
- package/dist/audio/Synth.js +4 -0
- package/dist/audio/Synth.js.map +1 -1
- package/dist/audio/SyxBank.d.ts +73 -0
- package/dist/audio/SyxBank.d.ts.map +1 -0
- package/dist/audio/SyxBank.js +130 -0
- package/dist/audio/SyxBank.js.map +1 -0
- package/dist/audio/Underrun.d.ts +25 -0
- package/dist/audio/Underrun.d.ts.map +1 -0
- package/dist/audio/Underrun.js +18 -0
- package/dist/audio/Underrun.js.map +1 -0
- package/dist/audio/Wav.d.ts +56 -0
- package/dist/audio/Wav.d.ts.map +1 -0
- package/dist/audio/Wav.js +164 -0
- package/dist/audio/Wav.js.map +1 -0
- package/dist/audio/WebMidi.d.ts +58 -0
- package/dist/audio/WebMidi.d.ts.map +1 -0
- package/dist/audio/WebMidi.js +79 -0
- package/dist/audio/WebMidi.js.map +1 -0
- package/dist/audio/humanize/Onsets.d.ts.map +1 -1
- package/dist/audio/humanize/Onsets.js +1 -44
- package/dist/audio/humanize/Onsets.js.map +1 -1
- package/dist/audio/index.d.ts +43 -7
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +21 -3
- package/dist/audio/index.js.map +1 -1
- package/dist/battle/AttackPreview.d.ts.map +1 -1
- package/dist/battle/AttackPreview.js +8 -8
- package/dist/battle/AttackPreview.js.map +1 -1
- package/dist/battle/StatStages.d.ts.map +1 -1
- package/dist/battle/StatStages.js +2 -1
- package/dist/battle/StatStages.js.map +1 -1
- package/dist/board/Engine.d.ts +2 -0
- package/dist/board/Engine.d.ts.map +1 -1
- package/dist/board/Engine.js +1 -1
- package/dist/board/Engine.js.map +1 -1
- package/dist/board/HexSkirmish.d.ts.map +1 -1
- package/dist/board/HexSkirmish.js +11 -72
- package/dist/board/HexSkirmish.js.map +1 -1
- package/dist/board/Tactics.d.ts.map +1 -1
- package/dist/board/Tactics.js +3 -5
- package/dist/board/Tactics.js.map +1 -1
- package/dist/board/tourney.d.ts.map +1 -1
- package/dist/board/tourney.js +2 -19
- package/dist/board/tourney.js.map +1 -1
- package/dist/core/Handles.js +7 -7
- package/dist/core/Handles.js.map +1 -1
- package/dist/mw_games.global.js +25 -22
- package/dist/mwl/conditions.d.ts +12 -0
- package/dist/mwl/conditions.d.ts.map +1 -1
- package/dist/mwl/conditions.js +49 -8
- package/dist/mwl/conditions.js.map +1 -1
- package/dist/mwl/index.d.ts +13 -0
- package/dist/mwl/index.d.ts.map +1 -1
- package/dist/mwl/index.js +7 -0
- package/dist/mwl/index.js.map +1 -1
- package/dist/mwl/objects.d.ts +31 -0
- package/dist/mwl/objects.d.ts.map +1 -0
- package/dist/mwl/objects.js +83 -0
- package/dist/mwl/objects.js.map +1 -0
- package/dist/mwl/package-container.d.ts +185 -0
- package/dist/mwl/package-container.d.ts.map +1 -0
- package/dist/mwl/package-container.js +446 -0
- package/dist/mwl/package-container.js.map +1 -0
- package/dist/mwl/package-document.d.ts +217 -0
- package/dist/mwl/package-document.d.ts.map +1 -0
- package/dist/mwl/package-document.js +639 -0
- package/dist/mwl/package-document.js.map +1 -0
- package/dist/mwl/package-rom.d.ts +37 -0
- package/dist/mwl/package-rom.d.ts.map +1 -0
- package/dist/mwl/package-rom.js +96 -0
- package/dist/mwl/package-rom.js.map +1 -0
- package/dist/mwl/package-verify.d.ts +34 -0
- package/dist/mwl/package-verify.d.ts.map +1 -0
- package/dist/mwl/package-verify.js +111 -0
- package/dist/mwl/package-verify.js.map +1 -0
- package/dist/mwl/package.d.ts +184 -0
- package/dist/mwl/package.d.ts.map +1 -0
- package/dist/mwl/package.js +382 -0
- package/dist/mwl/package.js.map +1 -0
- package/dist/mwl/schema.d.ts +1 -1
- package/dist/mwl/schema.d.ts.map +1 -1
- package/dist/mwl/tables.d.ts +2 -0
- package/dist/mwl/tables.d.ts.map +1 -1
- package/dist/mwl/tables.js +13 -0
- package/dist/mwl/tables.js.map +1 -1
- package/dist/physics/PhysicsWorld.d.ts.map +1 -1
- package/dist/physics/PhysicsWorld.js +4 -3
- package/dist/physics/PhysicsWorld.js.map +1 -1
- package/dist/physics/XpbdWorld.d.ts +23 -5
- package/dist/physics/XpbdWorld.d.ts.map +1 -1
- package/dist/physics/XpbdWorld.js +91 -6
- package/dist/physics/XpbdWorld.js.map +1 -1
- package/dist/testing/FakeAudio.d.ts +149 -0
- package/dist/testing/FakeAudio.d.ts.map +1 -0
- package/dist/testing/FakeAudio.js +219 -0
- package/dist/testing/FakeAudio.js.map +1 -0
- package/dist/testing/index.d.ts +18 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +22 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/threads/index.d.ts +2 -2
- package/dist/threads/index.d.ts.map +1 -1
- package/dist/threads/index.js +1 -1
- package/dist/threads/index.js.map +1 -1
- package/dist/threads/spawn.d.ts +32 -0
- package/dist/threads/spawn.d.ts.map +1 -1
- package/dist/threads/spawn.js +35 -11
- package/dist/threads/spawn.js.map +1 -1
- package/dist/three-d/Collision3D.d.ts +46 -2
- package/dist/three-d/Collision3D.d.ts.map +1 -1
- package/dist/three-d/Collision3D.js +46 -2
- package/dist/three-d/Collision3D.js.map +1 -1
- package/dist/three-d/Grid.d.ts +12 -1
- package/dist/three-d/Grid.d.ts.map +1 -1
- package/dist/three-d/Grid.js +12 -1
- package/dist/three-d/Grid.js.map +1 -1
- package/dist/three-d/Vox.d.ts +13 -1
- package/dist/three-d/Vox.d.ts.map +1 -1
- package/dist/three-d/Vox.js +13 -1
- package/dist/three-d/Vox.js.map +1 -1
- package/dist/two-d/Game.d.ts +32 -1
- package/dist/two-d/Game.d.ts.map +1 -1
- package/dist/two-d/Game.js +105 -14
- package/dist/two-d/Game.js.map +1 -1
- package/dist/two-d/Scene2D.d.ts +24 -0
- package/dist/two-d/Scene2D.d.ts.map +1 -1
- package/dist/two-d/Scene2D.js +30 -0
- package/dist/two-d/Scene2D.js.map +1 -1
- package/dist/two-d/render/HazeLayer.d.ts.map +1 -1
- package/dist/two-d/render/HazeLayer.js +2 -1
- package/dist/two-d/render/HazeLayer.js.map +1 -1
- package/dist/two-d/render/ImageModifiers.d.ts +4 -2
- package/dist/two-d/render/ImageModifiers.d.ts.map +1 -1
- package/dist/two-d/render/ImageModifiers.js +13 -8
- package/dist/two-d/render/ImageModifiers.js.map +1 -1
- package/dist/two-d/render/NeonGlow.d.ts.map +1 -1
- package/dist/two-d/render/NeonGlow.js +2 -1
- package/dist/two-d/render/NeonGlow.js.map +1 -1
- package/dist/two-d/render/Particles.d.ts.map +1 -1
- package/dist/two-d/render/Particles.js +2 -1
- package/dist/two-d/render/Particles.js.map +1 -1
- package/dist/two-d/render/SceneTransition.d.ts +301 -0
- package/dist/two-d/render/SceneTransition.d.ts.map +1 -0
- package/dist/two-d/render/SceneTransition.js +458 -0
- package/dist/two-d/render/SceneTransition.js.map +1 -0
- package/dist/two-d/render/ScreenEffects.d.ts.map +1 -1
- package/dist/two-d/render/ScreenEffects.js +2 -1
- package/dist/two-d/render/ScreenEffects.js.map +1 -1
- package/dist/two-d/render/SkeletalSprite.d.ts.map +1 -1
- package/dist/two-d/render/SkeletalSprite.js +2 -1
- package/dist/two-d/render/SkeletalSprite.js.map +1 -1
- package/dist/two-d/render/Types2D.d.ts +2 -2
- package/dist/two-d/render/Types2D.d.ts.map +1 -1
- package/dist/two-d/render/Types2D.js +2 -2
- package/dist/two-d/render/Types2D.js.map +1 -1
- package/dist/two-d/render/index.d.ts +2 -0
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +1 -0
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/two-d/ui/Bar.d.ts.map +1 -1
- package/dist/two-d/ui/Bar.js +3 -5
- package/dist/two-d/ui/Bar.js.map +1 -1
- package/dist/two-d/ui/Button.d.ts +23 -0
- package/dist/two-d/ui/Button.d.ts.map +1 -1
- package/dist/two-d/ui/Button.js +37 -0
- package/dist/two-d/ui/Button.js.map +1 -1
- package/dist/two-d/ui/Dropdown.d.ts +5 -1
- package/dist/two-d/ui/Dropdown.d.ts.map +1 -1
- package/dist/two-d/ui/Dropdown.js +8 -1
- package/dist/two-d/ui/Dropdown.js.map +1 -1
- package/dist/two-d/ui/DropdownView.d.ts +63 -3
- package/dist/two-d/ui/DropdownView.d.ts.map +1 -1
- package/dist/two-d/ui/DropdownView.js +168 -9
- package/dist/two-d/ui/DropdownView.js.map +1 -1
- package/dist/two-d/ui/FloatingText.d.ts.map +1 -1
- package/dist/two-d/ui/FloatingText.js +2 -1
- package/dist/two-d/ui/FloatingText.js.map +1 -1
- package/dist/two-d/ui/IconGrid.d.ts.map +1 -1
- package/dist/two-d/ui/IconGrid.js +2 -1
- package/dist/two-d/ui/IconGrid.js.map +1 -1
- package/dist/two-d/ui/ListView.d.ts.map +1 -1
- package/dist/two-d/ui/ListView.js +2 -1
- package/dist/two-d/ui/ListView.js.map +1 -1
- package/dist/two-d/ui/ScrollBox.d.ts +96 -0
- package/dist/two-d/ui/ScrollBox.d.ts.map +1 -1
- package/dist/two-d/ui/ScrollBox.js +218 -5
- package/dist/two-d/ui/ScrollBox.js.map +1 -1
- package/dist/two-d/ui/ScrollText.d.ts +131 -0
- package/dist/two-d/ui/ScrollText.d.ts.map +1 -0
- package/dist/two-d/ui/ScrollText.js +218 -0
- package/dist/two-d/ui/ScrollText.js.map +1 -0
- package/dist/two-d/ui/Slider.d.ts.map +1 -1
- package/dist/two-d/ui/Slider.js +4 -4
- package/dist/two-d/ui/Slider.js.map +1 -1
- package/dist/two-d/ui/Tooltip.d.ts +70 -1
- package/dist/two-d/ui/Tooltip.d.ts.map +1 -1
- package/dist/two-d/ui/Tooltip.js +115 -3
- package/dist/two-d/ui/Tooltip.js.map +1 -1
- package/dist/two-d/ui/Window.d.ts +35 -2
- package/dist/two-d/ui/Window.d.ts.map +1 -1
- package/dist/two-d/ui/Window.js +58 -3
- package/dist/two-d/ui/Window.js.map +1 -1
- package/dist/two-d/ui/index.d.ts +7 -5
- package/dist/two-d/ui/index.d.ts.map +1 -1
- package/dist/two-d/ui/index.js +3 -2
- package/dist/two-d/ui/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -3
- package/sbom.cdx.json +19 -260
- package/tools/audio.d.mts +75 -0
- package/tools/audio.mjs +437 -0
- package/tools/browser-driver.mjs +3 -3
- package/tools/browser-smoke.mjs +19 -2
- package/tools/build-tablebases.mjs +7 -3
- package/tools/compile-openings.mjs +7 -3
- package/tools/compile-resources.d.mts +2 -0
- package/tools/compile-resources.mjs +13 -1
- package/tools/emit-page.d.mts +6 -0
- package/tools/emit-page.mjs +43 -3
- package/tools/find-chrome.mjs +4 -2
- package/tools/init.mjs +11 -3
- package/tools/mwl.mjs +144 -2
- package/tools/play-uci.mjs +9 -5
- package/tools/vite.mjs +9 -1
- package/tools.md +2 -1
package/tools/audio.mjs
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, extname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `mwg-audio`: the developer-side tool for a game's audio files. It tells what a file is
|
|
8
|
+
* (`inspect`), renders a MIDI file to a WAV through any registered engine (`render`), converts
|
|
9
|
+
* between the formats the library reads and writes (`convert`), measures what the audio costs a
|
|
10
|
+
* page and the memory a bus holds once tracks are rendered (`budget`), and says how far apart
|
|
11
|
+
* two renders sound (`compare`). Nothing here runs in a browser or reaches a player.
|
|
12
|
+
*
|
|
13
|
+
* The chip formats (MWA, YM, VGM, NSF, SID) belong to a separate package: `inspect` names them
|
|
14
|
+
* by their signature, and `convert` hands them to `--converter <module>`, a module whose default
|
|
15
|
+
* export is `(bytes, name) => Uint8Array`. Engines for `render --synth` come from `--module
|
|
16
|
+
* <file>`, a module that calls `registerMidiSynth` when imported.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
|
|
21
|
+
/** the library: the TypeScript source in this repository, the built `dist/` in an installed package */
|
|
22
|
+
async function loadLibrary() {
|
|
23
|
+
try {
|
|
24
|
+
const [midi, render, soundFont, wav, resample, distance, synth] = await Promise.all([
|
|
25
|
+
import('../src/audio/Midi.ts'),
|
|
26
|
+
import('../src/audio/MidiRender.ts'),
|
|
27
|
+
import('../src/audio/SoundFont.ts'),
|
|
28
|
+
import('../src/audio/Wav.ts'),
|
|
29
|
+
import('../src/audio/Resample.ts'),
|
|
30
|
+
import('../src/audio/SpectralDistance.ts'),
|
|
31
|
+
import('../src/audio/MidiSynth.ts'),
|
|
32
|
+
]);
|
|
33
|
+
return { ...midi, ...render, ...soundFont, ...wav, ...resample, ...distance, ...synth };
|
|
34
|
+
} catch {
|
|
35
|
+
return import(pathToFileURL(join(HERE, '..', 'dist', 'audio', 'index.js')).href);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ascii = (bytes, from, to) => String.fromCharCode(...bytes.subarray(from, to));
|
|
40
|
+
|
|
41
|
+
const SYSEX_MAKERS = new Map([
|
|
42
|
+
[0x41, 'Roland'],
|
|
43
|
+
[0x42, 'Korg'],
|
|
44
|
+
[0x43, 'Yamaha'],
|
|
45
|
+
[0x44, 'Casio'],
|
|
46
|
+
[0x47, 'Akai'],
|
|
47
|
+
[0x7e, 'universal non-real-time'],
|
|
48
|
+
[0x7f, 'universal real-time'],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Names a file by its content, falling back on the extension for formats with no signature.
|
|
53
|
+
* Returns `{ format, kind }` where kind is `midi`, `wav`, `soundfont`, `compressed`, `chip`,
|
|
54
|
+
* `tracker`, `sysex` or `unknown`.
|
|
55
|
+
*/
|
|
56
|
+
export function detectAudioFormat(bytes, name = '') {
|
|
57
|
+
const head4 = ascii(bytes, 0, 4);
|
|
58
|
+
const extension = extname(name).toLowerCase();
|
|
59
|
+
if (head4 === 'MThd') return { format: 'MIDI', kind: 'midi' };
|
|
60
|
+
if (head4 === 'RIFF') {
|
|
61
|
+
const form = ascii(bytes, 8, 12);
|
|
62
|
+
if (form === 'WAVE') return { format: 'WAV', kind: 'wav' };
|
|
63
|
+
if (form === 'sfbk') return { format: 'SoundFont 2', kind: 'soundfont' };
|
|
64
|
+
if (form === 'RMID') return { format: 'RIFF MIDI', kind: 'midi' };
|
|
65
|
+
}
|
|
66
|
+
if (head4 === 'OggS')
|
|
67
|
+
return { format: ascii(bytes, 28, 35) === '\x01vorbis' ? 'Ogg Vorbis' : 'Ogg', kind: 'compressed' };
|
|
68
|
+
if (head4 === 'fLaC') return { format: 'FLAC', kind: 'compressed' };
|
|
69
|
+
if (head4 === 'MWA1') return { format: 'MWA', kind: 'chip' };
|
|
70
|
+
if (head4 === 'NESM') return { format: 'NSF', kind: 'chip' };
|
|
71
|
+
if (head4 === 'PSID' || head4 === 'RSID') return { format: 'SID', kind: 'chip' };
|
|
72
|
+
if (head4 === 'Vgm ') return { format: 'VGM', kind: 'chip' };
|
|
73
|
+
if (head4 === 'IMPM') return { format: 'Impulse Tracker', kind: 'tracker' };
|
|
74
|
+
if (ascii(bytes, 0, 17) === 'Extended Module: ') return { format: 'XM', kind: 'tracker' };
|
|
75
|
+
if (head4.startsWith('YM') && /^YM[3456]/.test(head4)) return { format: 'YM', kind: 'chip' };
|
|
76
|
+
if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
|
|
77
|
+
return {
|
|
78
|
+
format: extension === '.vgz' ? 'VGZ (gzip VGM)' : 'gzip',
|
|
79
|
+
kind: extension === '.vgz' ? 'chip' : 'unknown',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (bytes[0] === 0xf0 && bytes.includes(0xf7)) return { format: 'MIDI SysEx', kind: 'sysex' };
|
|
83
|
+
if (head4.startsWith('ID3') || (bytes[0] === 0xff && (bytes[1] & 0xe0) === 0xe0))
|
|
84
|
+
return { format: 'MP3', kind: 'compressed' };
|
|
85
|
+
if (ascii(bytes, 4, 8) === 'ftyp') return { format: 'MP4 audio', kind: 'compressed' };
|
|
86
|
+
if (extension === '.mod' && bytes.length > 1084) return { format: 'MOD', kind: 'tracker' };
|
|
87
|
+
return { format: 'unknown', kind: 'unknown' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** the SysEx messages of a file: how many, who made the instrument, how large */
|
|
91
|
+
function sysexSummary(bytes) {
|
|
92
|
+
const messages = [];
|
|
93
|
+
let start = -1;
|
|
94
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
95
|
+
if (bytes[i] === 0xf0) start = i;
|
|
96
|
+
else if (bytes[i] === 0xf7 && start !== -1) {
|
|
97
|
+
messages.push({ maker: bytes[start + 1], size: i - start + 1 });
|
|
98
|
+
start = -1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
messages: messages.length,
|
|
103
|
+
makers: [...new Set(messages.map((m) => SYSEX_MAKERS.get(m.maker) ?? `0x${m.maker.toString(16)}`))],
|
|
104
|
+
largestMessage: Math.max(0, ...messages.map((m) => m.size)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** the facts about a MIDI file a developer wants before rendering it */
|
|
109
|
+
function midiSummary(lib, bytes) {
|
|
110
|
+
const file = lib.parseMidi(bytes);
|
|
111
|
+
const notes = lib.scheduleMidi(file);
|
|
112
|
+
const channels = {};
|
|
113
|
+
for (const note of notes) {
|
|
114
|
+
const entry = (channels[note.channel] ??= { notes: 0, programs: new Set() });
|
|
115
|
+
entry.notes++;
|
|
116
|
+
entry.programs.add(note.channel === 9 ? 'drums' : note.program);
|
|
117
|
+
}
|
|
118
|
+
const end = notes.reduce((latest, note) => Math.max(latest, note.time + note.duration), 0);
|
|
119
|
+
return {
|
|
120
|
+
ticksPerQuarter: file.ticksPerQuarter,
|
|
121
|
+
events: file.events.length,
|
|
122
|
+
notes: notes.length,
|
|
123
|
+
seconds: Number(end.toFixed(3)),
|
|
124
|
+
loopStartSeconds: lib.midiLoopStart(file),
|
|
125
|
+
channels: Object.fromEntries(
|
|
126
|
+
Object.entries(channels).map(([channel, entry]) => [
|
|
127
|
+
channel,
|
|
128
|
+
{ notes: entry.notes, programs: [...entry.programs] },
|
|
129
|
+
]),
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** describes one file: its format and the numbers that matter for that format */
|
|
135
|
+
export async function inspectAudio(bytes, name = '') {
|
|
136
|
+
const lib = await loadLibrary();
|
|
137
|
+
const detected = detectAudioFormat(bytes, name);
|
|
138
|
+
const report = { name, bytes: bytes.length, ...detected };
|
|
139
|
+
if (detected.kind === 'wav') Object.assign(report, lib.wavInfo(bytes));
|
|
140
|
+
else if (detected.kind === 'midi' && detected.format === 'MIDI') Object.assign(report, midiSummary(lib, bytes));
|
|
141
|
+
else if (detected.kind === 'sysex') Object.assign(report, sysexSummary(bytes));
|
|
142
|
+
else if (detected.kind === 'soundfont') {
|
|
143
|
+
const font = lib.parseSoundFont(bytes);
|
|
144
|
+
report.presets = font.presets ? font.presets.length : undefined;
|
|
145
|
+
}
|
|
146
|
+
return report;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** `"0,9"` as `[0, 9]`, each a MIDI channel */
|
|
150
|
+
export function parseChannelList(text) {
|
|
151
|
+
return text.split(',').map((item) => {
|
|
152
|
+
const channel = Number(item.trim());
|
|
153
|
+
if (!Number.isInteger(channel) || channel < 0 || channel > 15)
|
|
154
|
+
throw new Error(`invalid channel "${item}": expected 0 to 15`);
|
|
155
|
+
return channel;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function importModule(path) {
|
|
160
|
+
return import(pathToFileURL(resolve(path)).href);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Renders MIDI bytes to WAV bytes. `synth` is an engine name or a comma list tried in order
|
|
165
|
+
* (the engines come from `modules`, imported first), `soundfont` the bytes of a SoundFont.
|
|
166
|
+
*/
|
|
167
|
+
export async function renderToWav(midiBytes, options = {}) {
|
|
168
|
+
const lib = await loadLibrary();
|
|
169
|
+
for (const module of options.modules ?? []) await importModule(module);
|
|
170
|
+
const rendered = lib.renderMidiToBuffer(lib.parseMidi(midiBytes), {
|
|
171
|
+
synth: options.synth ? options.synth.split(',').map((name) => name.trim()) : undefined,
|
|
172
|
+
soundfont: options.soundfont ? lib.parseSoundFont(options.soundfont) : null,
|
|
173
|
+
sampleRate: options.rate,
|
|
174
|
+
gain: options.gain,
|
|
175
|
+
channels: options.channels,
|
|
176
|
+
});
|
|
177
|
+
return lib.encodeWav(rendered, options.float ? 'float32' : 'pcm16');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** resamples WAV bytes to another rate */
|
|
181
|
+
export async function resampleWav(wavBytes, rate, float = false) {
|
|
182
|
+
const lib = await loadLibrary();
|
|
183
|
+
return lib.encodeWav(lib.resampleStereo(lib.decodeWav(wavBytes), rate), float ? 'float32' : 'pcm16');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** how far apart two WAV files sound, in dB of log-spectrum difference */
|
|
187
|
+
export async function compareWavs(a, b, options = {}) {
|
|
188
|
+
const lib = await loadLibrary();
|
|
189
|
+
return lib.spectralDistance(lib.decodeWav(a), lib.decodeWav(b), options);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const AUDIO_EXTENSIONS = new Set([
|
|
193
|
+
'.mid',
|
|
194
|
+
'.midi',
|
|
195
|
+
'.wav',
|
|
196
|
+
'.ogg',
|
|
197
|
+
'.mp3',
|
|
198
|
+
'.flac',
|
|
199
|
+
'.m4a',
|
|
200
|
+
'.mwa',
|
|
201
|
+
'.sf2',
|
|
202
|
+
'.sf3',
|
|
203
|
+
'.syx',
|
|
204
|
+
'.nsf',
|
|
205
|
+
'.sid',
|
|
206
|
+
'.vgm',
|
|
207
|
+
'.ym',
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
/** every audio file under the given files and folders */
|
|
211
|
+
function collectFiles(paths) {
|
|
212
|
+
const found = [];
|
|
213
|
+
const visit = (path) => {
|
|
214
|
+
const stats = statSync(path);
|
|
215
|
+
if (stats.isDirectory()) {
|
|
216
|
+
for (const entry of readdirSync(path).sort()) visit(join(path, entry));
|
|
217
|
+
} else if (AUDIO_EXTENSIONS.has(extname(path).toLowerCase())) {
|
|
218
|
+
found.push(path);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
for (const path of paths) visit(path);
|
|
222
|
+
return found;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const MEGABYTE = 1024 * 1024;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* What the audio of a game costs: for each file the bytes on disk, the bytes it adds to a page
|
|
229
|
+
* (base64 makes it a third larger) and the memory it holds once decoded into a bus. A MIDI file
|
|
230
|
+
* is rendered to PCM before it plays, so its memory is its length times the render rate; a
|
|
231
|
+
* compressed file's decoded size cannot be known without decoding it, and is left out of the
|
|
232
|
+
* total rather than guessed.
|
|
233
|
+
*/
|
|
234
|
+
export async function audioBudget(paths, options = {}) {
|
|
235
|
+
const lib = await loadLibrary();
|
|
236
|
+
const rate = options.rate ?? 22050;
|
|
237
|
+
const maxSeconds = options.maxSeconds ?? 480;
|
|
238
|
+
const files = [];
|
|
239
|
+
for (const path of collectFiles(paths)) {
|
|
240
|
+
const bytes = readFileSync(path);
|
|
241
|
+
const detected = detectAudioFormat(bytes, path);
|
|
242
|
+
let seconds = null;
|
|
243
|
+
let decodedBytes = null;
|
|
244
|
+
let renderRate = rate;
|
|
245
|
+
let sizingSeconds = null;
|
|
246
|
+
if (detected.kind === 'midi' && detected.format === 'MIDI') {
|
|
247
|
+
const summary = midiSummary(lib, bytes);
|
|
248
|
+
seconds = summary.seconds;
|
|
249
|
+
//the memory a render holds includes its decay tail; the track's own length does not
|
|
250
|
+
sizingSeconds = Math.min(maxSeconds, seconds + 1.5);
|
|
251
|
+
} else if (detected.kind === 'wav') {
|
|
252
|
+
const info = lib.wavInfo(bytes);
|
|
253
|
+
seconds = info.seconds;
|
|
254
|
+
sizingSeconds = seconds;
|
|
255
|
+
renderRate = info.sampleRate;
|
|
256
|
+
}
|
|
257
|
+
if (sizingSeconds !== null) decodedBytes = Math.ceil(sizingSeconds * renderRate) * 2 * 4;
|
|
258
|
+
files.push({
|
|
259
|
+
path,
|
|
260
|
+
format: detected.format,
|
|
261
|
+
diskBytes: bytes.length,
|
|
262
|
+
pageBytes: Math.ceil((bytes.length * 4) / 3),
|
|
263
|
+
seconds,
|
|
264
|
+
decodedBytes,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const total = (key) => files.reduce((sum, file) => sum + (file[key] ?? 0), 0);
|
|
268
|
+
const problems = [];
|
|
269
|
+
if (options.maxMegabytes !== undefined && total('decodedBytes') > options.maxMegabytes * MEGABYTE) {
|
|
270
|
+
problems.push(
|
|
271
|
+
`decoded audio is ${(total('decodedBytes') / MEGABYTE).toFixed(1)} MB, over the ${options.maxMegabytes} MB budget`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
if (file.seconds !== null && file.seconds > (options.maxTrackSeconds ?? Infinity)) {
|
|
276
|
+
problems.push(`${file.path} is ${file.seconds.toFixed(0)} s, over ${options.maxTrackSeconds} s`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
files,
|
|
281
|
+
totals: { diskBytes: total('diskBytes'), pageBytes: total('pageBytes'), decodedBytes: total('decodedBytes') },
|
|
282
|
+
problems,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const USAGE = `usage: mwg-audio <command>
|
|
287
|
+
inspect <file>... the format and its numbers: MIDI notes and channels, WAV layout, SysEx messages
|
|
288
|
+
render <in.mid> -o <out.wav> [--synth <names>] [--module <file>...] [--soundfont <file>] [--rate <hz>]
|
|
289
|
+
[--channels 0,9] [--gain <0-1>] [--float]
|
|
290
|
+
a MIDI file to WAV through a registered engine (--module imports the code that registers it)
|
|
291
|
+
convert <in> <out> [--rate <hz>] [--converter <module>]
|
|
292
|
+
.mid to .wav, .wav to .wav at another rate, or any file through a converter module
|
|
293
|
+
(MWA and the other chip formats come from the retro-audio package's converter)
|
|
294
|
+
budget <file|folder>... [--rate <hz>] [--max-mb <n>] [--max-track-seconds <n>] [--json]
|
|
295
|
+
disk, page and decoded memory cost of the audio; exits 1 over a budget
|
|
296
|
+
compare <a.wav> <b.wav> [--max <db>] spectral distance between two renders; exits 1 over --max`;
|
|
297
|
+
|
|
298
|
+
async function main(argv) {
|
|
299
|
+
const [command, ...rest] = argv;
|
|
300
|
+
if (!command || command === '--help' || command === '-h') {
|
|
301
|
+
console.log(USAGE);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const flag = (name) => {
|
|
305
|
+
const at = rest.indexOf(name);
|
|
306
|
+
return at === -1 ? undefined : rest[at + 1];
|
|
307
|
+
};
|
|
308
|
+
const valuesAfter = (name) => {
|
|
309
|
+
const out = [];
|
|
310
|
+
for (let at = rest.indexOf(name); at !== -1 && at < rest.length; at = rest.indexOf(name, at + 1)) {
|
|
311
|
+
for (let i = at + 1; i < rest.length && !rest[i].startsWith('--') && rest[i] !== '-o'; i++)
|
|
312
|
+
out.push(rest[i]);
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
};
|
|
316
|
+
const number = (name) => (flag(name) === undefined ? undefined : Number(flag(name)));
|
|
317
|
+
const positional = () => {
|
|
318
|
+
const out = [];
|
|
319
|
+
const valued = new Set([
|
|
320
|
+
'-o',
|
|
321
|
+
'--synth',
|
|
322
|
+
'--module',
|
|
323
|
+
'--soundfont',
|
|
324
|
+
'--rate',
|
|
325
|
+
'--channels',
|
|
326
|
+
'--gain',
|
|
327
|
+
'--converter',
|
|
328
|
+
'--max',
|
|
329
|
+
'--max-mb',
|
|
330
|
+
'--max-track-seconds',
|
|
331
|
+
]);
|
|
332
|
+
for (let i = 0; i < rest.length; i++) {
|
|
333
|
+
if (valued.has(rest[i])) i++;
|
|
334
|
+
else if (!rest[i].startsWith('-')) out.push(rest[i]);
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (command === 'inspect') {
|
|
340
|
+
const files = positional();
|
|
341
|
+
if (files.length === 0) throw new Error('usage: mwg-audio inspect <file>...');
|
|
342
|
+
for (const file of files) console.log(JSON.stringify(await inspectAudio(readFileSync(file), file), null, 2));
|
|
343
|
+
} else if (command === 'render') {
|
|
344
|
+
const [input] = positional();
|
|
345
|
+
const out = flag('-o');
|
|
346
|
+
if (!input || !out) throw new Error('usage: mwg-audio render <in.mid> -o <out.wav>');
|
|
347
|
+
const wav = await renderToWav(readFileSync(input), {
|
|
348
|
+
synth: flag('--synth'),
|
|
349
|
+
modules: valuesAfter('--module'),
|
|
350
|
+
soundfont: flag('--soundfont') ? readFileSync(flag('--soundfont')) : undefined,
|
|
351
|
+
rate: number('--rate'),
|
|
352
|
+
gain: number('--gain'),
|
|
353
|
+
channels: flag('--channels') ? parseChannelList(flag('--channels')) : undefined,
|
|
354
|
+
float: rest.includes('--float'),
|
|
355
|
+
});
|
|
356
|
+
mkdirSync(dirname(resolve(out)), { recursive: true });
|
|
357
|
+
writeFileSync(out, wav);
|
|
358
|
+
console.log(`wrote ${out}: ${(wav.length / 1024).toFixed(0)} KB`);
|
|
359
|
+
} else if (command === 'convert') {
|
|
360
|
+
const [input, output] = positional();
|
|
361
|
+
if (!input || !output) throw new Error('usage: mwg-audio convert <in> <out>');
|
|
362
|
+
const bytes = readFileSync(input);
|
|
363
|
+
const target = extname(output).toLowerCase();
|
|
364
|
+
let result;
|
|
365
|
+
if (flag('--converter')) {
|
|
366
|
+
const converter = (await importModule(flag('--converter'))).default;
|
|
367
|
+
if (typeof converter !== 'function')
|
|
368
|
+
throw new Error('a converter module must export a default function (bytes, name) => Uint8Array');
|
|
369
|
+
result = await converter(bytes, input);
|
|
370
|
+
} else if (target === '.mwa') {
|
|
371
|
+
throw new Error(
|
|
372
|
+
"converting to MWA is the retro-audio package's job: pass its converter with --converter <module>",
|
|
373
|
+
);
|
|
374
|
+
} else if (target === '.wav') {
|
|
375
|
+
const detected = detectAudioFormat(bytes, input);
|
|
376
|
+
if (detected.kind === 'midi') result = await renderToWav(bytes, { rate: number('--rate') });
|
|
377
|
+
else if (detected.kind === 'wav')
|
|
378
|
+
result = number('--rate') ? await resampleWav(bytes, number('--rate')) : bytes;
|
|
379
|
+
else throw new Error(`cannot convert ${detected.format} to .wav without a converter`);
|
|
380
|
+
} else {
|
|
381
|
+
throw new Error(`no conversion to "${target}" is built in: pass --converter <module>`);
|
|
382
|
+
}
|
|
383
|
+
mkdirSync(dirname(resolve(output)), { recursive: true });
|
|
384
|
+
writeFileSync(output, result);
|
|
385
|
+
console.log(`wrote ${output}: ${(result.length / 1024).toFixed(0)} KB`);
|
|
386
|
+
} else if (command === 'budget') {
|
|
387
|
+
const paths = positional();
|
|
388
|
+
if (paths.length === 0) throw new Error('usage: mwg-audio budget <file|folder>...');
|
|
389
|
+
const report = await audioBudget(paths, {
|
|
390
|
+
rate: number('--rate'),
|
|
391
|
+
maxMegabytes: number('--max-mb'),
|
|
392
|
+
maxTrackSeconds: number('--max-track-seconds'),
|
|
393
|
+
});
|
|
394
|
+
if (rest.includes('--json')) {
|
|
395
|
+
console.log(JSON.stringify(report, null, 2));
|
|
396
|
+
} else {
|
|
397
|
+
for (const file of report.files) {
|
|
398
|
+
const memory =
|
|
399
|
+
file.decodedBytes === null
|
|
400
|
+
? 'decoded size unknown'
|
|
401
|
+
: `${(file.decodedBytes / MEGABYTE).toFixed(1)} MB decoded`;
|
|
402
|
+
console.log(
|
|
403
|
+
`${file.path} ${file.format} ${(file.pageBytes / 1024).toFixed(0)} KB in the page ${memory}`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const { totals } = report;
|
|
407
|
+
console.log(
|
|
408
|
+
`total: ${(totals.pageBytes / 1024).toFixed(0)} KB in the page, ${(totals.decodedBytes / MEGABYTE).toFixed(1)} MB decoded (files of unknown decoded size not counted)`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
for (const problem of report.problems) console.error(problem);
|
|
412
|
+
if (report.problems.length > 0) process.exitCode = 1;
|
|
413
|
+
} else if (command === 'compare') {
|
|
414
|
+
const [a, b] = positional();
|
|
415
|
+
if (!a || !b) throw new Error('usage: mwg-audio compare <a.wav> <b.wav>');
|
|
416
|
+
const result = await compareWavs(readFileSync(a), readFileSync(b));
|
|
417
|
+
console.log(`${result.decibels.toFixed(2)} dB over ${result.windows} windows`);
|
|
418
|
+
if (number('--max') !== undefined && result.decibels > number('--max')) {
|
|
419
|
+
console.error(`over the ${number('--max')} dB limit`);
|
|
420
|
+
process.exitCode = 1;
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
throw new Error(`unknown command "${command}"\n${USAGE}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
//realpath, since an npm `bin` shim reaches this file through a symlink
|
|
428
|
+
if (
|
|
429
|
+
process.argv[1] &&
|
|
430
|
+
existsSync(process.argv[1]) &&
|
|
431
|
+
realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
432
|
+
) {
|
|
433
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
434
|
+
console.error(error instanceof Error ? error.message : error);
|
|
435
|
+
process.exitCode = 1;
|
|
436
|
+
});
|
|
437
|
+
}
|
package/tools/browser-driver.mjs
CHANGED
|
@@ -361,12 +361,12 @@ function findOnPath(names) {
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
/**
|
|
364
|
-
* Locates a browser executable without a driver install:
|
|
365
|
-
* `FIREFOX_PATH`
|
|
364
|
+
* Locates a browser executable without a driver install: `CHROME_PATH` (or `CHROME`) /
|
|
365
|
+
* `FIREFOX_PATH` overrides first, then well-known install locations. Returns null
|
|
366
366
|
* when neither yields anything, so callers skip rather than fail.
|
|
367
367
|
*/
|
|
368
368
|
export function findBrowser(kind) {
|
|
369
|
-
const override =
|
|
369
|
+
const override = kind === 'chrome' ? process.env.CHROME_PATH || process.env.CHROME : process.env.FIREFOX_PATH;
|
|
370
370
|
if (override) return override;
|
|
371
371
|
const onPath = findOnPath(kind === 'chrome' ? ['chrome', 'chromium', 'google-chrome'] : ['firefox']);
|
|
372
372
|
if (onPath) return onPath;
|
package/tools/browser-smoke.mjs
CHANGED
|
@@ -29,6 +29,23 @@ export async function waitForGame(page, url, pageErrors = []) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Waits for the page to draw a few frames instead of sleeping a fixed time: a software renderer can need
|
|
34
|
+
* far longer than any fixed pause, and a fast one far less. Capped, since a hidden tab throttles frames.
|
|
35
|
+
*/
|
|
36
|
+
async function settle(page, frames = 3, capMs = 5000) {
|
|
37
|
+
await page.evaluate(
|
|
38
|
+
([count, cap]) =>
|
|
39
|
+
new Promise((done) => {
|
|
40
|
+
const timer = setTimeout(done, cap);
|
|
41
|
+
const next = (left) =>
|
|
42
|
+
left === 0 ? (clearTimeout(timer), done()) : requestAnimationFrame(() => next(left - 1));
|
|
43
|
+
next(count);
|
|
44
|
+
}),
|
|
45
|
+
[frames, capMs],
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
/**
|
|
33
50
|
* Opens a built page from `file://` in headless Chrome and asserts it actually rendered.
|
|
34
51
|
*
|
|
@@ -76,11 +93,11 @@ export async function smokePage({
|
|
|
76
93
|
await waitForGame(page, url, pageErrors);
|
|
77
94
|
|
|
78
95
|
//let the scene finish its first frames before reading pixels back
|
|
79
|
-
await page
|
|
96
|
+
await settle(page);
|
|
80
97
|
if (keyToPress) {
|
|
81
98
|
await page.keyboard.press(keyToPress);
|
|
82
99
|
//give whatever the key opened time to lay itself out and draw
|
|
83
|
-
await page
|
|
100
|
+
await settle(page);
|
|
84
101
|
}
|
|
85
102
|
if (after) await after(page);
|
|
86
103
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join, relative } from 'node:path';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
//the framework's own modules: the TypeScript sources inside this repository, the built `dist` inside an
|
|
7
|
+
//installed package, which ships no `src`
|
|
8
|
+
const fromSource = existsSync(new URL('../src/board/tablebase.ts', import.meta.url));
|
|
9
|
+
const load = (path) => import(fromSource ? `../src/${path}.ts` : `../dist/${path}.js`);
|
|
10
|
+
const { huffEncode } = await load('board/tablebase');
|
|
11
|
+
const { spawn } = await load('threads/spawn');
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Generates pawnless endgame tablebases (WDL plus distance-to-mate) and ships
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { join, relative } from 'node:path';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
//the framework's own modules: the TypeScript sources inside this repository, the built `dist` inside an
|
|
7
|
+
//installed package, which ships no `src`
|
|
8
|
+
const fromSource = existsSync(new URL('../src/board/chess.ts', import.meta.url));
|
|
9
|
+
const load = (path) => import(fromSource ? `../src/${path}.ts` : `../dist/${path}.js`);
|
|
10
|
+
const { applyMove, legalMoves, positionKey, sq, squareName, startingChess } = await load('board/chess');
|
|
11
|
+
const { parseUciMove } = await load('board/openings');
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Compiles opening tables (`eco`, `name`, `pgn` TSV rows, one per line after the header)
|
|
@@ -28,6 +28,8 @@ export interface CompileResourcesGroup {
|
|
|
28
28
|
|
|
29
29
|
export interface CompileResourcesResult {
|
|
30
30
|
groups: CompileResourcesGroup[];
|
|
31
|
+
/** the asset path of every embedded file, as game code passes it to `load()` */
|
|
32
|
+
keys: string[];
|
|
31
33
|
rawBytes: number;
|
|
32
34
|
embeddedBytes: number;
|
|
33
35
|
webpConverted: number;
|
|
@@ -219,6 +219,7 @@ export async function compileResources({
|
|
|
219
219
|
|
|
220
220
|
const creditsFile = join(from, 'credits.json');
|
|
221
221
|
let credits = null;
|
|
222
|
+
const keys = [];
|
|
222
223
|
|
|
223
224
|
for (const file of files) {
|
|
224
225
|
if (selected && !selected.has(keyOf(file))) continue;
|
|
@@ -283,6 +284,7 @@ export async function compileResources({
|
|
|
283
284
|
const name = group(key);
|
|
284
285
|
if (!groups.has(name)) groups.set(name, {});
|
|
285
286
|
groups.get(name)[key] = `data:${mimeType};base64,${bytes.toString('base64')}`;
|
|
287
|
+
keys.push(key);
|
|
286
288
|
rawBytes += data.length;
|
|
287
289
|
embeddedBytes += bytes.length;
|
|
288
290
|
}
|
|
@@ -300,7 +302,17 @@ export async function compileResources({
|
|
|
300
302
|
|
|
301
303
|
if (credits) await writeFile(join(to, 'CREDITS.txt'), credits, 'utf8');
|
|
302
304
|
|
|
303
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
groups: written,
|
|
307
|
+
keys,
|
|
308
|
+
rawBytes,
|
|
309
|
+
embeddedBytes,
|
|
310
|
+
webpConverted,
|
|
311
|
+
svgOptimised,
|
|
312
|
+
skipped,
|
|
313
|
+
credits,
|
|
314
|
+
soundFonts,
|
|
315
|
+
};
|
|
304
316
|
}
|
|
305
317
|
|
|
306
318
|
/** the plain-text credits for a manifest, one block per entry; throws on an entry that cannot be attributed */
|
package/tools/emit-page.d.mts
CHANGED
|
@@ -28,6 +28,8 @@ export interface EmitPageOptions {
|
|
|
28
28
|
soundFontUsage?: readonly SoundFontUsage[];
|
|
29
29
|
/** the page's Content-Security-Policy: on by default, `false` to leave it out */
|
|
30
30
|
csp?: boolean | CspOptions;
|
|
31
|
+
/** fails the build when the page a player downloads (the standalone file, else the page and its scripts) is larger than this many bytes */
|
|
32
|
+
maxPageBytes?: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export interface CspOptions {
|
|
@@ -49,6 +51,10 @@ export interface EmitPageResult {
|
|
|
49
51
|
soundFonts: SoundFontEmbed[];
|
|
50
52
|
compressed: CompressRow[] | null;
|
|
51
53
|
single: SingleFileResult | null;
|
|
54
|
+
/** embedded assets whose path is never named in the game's script: a hint, since a path built at run time is not seen */
|
|
55
|
+
unusedAssets: string[];
|
|
56
|
+
/** bytes a player downloads to open the page */
|
|
57
|
+
pageBytes: number;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
/** finishes a vite build as a page that opens from `file://` */
|
package/tools/emit-page.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile, writeFile, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { readFile, readdir, writeFile, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
4
|
import { join, resolve as resolvePath } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -81,6 +81,7 @@ export async function emitPage({
|
|
|
81
81
|
soundFont = true,
|
|
82
82
|
soundFontUsage,
|
|
83
83
|
csp = true,
|
|
84
|
+
maxPageBytes,
|
|
84
85
|
}) {
|
|
85
86
|
if (!dist) throw new Error('emitPage needs the dist folder of a vite build');
|
|
86
87
|
if (!assets && assetFiles?.length) throw new Error(`unknown asset file ${assetFiles[0]}: no assets folder`);
|
|
@@ -97,7 +98,7 @@ export async function emitPage({
|
|
|
97
98
|
soundFont,
|
|
98
99
|
soundFontUsage,
|
|
99
100
|
})
|
|
100
|
-
: { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0, svgOptimised: 0, soundFonts: [] };
|
|
101
|
+
: { groups: [], keys: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0, svgOptimised: 0, soundFonts: [] };
|
|
101
102
|
|
|
102
103
|
const html = await readFile(join(dist, 'index.html'), 'utf8');
|
|
103
104
|
const classic = toClassicScript(html);
|
|
@@ -124,7 +125,38 @@ export async function emitPage({
|
|
|
124
125
|
const standalone = await readFile(single.path, 'utf8');
|
|
125
126
|
await writeFile(single.path, withContentSecurityPolicy(standalone, { ...cspOptions, scripts }), 'utf8');
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
+
const unusedAssets = await unreferencedAssets(dist, classic.src, compiled.keys);
|
|
129
|
+
const pageBytes = await pageWeight(dist, single, compiled.groups);
|
|
130
|
+
if (maxPageBytes !== undefined && pageBytes > maxPageBytes) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`the page weighs ${kb(pageBytes)}, over the ${kb(maxPageBytes)} budget (maxPageBytes)` +
|
|
133
|
+
(unusedAssets.length
|
|
134
|
+
? `; ${unusedAssets.length} embedded asset(s) are never named in the game code: ${unusedAssets.slice(0, 5).join(', ')}`
|
|
135
|
+
: ''),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return { dist, entry: classic.src, ...compiled, compressed, single, unusedAssets, pageBytes };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** the embedded assets whose path appears nowhere in the game's script: a hint (a path built at run time is not seen), never an error */
|
|
142
|
+
async function unreferencedAssets(dist, entry, keys) {
|
|
143
|
+
if (keys.length === 0) return [];
|
|
144
|
+
const code = await readFile(join(dist, entry), 'utf8').catch(() => '');
|
|
145
|
+
return keys.filter((key) => !code.includes(key)).sort();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** the bytes a player downloads to open the page: the single file when there is one, else the page, the game script and the asset scripts */
|
|
149
|
+
async function pageWeight(dist, single, groups) {
|
|
150
|
+
const files = single
|
|
151
|
+
? [single.path]
|
|
152
|
+
: [
|
|
153
|
+
join(dist, 'index.html'),
|
|
154
|
+
...(await readdir(dist)).filter((name) => name.endsWith('.js')).map((name) => join(dist, name)),
|
|
155
|
+
...groups.map((g) => join(dist, 'assets', `${g.name}.js`)),
|
|
156
|
+
];
|
|
157
|
+
let total = 0;
|
|
158
|
+
for (const file of files) total += (await stat(file)).size;
|
|
159
|
+
return total;
|
|
128
160
|
}
|
|
129
161
|
|
|
130
162
|
const kb = (n) => (n / 1024).toFixed(1) + ' KB';
|
|
@@ -170,6 +202,7 @@ async function main(argv) {
|
|
|
170
202
|
svgo: argv.includes('--svgo'),
|
|
171
203
|
soundFont: !argv.includes('--no-soundfont-subset'),
|
|
172
204
|
soundFontUsage,
|
|
205
|
+
maxPageBytes: value('max-page-bytes') ? Number(value('max-page-bytes')) : undefined,
|
|
173
206
|
csp: argv.includes('--no-csp')
|
|
174
207
|
? false
|
|
175
208
|
: { connect: argv.filter((a) => a.startsWith('--connect=')).map((a) => a.slice('--connect='.length)) },
|
|
@@ -186,6 +219,13 @@ async function main(argv) {
|
|
|
186
219
|
);
|
|
187
220
|
}
|
|
188
221
|
if (argv.includes('--svgo')) console.log(` ${result.svgOptimised} SVG(s) optimised`);
|
|
222
|
+
console.log(` page weight ${kb(result.pageBytes)}`);
|
|
223
|
+
if (result.unusedAssets.length > 0) {
|
|
224
|
+
console.log(
|
|
225
|
+
` warning: ${result.unusedAssets.length} embedded asset(s) are never named in the game code, ` +
|
|
226
|
+
`so they only add weight (a path built at run time is not seen): ${result.unusedAssets.join(', ')}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
189
229
|
for (const font of result.soundFonts ?? []) {
|
|
190
230
|
console.log(
|
|
191
231
|
font.programs === null
|