@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.
- package/CHANGELOG.md +105 -0
- package/README.md +62 -1
- package/REFERENCE.md +73 -8
- package/api-surface.json +696 -0
- package/data/microgm-essential.PROVENANCE.md +13 -0
- package/data/microgm-essential.sf2 +0 -0
- package/data/soundfont-catalog.json +8 -0
- package/dist/audio/AudioLoader.d.ts +28 -0
- package/dist/audio/AudioLoader.d.ts.map +1 -0
- package/dist/audio/AudioLoader.js +53 -0
- package/dist/audio/AudioLoader.js.map +1 -0
- package/dist/audio/Channels.d.ts +1 -0
- package/dist/audio/Channels.d.ts.map +1 -1
- package/dist/audio/Channels.js +2 -1
- package/dist/audio/Channels.js.map +1 -1
- package/dist/audio/MidiRender.d.ts +19 -1
- package/dist/audio/MidiRender.d.ts.map +1 -1
- package/dist/audio/MidiRender.js +24 -1
- package/dist/audio/MidiRender.js.map +1 -1
- 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/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/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 +40 -5
- package/dist/audio/SoundFont.d.ts.map +1 -1
- package/dist/audio/SoundFont.js +52 -14
- package/dist/audio/SoundFont.js.map +1 -1
- 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/index.d.ts +8 -4
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +4 -2
- package/dist/audio/index.js.map +1 -1
- package/dist/core/DialogueText.d.ts +25 -0
- package/dist/core/DialogueText.d.ts.map +1 -1
- package/dist/core/DialogueText.js +25 -0
- package/dist/core/DialogueText.js.map +1 -1
- package/dist/core/Motion.d.ts +20 -0
- package/dist/core/Motion.d.ts.map +1 -1
- package/dist/core/Motion.js +20 -0
- package/dist/core/Motion.js.map +1 -1
- package/dist/core/Save.d.ts +31 -0
- package/dist/core/Save.d.ts.map +1 -1
- package/dist/core/Save.js +31 -0
- package/dist/core/Save.js.map +1 -1
- package/dist/core/Twee.d.ts +34 -0
- package/dist/core/Twee.d.ts.map +1 -1
- package/dist/core/Twee.js +34 -0
- package/dist/core/Twee.js.map +1 -1
- package/dist/mw_games.global.js +22 -22
- package/dist/mwl/index.d.ts +2 -0
- package/dist/mwl/index.d.ts.map +1 -1
- package/dist/mwl/index.js +1 -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/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/XpbdWorld.d.ts +68 -5
- package/dist/physics/XpbdWorld.d.ts.map +1 -1
- package/dist/physics/XpbdWorld.js +135 -6
- package/dist/physics/XpbdWorld.js.map +1 -1
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +21 -0
- package/dist/testing/index.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 +21 -1
- package/dist/two-d/Game.d.ts.map +1 -1
- package/dist/two-d/Game.js +73 -6
- 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/SceneTransition.d.ts +272 -0
- package/dist/two-d/render/SceneTransition.d.ts.map +1 -0
- package/dist/two-d/render/SceneTransition.js +431 -0
- package/dist/two-d/render/SceneTransition.js.map +1 -0
- 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/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/DropdownView.d.ts +49 -3
- package/dist/two-d/ui/DropdownView.d.ts.map +1 -1
- package/dist/two-d/ui/DropdownView.js +150 -9
- package/dist/two-d/ui/DropdownView.js.map +1 -1
- package/dist/two-d/ui/ScrollBox.d.ts +90 -0
- package/dist/two-d/ui/ScrollBox.d.ts.map +1 -1
- package/dist/two-d/ui/ScrollBox.js +206 -4
- package/dist/two-d/ui/ScrollBox.js.map +1 -1
- package/dist/two-d/ui/ScrollText.d.ts +122 -0
- package/dist/two-d/ui/ScrollText.d.ts.map +1 -0
- package/dist/two-d/ui/ScrollText.js +202 -0
- package/dist/two-d/ui/ScrollText.js.map +1 -0
- 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/two-d/ui/theme.d.ts +18 -0
- package/dist/two-d/ui/theme.d.ts.map +1 -1
- package/dist/two-d/ui/theme.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 +462 -243
- 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 +8 -0
- package/tools/compile-resources.mjs +62 -14
- package/tools/emit-page.d.mts +9 -0
- package/tools/emit-page.mjs +57 -4
- package/tools/find-chrome.mjs +4 -2
- package/tools/init.mjs +11 -3
- package/tools/play-uci.mjs +9 -5
- package/tools/svg-optimise.d.mts +8 -0
- package/tools/svg-optimise.mjs +45 -0
- package/tools/vite.mjs +10 -1
- 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+
|
|
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 >
|
|
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 >
|
|
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":"AAiBA,4EAA4E;AAC5E,MAAM,QAAQ,GAAG,IAAI,CAAC;AACtB,MAAM,QAAQ,GAAG,KAAK,CAAC;AAEvB;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AACnC,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"SampleDecoder.js","sourceRoot":"","sources":["../../src/audio/SampleDecoder.ts"],"names":[],"mappings":"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"}
|
package/dist/audio/Sound.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/audio/Sound.js
CHANGED
|
@@ -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;
|
package/dist/audio/Sound.js.map
CHANGED
|
@@ -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;
|
|
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;
|
|
1
|
+
{"version":3,"file":"SoundFont.d.ts","sourceRoot":"","sources":["../../src/audio/SoundFont.ts"],"names":[],"mappings":"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"}
|
package/dist/audio/SoundFont.js
CHANGED
|
@@ -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
|
-
(
|
|
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 >
|
|
514
|
-
throw new Error('the decoded sample data is larger than
|
|
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
|
-
|
|
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
|