@datamoc/mw_games 0.41.0 → 0.43.0

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 (160) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +62 -1
  3. package/REFERENCE.md +73 -8
  4. package/api-surface.json +696 -0
  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/audio/AudioLoader.d.ts +28 -0
  9. package/dist/audio/AudioLoader.d.ts.map +1 -0
  10. package/dist/audio/AudioLoader.js +53 -0
  11. package/dist/audio/AudioLoader.js.map +1 -0
  12. package/dist/audio/Channels.d.ts +1 -0
  13. package/dist/audio/Channels.d.ts.map +1 -1
  14. package/dist/audio/Channels.js +2 -1
  15. package/dist/audio/Channels.js.map +1 -1
  16. package/dist/audio/MidiRender.d.ts +19 -1
  17. package/dist/audio/MidiRender.d.ts.map +1 -1
  18. package/dist/audio/MidiRender.js +24 -1
  19. package/dist/audio/MidiRender.js.map +1 -1
  20. package/dist/audio/MusicPlayer.d.ts +80 -0
  21. package/dist/audio/MusicPlayer.d.ts.map +1 -0
  22. package/dist/audio/MusicPlayer.js +147 -0
  23. package/dist/audio/MusicPlayer.js.map +1 -0
  24. package/dist/audio/SampleDecoder.d.ts +16 -2
  25. package/dist/audio/SampleDecoder.d.ts.map +1 -1
  26. package/dist/audio/SampleDecoder.js +10 -6
  27. package/dist/audio/SampleDecoder.js.map +1 -1
  28. package/dist/audio/Sound.d.ts +2 -0
  29. package/dist/audio/Sound.d.ts.map +1 -1
  30. package/dist/audio/Sound.js +3 -0
  31. package/dist/audio/Sound.js.map +1 -1
  32. package/dist/audio/SoundFont.d.ts +40 -5
  33. package/dist/audio/SoundFont.d.ts.map +1 -1
  34. package/dist/audio/SoundFont.js +52 -14
  35. package/dist/audio/SoundFont.js.map +1 -1
  36. package/dist/audio/Synth.d.ts +1 -1
  37. package/dist/audio/Synth.d.ts.map +1 -1
  38. package/dist/audio/Synth.js +4 -0
  39. package/dist/audio/Synth.js.map +1 -1
  40. package/dist/audio/index.d.ts +8 -4
  41. package/dist/audio/index.d.ts.map +1 -1
  42. package/dist/audio/index.js +4 -2
  43. package/dist/audio/index.js.map +1 -1
  44. package/dist/core/DialogueText.d.ts +25 -0
  45. package/dist/core/DialogueText.d.ts.map +1 -1
  46. package/dist/core/DialogueText.js +25 -0
  47. package/dist/core/DialogueText.js.map +1 -1
  48. package/dist/core/Motion.d.ts +20 -0
  49. package/dist/core/Motion.d.ts.map +1 -1
  50. package/dist/core/Motion.js +20 -0
  51. package/dist/core/Motion.js.map +1 -1
  52. package/dist/core/Save.d.ts +31 -0
  53. package/dist/core/Save.d.ts.map +1 -1
  54. package/dist/core/Save.js +31 -0
  55. package/dist/core/Save.js.map +1 -1
  56. package/dist/core/Twee.d.ts +34 -0
  57. package/dist/core/Twee.d.ts.map +1 -1
  58. package/dist/core/Twee.js +34 -0
  59. package/dist/core/Twee.js.map +1 -1
  60. package/dist/mw_games.global.js +22 -22
  61. package/dist/mwl/index.d.ts +2 -0
  62. package/dist/mwl/index.d.ts.map +1 -1
  63. package/dist/mwl/index.js +1 -0
  64. package/dist/mwl/index.js.map +1 -1
  65. package/dist/mwl/objects.d.ts +31 -0
  66. package/dist/mwl/objects.d.ts.map +1 -0
  67. package/dist/mwl/objects.js +83 -0
  68. package/dist/mwl/objects.js.map +1 -0
  69. package/dist/mwl/schema.d.ts +1 -1
  70. package/dist/mwl/schema.d.ts.map +1 -1
  71. package/dist/mwl/tables.d.ts +2 -0
  72. package/dist/mwl/tables.d.ts.map +1 -1
  73. package/dist/mwl/tables.js +13 -0
  74. package/dist/mwl/tables.js.map +1 -1
  75. package/dist/physics/XpbdWorld.d.ts +68 -5
  76. package/dist/physics/XpbdWorld.d.ts.map +1 -1
  77. package/dist/physics/XpbdWorld.js +135 -6
  78. package/dist/physics/XpbdWorld.js.map +1 -1
  79. package/dist/testing/index.d.ts +16 -0
  80. package/dist/testing/index.d.ts.map +1 -1
  81. package/dist/testing/index.js +21 -0
  82. package/dist/testing/index.js.map +1 -1
  83. package/dist/three-d/Collision3D.d.ts +46 -2
  84. package/dist/three-d/Collision3D.d.ts.map +1 -1
  85. package/dist/three-d/Collision3D.js +46 -2
  86. package/dist/three-d/Collision3D.js.map +1 -1
  87. package/dist/three-d/Grid.d.ts +12 -1
  88. package/dist/three-d/Grid.d.ts.map +1 -1
  89. package/dist/three-d/Grid.js +12 -1
  90. package/dist/three-d/Grid.js.map +1 -1
  91. package/dist/three-d/Vox.d.ts +13 -1
  92. package/dist/three-d/Vox.d.ts.map +1 -1
  93. package/dist/three-d/Vox.js +13 -1
  94. package/dist/three-d/Vox.js.map +1 -1
  95. package/dist/two-d/Game.d.ts +21 -1
  96. package/dist/two-d/Game.d.ts.map +1 -1
  97. package/dist/two-d/Game.js +73 -6
  98. package/dist/two-d/Game.js.map +1 -1
  99. package/dist/two-d/Scene2D.d.ts +24 -0
  100. package/dist/two-d/Scene2D.d.ts.map +1 -1
  101. package/dist/two-d/Scene2D.js +30 -0
  102. package/dist/two-d/Scene2D.js.map +1 -1
  103. package/dist/two-d/render/SceneTransition.d.ts +272 -0
  104. package/dist/two-d/render/SceneTransition.d.ts.map +1 -0
  105. package/dist/two-d/render/SceneTransition.js +431 -0
  106. package/dist/two-d/render/SceneTransition.js.map +1 -0
  107. package/dist/two-d/render/index.d.ts +2 -0
  108. package/dist/two-d/render/index.d.ts.map +1 -1
  109. package/dist/two-d/render/index.js +1 -0
  110. package/dist/two-d/render/index.js.map +1 -1
  111. package/dist/two-d/ui/Button.d.ts +23 -0
  112. package/dist/two-d/ui/Button.d.ts.map +1 -1
  113. package/dist/two-d/ui/Button.js +37 -0
  114. package/dist/two-d/ui/Button.js.map +1 -1
  115. package/dist/two-d/ui/DropdownView.d.ts +49 -3
  116. package/dist/two-d/ui/DropdownView.d.ts.map +1 -1
  117. package/dist/two-d/ui/DropdownView.js +150 -9
  118. package/dist/two-d/ui/DropdownView.js.map +1 -1
  119. package/dist/two-d/ui/ScrollBox.d.ts +90 -0
  120. package/dist/two-d/ui/ScrollBox.d.ts.map +1 -1
  121. package/dist/two-d/ui/ScrollBox.js +206 -4
  122. package/dist/two-d/ui/ScrollBox.js.map +1 -1
  123. package/dist/two-d/ui/ScrollText.d.ts +122 -0
  124. package/dist/two-d/ui/ScrollText.d.ts.map +1 -0
  125. package/dist/two-d/ui/ScrollText.js +202 -0
  126. package/dist/two-d/ui/ScrollText.js.map +1 -0
  127. package/dist/two-d/ui/Tooltip.d.ts +70 -1
  128. package/dist/two-d/ui/Tooltip.d.ts.map +1 -1
  129. package/dist/two-d/ui/Tooltip.js +115 -3
  130. package/dist/two-d/ui/Tooltip.js.map +1 -1
  131. package/dist/two-d/ui/Window.d.ts +35 -2
  132. package/dist/two-d/ui/Window.d.ts.map +1 -1
  133. package/dist/two-d/ui/Window.js +58 -3
  134. package/dist/two-d/ui/Window.js.map +1 -1
  135. package/dist/two-d/ui/index.d.ts +7 -5
  136. package/dist/two-d/ui/index.d.ts.map +1 -1
  137. package/dist/two-d/ui/index.js +3 -2
  138. package/dist/two-d/ui/index.js.map +1 -1
  139. package/dist/two-d/ui/theme.d.ts +18 -0
  140. package/dist/two-d/ui/theme.d.ts.map +1 -1
  141. package/dist/two-d/ui/theme.js.map +1 -1
  142. package/dist/version.d.ts +1 -1
  143. package/dist/version.js +1 -1
  144. package/package.json +14 -3
  145. package/sbom.cdx.json +462 -243
  146. package/tools/browser-driver.mjs +3 -3
  147. package/tools/browser-smoke.mjs +19 -2
  148. package/tools/build-tablebases.mjs +7 -3
  149. package/tools/compile-openings.mjs +7 -3
  150. package/tools/compile-resources.d.mts +8 -0
  151. package/tools/compile-resources.mjs +62 -14
  152. package/tools/emit-page.d.mts +9 -0
  153. package/tools/emit-page.mjs +57 -4
  154. package/tools/find-chrome.mjs +4 -2
  155. package/tools/init.mjs +11 -3
  156. package/tools/play-uci.mjs +9 -5
  157. package/tools/svg-optimise.d.mts +8 -0
  158. package/tools/svg-optimise.mjs +45 -0
  159. package/tools/vite.mjs +10 -1
  160. package/tools.md +5 -2
@@ -0,0 +1,147 @@
1
+ import { Signal } from '../core/Signal.js';
2
+ import { Channel } from './Channels.js';
3
+ import { toAudioBuffer } from './MidiRender.js';
4
+ /**
5
+ * One music track at a time with the controls a game's jukebox needs: load a decoded track or a
6
+ * rendered MIDI file, play, pause, resume, read the position, seek, and be told when the track
7
+ * ends so the game can pick the next one. It wraps a `Channel`, so the buffer, the loop region
8
+ * and the playhead behave as they do there; what it adds is the paused state and the end of the
9
+ * track. Like `Music`, nothing advances on its own: call `update()` each frame.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { MusicPlayer, renderMidiToBufferAsync } from '@datamoc/mw_games/audio';
14
+ *
15
+ * declare const context: AudioContext;
16
+ * declare const midiBytes: ArrayBuffer;
17
+ * declare function nextTrack(): Promise<ArrayBuffer>;
18
+ *
19
+ * const player = new MusicPlayer(context, { volume: 0.8 });
20
+ * player.load(await renderMidiToBufferAsync(midiBytes));
21
+ * player.play();
22
+ * async function playNext(): Promise<void> {
23
+ * player.load(await renderMidiToBufferAsync(await nextTrack()));
24
+ * player.play();
25
+ * }
26
+ * player.onEnded.add(() => { void playNext(); });
27
+ *
28
+ * // in the scene's update:
29
+ * player.update();
30
+ * ```
31
+ */
32
+ export class MusicPlayer {
33
+ constructor(context, options = {}) {
34
+ /** fires once when a track that was not repeating plays through to its end */
35
+ this.onEnded = new Signal();
36
+ this.buffer = null;
37
+ this.loop = null;
38
+ this.repeat = false;
39
+ this.playing = false;
40
+ this.paused = false;
41
+ this.resumeAt = 0;
42
+ this.context = context;
43
+ this.channel = new Channel(context, options.destination);
44
+ this.volume_ = Math.min(1, Math.max(0, options.volume ?? 1));
45
+ }
46
+ /** stops what plays and makes this track the one to play; a rendered MIDI file is copied into a buffer first */
47
+ load(track) {
48
+ this.stop();
49
+ if ('left' in track) {
50
+ this.buffer = toAudioBuffer(this.context, track);
51
+ this.loop = { start: track.loopStart, end: track.loopEnd };
52
+ }
53
+ else {
54
+ this.buffer = track;
55
+ this.loop = null;
56
+ }
57
+ }
58
+ /** plays the loaded track from the start, or from `pos` */
59
+ play(options = {}) {
60
+ if (!this.buffer)
61
+ return;
62
+ this.repeat = options.repeat ?? false;
63
+ this.paused = false;
64
+ this.playing = true;
65
+ this.resumeAt = options.pos ?? 0;
66
+ this.start();
67
+ }
68
+ pause() {
69
+ if (!this.playing || this.paused)
70
+ return;
71
+ this.resumeAt = this.channel.position();
72
+ this.paused = true;
73
+ this.channel.stop();
74
+ }
75
+ resume() {
76
+ if (!this.paused || !this.buffer)
77
+ return;
78
+ this.paused = false;
79
+ this.start();
80
+ }
81
+ /** pauses a playing track and resumes a paused one */
82
+ togglePause() {
83
+ if (this.paused)
84
+ this.resume();
85
+ else
86
+ this.pause();
87
+ }
88
+ /** stops and forgets the position; `play()` starts over */
89
+ stop() {
90
+ this.channel.stop();
91
+ this.playing = false;
92
+ this.paused = false;
93
+ this.resumeAt = 0;
94
+ }
95
+ get isPlaying() {
96
+ return this.playing && !this.paused;
97
+ }
98
+ get isPaused() {
99
+ return this.paused;
100
+ }
101
+ /** seconds of the loaded track */
102
+ get duration() {
103
+ return this.buffer?.duration ?? 0;
104
+ }
105
+ /** seconds into the track, held while paused */
106
+ position() {
107
+ if (!this.playing)
108
+ return 0;
109
+ return this.paused ? this.resumeAt : this.channel.position();
110
+ }
111
+ /** moves the playhead, keeping a paused track paused */
112
+ seek(pos) {
113
+ if (!this.buffer)
114
+ return;
115
+ const target = Math.min(Math.max(0, pos), this.buffer.duration);
116
+ if (this.paused)
117
+ this.resumeAt = target;
118
+ else if (this.playing)
119
+ this.play({ pos: target, repeat: this.repeat });
120
+ }
121
+ get volume() {
122
+ return this.volume_;
123
+ }
124
+ set volume(level) {
125
+ this.volume_ = Math.min(1, Math.max(0, level));
126
+ this.channel.update({ volume: this.volume_ });
127
+ }
128
+ /** call each frame: fires `onEnded` once when a non-repeating track has finished */
129
+ update() {
130
+ if (!this.playing || this.paused || this.channel.isPlaying)
131
+ return;
132
+ this.playing = false;
133
+ this.resumeAt = 0;
134
+ this.onEnded.dispatch();
135
+ }
136
+ start() {
137
+ if (!this.buffer)
138
+ return;
139
+ this.channel.play(this.buffer, {
140
+ volume: this.volume_,
141
+ pos: this.resumeAt,
142
+ loop: this.loop,
143
+ repeat: this.repeat,
144
+ });
145
+ }
146
+ }
147
+ //# sourceMappingURL=MusicPlayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MusicPlayer.js","sourceRoot":"","sources":["../../src/audio/MusicPlayer.ts"],"names":[],"mappings":"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"}
@@ -21,11 +21,25 @@ export interface SampleDecodingContext {
21
21
  export declare const MIN_SAMPLE_RATE = 400;
22
22
  export declare const MAX_SAMPLE_RATE = 200000;
23
23
  export declare const MAX_SAMPLE_FRAMES: number;
24
+ /**
25
+ * How much a SoundFont 3 may decode to, in frames. The defaults (`MAX_SAMPLE_FRAMES` for one sample, 128
26
+ * million, 256 MB of 16-bit audio, for the whole font) suit a game font; raise them for a very large one
27
+ * you trust, since each frame is two bytes of memory and a damaged font can ask for any number.
28
+ */
29
+ export interface SoundFontLimits {
30
+ /** frames one decoded sample may have */
31
+ sampleFrames?: number;
32
+ /** frames all the decoded samples of one font may have */
33
+ totalFrames?: number;
34
+ }
35
+ /** the whole-font default of {@link SoundFontLimits} */
36
+ export declare const MAX_TOTAL_FRAMES: number;
24
37
  /**
25
38
  * A decoder over WebAudio's `decodeAudioData`, for a browser: each sample decodes in a context
26
39
  * created at the sample's own rate, so the browser does not resample it and the loop points keep
27
40
  * meaning what the font wrote. A rate outside what a context accepts (8 to 96 kHz) decodes at
28
- * the nearest accepted rate and is resampled back. `create` replaces the context factory.
41
+ * the nearest accepted rate and is resampled back. `create` replaces the context factory and `limits`
42
+ * the longest sample it will return.
29
43
  *
30
44
  * @example
31
45
  * ```ts
@@ -37,5 +51,5 @@ export declare const MAX_SAMPLE_FRAMES: number;
37
51
  * console.log(font.presetCount);
38
52
  * ```
39
53
  */
40
- export declare function webAudioSampleDecoder(create?: (rate: number) => SampleDecodingContext): SoundFontSampleDecoder;
54
+ export declare function webAudioSampleDecoder(create?: (rate: number) => SampleDecodingContext, limits?: SoundFontLimits): SoundFontSampleDecoder;
41
55
  //# sourceMappingURL=SampleDecoder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SampleDecoder.d.ts","sourceRoot":"","sources":["../../src/audio/SampleDecoder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEzG,yFAAyF;AACzF,MAAM,WAAW,qBAAqB;IACrC,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC;QAC3C,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAAC;KAC9C,CAAC,CAAC;CACH;AAMD;;;;GAIG;AACH,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,eAAO,MAAM,eAAe,SAAS,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAU,CAAC;AA+BzC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,qBAAsC,GAC9D,sBAAsB,CAcxB"}
1
+ {"version":3,"file":"SampleDecoder.d.ts","sourceRoot":"","sources":["../../src/audio/SampleDecoder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEzG,yFAAyF;AACzF,MAAM,WAAW,qBAAqB;IACrC,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC;QAC3C,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAAC;KAC9C,CAAC,CAAC;CACH;AAMD;;;;GAIG;AACH,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,eAAO,MAAM,eAAe,SAAS,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAU,CAAC;AAEzC;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC/B,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wDAAwD;AACxD,eAAO,MAAM,gBAAgB,QAAU,CAAC;AA+BxC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,qBAAsC,EAChE,MAAM,GAAE,eAAoB,GAC1B,sBAAsB,CAcxB"}
@@ -9,6 +9,8 @@ const MAX_RATE = 96000;
9
9
  export const MIN_SAMPLE_RATE = 400;
10
10
  export const MAX_SAMPLE_RATE = 200000;
11
11
  export const MAX_SAMPLE_FRAMES = 1 << 24;
12
+ /** the whole-font default of {@link SoundFontLimits} */
13
+ export const MAX_TOTAL_FRAMES = 1 << 27;
12
14
  function toInt16(frames) {
13
15
  const out = new Int16Array(frames.length);
14
16
  for (let index = 0; index < frames.length; index++) {
@@ -17,9 +19,9 @@ function toInt16(frames) {
17
19
  return out;
18
20
  }
19
21
  /** linear resampling, only for the rare sample whose rate a context cannot be created at */
20
- function resample(frames, from, to) {
22
+ function resample(frames, from, to, maxFrames) {
21
23
  const length = Math.max(1, Math.round((frames.length * to) / from));
22
- if (length > MAX_SAMPLE_FRAMES)
24
+ if (length > maxFrames)
23
25
  throw new Error('a decoded sample is longer than a SoundFont sample may be');
24
26
  const out = new Float32Array(length);
25
27
  for (let index = 0; index < out.length; index++) {
@@ -40,7 +42,8 @@ function offlineContext(rate) {
40
42
  * A decoder over WebAudio's `decodeAudioData`, for a browser: each sample decodes in a context
41
43
  * created at the sample's own rate, so the browser does not resample it and the loop points keep
42
44
  * meaning what the font wrote. A rate outside what a context accepts (8 to 96 kHz) decodes at
43
- * the nearest accepted rate and is resampled back. `create` replaces the context factory.
45
+ * the nearest accepted rate and is resampled back. `create` replaces the context factory and `limits`
46
+ * the longest sample it will return.
44
47
  *
45
48
  * @example
46
49
  * ```ts
@@ -52,7 +55,8 @@ function offlineContext(rate) {
52
55
  * console.log(font.presetCount);
53
56
  * ```
54
57
  */
55
- export function webAudioSampleDecoder(create = offlineContext) {
58
+ export function webAudioSampleDecoder(create = offlineContext, limits = {}) {
59
+ const maxFrames = limits.sampleFrames ?? MAX_SAMPLE_FRAMES;
56
60
  return async (ogg, rate) => {
57
61
  if (!(rate >= MIN_SAMPLE_RATE && rate <= MAX_SAMPLE_RATE)) {
58
62
  throw new Error(`a sample rate of ${rate} Hz is not one a SoundFont can state`);
@@ -61,10 +65,10 @@ export function webAudioSampleDecoder(create = offlineContext) {
61
65
  //decodeAudioData detaches what it is given, and `ogg` is a view into the caller's font
62
66
  const copy = ogg.slice().buffer;
63
67
  const decoded = await create(contextRate).decodeAudioData(copy);
64
- if (decoded.length > MAX_SAMPLE_FRAMES)
68
+ if (decoded.length > maxFrames)
65
69
  throw new Error('a decoded sample is longer than a SoundFont sample may be');
66
70
  const channel = decoded.getChannelData(0);
67
- return toInt16(decoded.sampleRate === rate ? channel : resample(channel, decoded.sampleRate, rate));
71
+ return toInt16(decoded.sampleRate === rate ? channel : resample(channel, decoded.sampleRate, rate, maxFrames));
68
72
  };
69
73
  }
70
74
  //# sourceMappingURL=SampleDecoder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SampleDecoder.js","sourceRoot":"","sources":["../../src/audio/SampleDecoder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SampleDecoder.js","sourceRoot":"","sources":["../../src/audio/SampleDecoder.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;
@@ -1 +1 @@
1
- {"version":3,"file":"Sound.js","sourceRoot":"","sources":["../../src/audio/Sound.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAc1C;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,KAAK;IAOjB,YAAY,IAAY,EAAE,OAAO,GAAiB,EAAE;QAL5C,SAAI,GAAG,CAAC,CAAC;QAET,eAAU,GAAG,KAAK,CAAC;QAI1B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC;QAC7C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC;QACvB,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAE/C,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QAClC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;QAC3B,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC;QACtB,mFAAmF;QACnF,qFAAqF;QACrF,wFAAwF;QACxF,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAEnD,IAAI,IAAI,CAAC,OAAO;YAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO;QACN,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC/B,KAAK,CAAC,KAAK,EAAE,CAAC;YACd,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC;QACvB,CAAC;IACF,CAAC;IAED;;;OAGG;IACH,OAAO;QACN,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,OAAO,EAAE,CAAC;IAChB,CAAC;IAED,wCAAwC;IACxC,MAAM;QACL,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;IACzB,CAAC;IAED,IAAI,WAAW;QACd,OAAO,IAAI,CAAC,UAAU,CAAC;IACxB,CAAC;CACD"}
1
+ {"version":3,"file":"Sound.js","sourceRoot":"","sources":["../../src/audio/Sound.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAc1C;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,KAAK;IASjB,YAAY,IAAY,EAAE,OAAO,GAAiB,EAAE;QAP5C,SAAI,GAAG,CAAC,CAAC;QAET,eAAU,GAAG,KAAK,CAAC;QAE3B,4IAA4I;QAC5I,UAAK,GAAG,CAAC,CAAC;QAGT,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC;QAC7C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC;QACvB,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAE/C,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QAClC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;QAC3B,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC;QACtB,mFAAmF;QACnF,qFAAqF;QACrF,wFAAwF;QACxF,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAEnD,IAAI,IAAI,CAAC,OAAO;YAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO;QACN,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC/B,KAAK,CAAC,KAAK,EAAE,CAAC;YACd,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC;QACvB,CAAC;IACF,CAAC;IAED;;;OAGG;IACH,OAAO;QACN,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,OAAO,EAAE,CAAC;IAChB,CAAC;IAED,wCAAwC;IACxC,MAAM;QACL,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;IACzB,CAAC;IAED,IAAI,WAAW;QACd,OAAO,IAAI,CAAC,UAAU,CAAC;IACxB,CAAC;CACD"}
@@ -1,5 +1,5 @@
1
1
  import { type MidiFile } from './Midi.ts';
2
- import type { SoundFontSampleDecoder } from './SampleDecoder.ts';
2
+ import type { SoundFontLimits, SoundFontSampleDecoder } from './SampleDecoder.ts';
3
3
  /** one mono sample: raw frames plus its loop region in frames and native rate */
4
4
  export interface SoundFontSample {
5
5
  data: Float32Array;
@@ -40,6 +40,18 @@ export interface SoundFontProgram {
40
40
  bank: number;
41
41
  program: number;
42
42
  }
43
+ /**
44
+ * What plays for a (bank, program) the font does not carry. A bank missing the program falls back to
45
+ * bank 0 and a missing drum kit to the standard kit (never to a melodic preset) whatever the policy; past that, `'piano'` (the
46
+ * default, how most players behave) voices the font's own preset 0:0, and `'synth'` voices nothing, so
47
+ * the renderer's built-in synthesizer plays the instrument instead. A game with a trimmed bank picks
48
+ * `'synth'` to hear the synthesizer's timbre for an instrument it did not ship rather than a piano line.
49
+ */
50
+ export type MissingProgram = 'piano' | 'synth';
51
+ export interface SoundFontOptions {
52
+ /** what plays for a program the font lacks; `'piano'` when omitted */
53
+ missingProgram?: MissingProgram;
54
+ }
43
55
  /**
44
56
  * Reads SoundFont 2 bytes into layered preset voices.
45
57
  *
@@ -55,7 +67,7 @@ export interface SoundFontProgram {
55
67
  * console.log(font.presetCount, rendered.duration);
56
68
  * ```
57
69
  */
58
- export declare function parseSoundFont(data: ArrayBuffer | ArrayBufferView): SoundFont;
70
+ export declare function parseSoundFont(data: ArrayBuffer | ArrayBufferView, options?: SoundFontOptions): SoundFont;
59
71
  /**
60
72
  * Every sampled voice the given MIDI files can request, as (bank, program) pairs:
61
73
  * the channel voice in effect at each scheduled note, with drums mapped to bank 128
@@ -73,6 +85,27 @@ export declare function parseSoundFont(data: ArrayBuffer | ArrayBufferView): Sou
73
85
  * ```
74
86
  */
75
87
  export declare function collectSoundFontUsage(files: readonly MidiFile[]): SoundFontProgram[];
88
+ /**
89
+ * The programs of `usage` the font does not carry, so a game can tell at build time which of the
90
+ * instruments its MIDI files ask for would be missing. A bank without the program counts as carried
91
+ * when bank 0 has it (the fallback every player makes), a drum kit only when the font has that exact
92
+ * kit. What plays for a missing one is the `missingProgram` policy of `parseSoundFont`. Works on a
93
+ * SoundFont 2 or 3.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * import { missingSoundFontPrograms, parseMidi, collectSoundFontUsage } from '@datamoc/mw_games/audio';
98
+ *
99
+ * declare const soundfontBytes: ArrayBuffer;
100
+ * declare const midiBytes: ArrayBuffer;
101
+ *
102
+ * const usage = collectSoundFontUsage([parseMidi(midiBytes)]);
103
+ * for (const { bank, program } of missingSoundFontPrograms(soundfontBytes, usage)) {
104
+ * console.log(`bank ${bank} program ${program} is not in the font`);
105
+ * }
106
+ * ```
107
+ */
108
+ export declare function missingSoundFontPrograms(data: ArrayBuffer | ArrayBufferView, usage: readonly SoundFontProgram[]): SoundFontProgram[];
76
109
  /**
77
110
  * Joins several SoundFont 2 files into one, presets in the order given. When two files
78
111
  * carry the same (bank, program) the first wins and the other's preset, instruments
@@ -98,7 +131,8 @@ export declare function mergeSoundFonts(fonts: readonly (ArrayBuffer | ArrayBuff
98
131
  * the sample chunk, and the loop points a SoundFont 3 states relative to its sample become the
99
132
  * absolute ones SoundFont 2 uses. A SoundFont 2 comes back unchanged and `decode` is never called.
100
133
  * The result feeds everything that takes SoundFont 2 bytes, `renderMidiToBufferAsync` included.
101
- * A loop that ends past the decoded frames is cut to them.
134
+ * A loop that ends past the decoded frames is cut to them. `limits` raises or lowers what a sample and
135
+ * the whole font may decode to (see `SoundFontLimits`).
102
136
  *
103
137
  * @example
104
138
  * ```ts
@@ -110,7 +144,7 @@ export declare function mergeSoundFonts(fonts: readonly (ArrayBuffer | ArrayBuff
110
144
  * console.log(sf2.byteLength);
111
145
  * ```
112
146
  */
113
- export declare function decodeSoundFont(data: ArrayBuffer | ArrayBufferView, decode: SoundFontSampleDecoder): Promise<ArrayBuffer>;
147
+ export declare function decodeSoundFont(data: ArrayBuffer | ArrayBufferView, decode: SoundFontSampleDecoder, limits?: SoundFontLimits): Promise<ArrayBuffer>;
114
148
  /**
115
149
  * Reads a SoundFont 2 or 3 into layered preset voices: `parseSoundFont` for a plain font, and a
116
150
  * SoundFont 3 decoded first through `decode`, `webAudioSampleDecoder()` (the browser) unless one
@@ -128,8 +162,9 @@ export declare function decodeSoundFont(data: ArrayBuffer | ArrayBufferView, dec
128
162
  * console.log(rendered.duration);
129
163
  * ```
130
164
  */
131
- export declare function loadSoundFont(data: ArrayBuffer | ArrayBufferView, options?: {
165
+ export declare function loadSoundFont(data: ArrayBuffer | ArrayBufferView, options?: SoundFontOptions & {
132
166
  decode?: SoundFontSampleDecoder;
167
+ limits?: SoundFontLimits;
133
168
  }): Promise<SoundFont>;
134
169
  /**
135
170
  * Writes a smaller, valid SoundFont 2 file carrying only the used programs: their
@@ -1 +1 @@
1
- {"version":3,"file":"SoundFont.d.ts","sourceRoot":"","sources":["../../src/audio/SoundFont.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAwCjE,iFAAiF;AACjF,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACzB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,EAAE,CAAC;IACvF,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAClD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AAuND;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,GAAG,SAAS,CAuG7E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,gBAAgB,EAAE,CAcpF;AA6CD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,CAAC,WAAW,GAAG,eAAe,CAAC,EAAE,GAAG,WAAW,CA+F9F;AAkBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,eAAe,CACpC,IAAI,EAAE,WAAW,GAAG,eAAe,EACnC,MAAM,EAAE,sBAAsB,GAC5B,OAAO,CAAC,WAAW,CAAC,CAyDtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,CAClC,IAAI,EAAE,WAAW,GAAG,eAAe,EACnC,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,sBAAsB,CAAA;CAAO,GAC/C,OAAO,CAAC,SAAS,CAAC,CAEpB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,EAAE,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,WAAW,CAmBpH"}
1
+ {"version":3,"file":"SoundFont.d.ts","sourceRoot":"","sources":["../../src/audio/SoundFont.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { scheduleMidi } from './Midi.js';
2
- import { MAX_SAMPLE_FRAMES, MAX_SAMPLE_RATE, MIN_SAMPLE_RATE, webAudioSampleDecoder } from './SampleDecoder.js';
2
+ import { MAX_SAMPLE_FRAMES, MAX_SAMPLE_RATE, MAX_TOTAL_FRAMES, MIN_SAMPLE_RATE, webAudioSampleDecoder, } from './SampleDecoder.js';
3
3
  /**
4
4
  * A SoundFont 2 reader: presets to instruments to sample zones, enough to play
5
5
  * General MIDI music with real samples. Supported: key and velocity ranges, root key
@@ -174,11 +174,10 @@ function zonesOf(headers, bagList, gens, index, terminal) {
174
174
  function generatorOf(zone, oper) {
175
175
  return zone.gens.get(oper) ?? zone.global?.get(oper) ?? null;
176
176
  }
177
- function presetIndexOf(presets, bank, program) {
177
+ function presetIndexOf(presets, bank, program, missing = 'piano') {
178
178
  return (presets.get(`${bank}:${program}`) ??
179
- presets.get(`0:${program}`) ??
180
- (bank === 128 ? presets.get('128:0') : undefined) ??
181
- presets.get('0:0'));
179
+ (bank === 128 ? presets.get('128:0') : presets.get(`0:${program}`)) ??
180
+ (missing === 'piano' ? presets.get('0:0') : undefined));
182
181
  }
183
182
  /**
184
183
  * Reads SoundFont 2 bytes into layered preset voices.
@@ -195,7 +194,7 @@ function presetIndexOf(presets, bank, program) {
195
194
  * console.log(font.presetCount, rendered.duration);
196
195
  * ```
197
196
  */
198
- export function parseSoundFont(data) {
197
+ export function parseSoundFont(data, options = {}) {
199
198
  const tables = readTables(toBytes(data));
200
199
  if (tables.compressed) {
201
200
  throw new Error('SoundFont 3 (Ogg Vorbis samples) needs decoding first: use loadSoundFont() or decodeSoundFont(), or convert it with "mwg-soundfonts convert <in.sf3> <out.sf2>"');
@@ -233,7 +232,7 @@ export function parseSoundFont(data) {
233
232
  }
234
233
  //Zones that sound for (bank, program, key, velocity), each with the resolved playback recipe.
235
234
  function voices(bank, program, key, velocity) {
236
- const presetIndex = presetIndexOf(presets, bank, program);
235
+ const presetIndex = presetIndexOf(presets, bank, program, options.missingProgram);
237
236
  if (presetIndex === undefined)
238
237
  return [];
239
238
  const out = [];
@@ -314,6 +313,43 @@ export function collectSoundFontUsage(files) {
314
313
  }
315
314
  return out;
316
315
  }
316
+ /**
317
+ * The programs of `usage` the font does not carry, so a game can tell at build time which of the
318
+ * instruments its MIDI files ask for would be missing. A bank without the program counts as carried
319
+ * when bank 0 has it (the fallback every player makes), a drum kit only when the font has that exact
320
+ * kit. What plays for a missing one is the `missingProgram` policy of `parseSoundFont`. Works on a
321
+ * SoundFont 2 or 3.
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * import { missingSoundFontPrograms, parseMidi, collectSoundFontUsage } from '@datamoc/mw_games/audio';
326
+ *
327
+ * declare const soundfontBytes: ArrayBuffer;
328
+ * declare const midiBytes: ArrayBuffer;
329
+ *
330
+ * const usage = collectSoundFontUsage([parseMidi(midiBytes)]);
331
+ * for (const { bank, program } of missingSoundFontPrograms(soundfontBytes, usage)) {
332
+ * console.log(`bank ${bank} program ${program} is not in the font`);
333
+ * }
334
+ * ```
335
+ */
336
+ export function missingSoundFontPrograms(data, usage) {
337
+ const carried = new Set(readTables(toBytes(data))
338
+ .presets.slice(0, -1)
339
+ .map((preset) => `${preset.bank}:${preset.preset}`));
340
+ const seen = new Set();
341
+ const missing = [];
342
+ for (const { bank, program } of usage) {
343
+ const key = `${bank}:${program}`;
344
+ if (seen.has(key))
345
+ continue;
346
+ seen.add(key);
347
+ if (carried.has(key) || (bank !== 128 && carried.has(`0:${program}`)))
348
+ continue;
349
+ missing.push({ bank, program });
350
+ }
351
+ return missing;
352
+ }
317
353
  function u16le(value) {
318
354
  return [value & 0xff, (value >> 8) & 0xff];
319
355
  }
@@ -466,15 +502,14 @@ function distinctPresets(tables) {
466
502
  }
467
503
  return out;
468
504
  }
469
- /** decoded frames one font may hold in all: 256 MB of 16-bit audio */
470
- const MAX_DECODED_FRAMES = 1 << 27;
471
505
  /**
472
506
  * Decodes a SoundFont 3 into a plain SoundFont 2: every Ogg Vorbis sample a preset reaches goes
473
507
  * through `decode` (one call per sample, mono 16-bit frames at the rate the header states) into
474
508
  * the sample chunk, and the loop points a SoundFont 3 states relative to its sample become the
475
509
  * absolute ones SoundFont 2 uses. A SoundFont 2 comes back unchanged and `decode` is never called.
476
510
  * The result feeds everything that takes SoundFont 2 bytes, `renderMidiToBufferAsync` included.
477
- * A loop that ends past the decoded frames is cut to them.
511
+ * A loop that ends past the decoded frames is cut to them. `limits` raises or lowers what a sample and
512
+ * the whole font may decode to (see `SoundFontLimits`).
478
513
  *
479
514
  * @example
480
515
  * ```ts
@@ -486,7 +521,9 @@ const MAX_DECODED_FRAMES = 1 << 27;
486
521
  * console.log(sf2.byteLength);
487
522
  * ```
488
523
  */
489
- export async function decodeSoundFont(data, decode) {
524
+ export async function decodeSoundFont(data, decode, limits = {}) {
525
+ const maxSample = limits.sampleFrames ?? MAX_SAMPLE_FRAMES;
526
+ const maxTotal = limits.totalFrames ?? MAX_TOTAL_FRAMES;
490
527
  const bytes = toBytes(data);
491
528
  const tables = readTables(bytes);
492
529
  if (!tables.compressed)
@@ -510,8 +547,8 @@ export async function decodeSoundFont(data, decode) {
510
547
  }
511
548
  const pcm = await decode(reachable.smplBytes.subarray(sample.start, sample.end), sample.rate);
512
549
  //what a decoder returns is bounded too: a few bytes of Ogg can name a very long sound
513
- if (pcm.length > MAX_SAMPLE_FRAMES || total + pcm.length > MAX_DECODED_FRAMES) {
514
- throw new Error('the decoded sample data is larger than a SoundFont may hold: subset the font before shipping it');
550
+ if (pcm.length > maxSample || total + pcm.length > maxTotal) {
551
+ throw new Error('the decoded sample data is larger than the limits allow: subset the font, or raise `limits`');
515
552
  }
516
553
  at = { start: total, length: pcm.length };
517
554
  placed.set(key, at);
@@ -560,7 +597,8 @@ export async function decodeSoundFont(data, decode) {
560
597
  * ```
561
598
  */
562
599
  export async function loadSoundFont(data, options = {}) {
563
- return parseSoundFont(await decodeSoundFont(data, options.decode ?? webAudioSampleDecoder()));
600
+ const decode = options.decode ?? webAudioSampleDecoder(undefined, options.limits);
601
+ return parseSoundFont(await decodeSoundFont(data, decode, options.limits), options);
564
602
  }
565
603
  /**
566
604
  * Writes a smaller, valid SoundFont 2 file carrying only the used programs: their