@datamoc/mw_games 0.43.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 +67 -0
- package/README.md +22 -0
- package/REFERENCE.md +82 -6
- package/api-surface.json +1028 -2
- 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 +7 -2
- package/dist/audio/AudioLoader.d.ts.map +1 -1
- package/dist/audio/AudioLoader.js +23 -18
- package/dist/audio/AudioLoader.js.map +1 -1
- 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 +65 -1
- package/dist/audio/Channels.d.ts.map +1 -1
- package/dist/audio/Channels.js +117 -30
- 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 +31 -0
- package/dist/audio/MidiRender.d.ts.map +1 -1
- package/dist/audio/MidiRender.js +79 -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/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/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/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/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 +38 -6
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +19 -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 +11 -0
- package/dist/mwl/index.d.ts.map +1 -1
- package/dist/mwl/index.js +6 -0
- package/dist/mwl/index.js.map +1 -1
- 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/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.map +1 -1
- package/dist/physics/XpbdWorld.js +2 -1
- 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 +2 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -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/two-d/Game.d.ts +13 -2
- package/dist/two-d/Game.d.ts.map +1 -1
- package/dist/two-d/Game.js +42 -18
- package/dist/two-d/Game.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 +29 -0
- package/dist/two-d/render/SceneTransition.d.ts.map +1 -1
- package/dist/two-d/render/SceneTransition.js +27 -0
- package/dist/two-d/render/SceneTransition.js.map +1 -1
- 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 -2
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +1 -1
- 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/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 +15 -1
- package/dist/two-d/ui/DropdownView.d.ts.map +1 -1
- package/dist/two-d/ui/DropdownView.js +21 -3
- 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 +6 -0
- package/dist/two-d/ui/ScrollBox.d.ts.map +1 -1
- package/dist/two-d/ui/ScrollBox.js +12 -1
- package/dist/two-d/ui/ScrollBox.js.map +1 -1
- package/dist/two-d/ui/ScrollText.d.ts +11 -2
- package/dist/two-d/ui/ScrollText.d.ts.map +1 -1
- package/dist/two-d/ui/ScrollText.js +18 -2
- package/dist/two-d/ui/ScrollText.js.map +1 -1
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -1
- package/sbom.cdx.json +6 -6
- package/tools/audio.d.mts +75 -0
- package/tools/audio.mjs +437 -0
- package/tools/mwl.mjs +144 -2
- package/tools.md +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sound effect described as data and voiced by a synthesis backend instead of read from a
|
|
3
|
+
* file. mwg owns the shape the game and the mixer need (an id, a priority, what a second
|
|
4
|
+
* trigger does, a caption); whatever else a backend wants (chip, voices, curves) rides along
|
|
5
|
+
* untouched, so a retro-instrument package can carry its own `SfxDefinition` through unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export interface SfxDefinition {
|
|
8
|
+
id: string;
|
|
9
|
+
/** higher cuts lower when the mixer is full; 0 when omitted */
|
|
10
|
+
priority?: number;
|
|
11
|
+
/** what a second trigger does while the first still sounds: `'restart'` (the default), `'ignore'`, or `'layer'` */
|
|
12
|
+
retrigger?: 'restart' | 'ignore' | 'layer';
|
|
13
|
+
/** shown by a captioning overlay each time the effect plays */
|
|
14
|
+
caption?: string;
|
|
15
|
+
[backendData: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** what a play carries into the backend: the mix mwg computed and a seed that makes any randomness reproducible */
|
|
18
|
+
export interface SfxPlayOptions {
|
|
19
|
+
/** 0 to 1, after distance and the sound's own volume */
|
|
20
|
+
gain: number;
|
|
21
|
+
/** -1 (left) to 1 (right) */
|
|
22
|
+
pan: number;
|
|
23
|
+
/** playback ratio, 1 unchanged */
|
|
24
|
+
pitch: number;
|
|
25
|
+
/** the only source of chance an effect may use */
|
|
26
|
+
seed: number;
|
|
27
|
+
}
|
|
28
|
+
/** one effect sounding on a backend */
|
|
29
|
+
export interface SfxVoice {
|
|
30
|
+
stop(): void;
|
|
31
|
+
/** false once it has finished by itself or been stopped */
|
|
32
|
+
readonly active: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** the synthesis side: a chip engine that can voice an effect, wherever it keeps its voices */
|
|
35
|
+
export interface SfxBackend {
|
|
36
|
+
/** returns null when it cannot voice the effect (no free channel, unknown chip), which is not an error */
|
|
37
|
+
play(definition: SfxDefinition, options: SfxPlayOptions): SfxVoice | null;
|
|
38
|
+
}
|
|
39
|
+
/** effects by id, so a sound can be made from an id the content named */
|
|
40
|
+
export declare class SfxBank {
|
|
41
|
+
private readonly definitions;
|
|
42
|
+
constructor(definitions?: Iterable<SfxDefinition>);
|
|
43
|
+
define(definition: SfxDefinition): void;
|
|
44
|
+
has(id: string): boolean;
|
|
45
|
+
get(id: string): SfxDefinition;
|
|
46
|
+
ids(): string[];
|
|
47
|
+
}
|
|
48
|
+
/** a granted slot in the arbiter; `release` frees it */
|
|
49
|
+
export interface SfxGrant {
|
|
50
|
+
release(): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The shared voice limit between chip effects and the bus's `'se'` jingles: both ask here before
|
|
54
|
+
* they sound, and when the limit is reached the lowest-priority voice (the oldest among equals)
|
|
55
|
+
* is cut for a new one of equal or higher priority; a new one below everything sounding is
|
|
56
|
+
* refused. One arbiter handed to a bus and to every `SfxSound` makes them a single pool.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { SfxArbiter } from '@datamoc/mw_games/audio';
|
|
61
|
+
*
|
|
62
|
+
* const arbiter = new SfxArbiter({ maxVoices: 2 });
|
|
63
|
+
* const a = arbiter.request(1, () => console.log('a cut'));
|
|
64
|
+
* arbiter.request(1, () => console.log('b cut'));
|
|
65
|
+
* arbiter.request(5, () => console.log('c cut')); // cuts a, the oldest of the lowest priority
|
|
66
|
+
* a?.release();
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare class SfxArbiter {
|
|
70
|
+
private readonly maxVoices;
|
|
71
|
+
private readonly voices;
|
|
72
|
+
constructor(options?: {
|
|
73
|
+
maxVoices?: number;
|
|
74
|
+
});
|
|
75
|
+
get active(): number;
|
|
76
|
+
/**
|
|
77
|
+
* Asks for a slot. `stop` cuts the voice when a better one needs its place; `active` says
|
|
78
|
+
* whether it still sounds, so a voice that ended by itself frees its slot for any asker, not
|
|
79
|
+
* only for the sound that owned it.
|
|
80
|
+
*/
|
|
81
|
+
request(priority: number, stop: () => void, active?: () => boolean): SfxGrant | null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A reproducible seed for an effect: the same id on the same game frame (and the same ordinal
|
|
85
|
+
* within that frame) always gives the same number, so a replay or a lockstep peer hears exactly
|
|
86
|
+
* what the original run heard, and audio never perturbs the simulation.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { sfxSeed } from '@datamoc/mw_games/audio';
|
|
91
|
+
*
|
|
92
|
+
* sfxSeed('arrow', 120) === sfxSeed('arrow', 120); // true
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare function sfxSeed(id: string, frame: number, ordinal?: number): number;
|
|
96
|
+
export interface SfxSoundOptions {
|
|
97
|
+
/** where an effect given by id is looked up */
|
|
98
|
+
bank?: SfxBank;
|
|
99
|
+
/** the shared voice limit; none means a voice is never cut for another */
|
|
100
|
+
arbiter?: SfxArbiter;
|
|
101
|
+
/** the game's own frame counter, never the wall clock, so seeds repeat across runs; 0 when omitted */
|
|
102
|
+
frame?: () => number;
|
|
103
|
+
/** shown with each play when the definition has no caption of its own */
|
|
104
|
+
caption?: string;
|
|
105
|
+
/** level, 0 to 1; 1 when omitted */
|
|
106
|
+
volume?: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The data-driven counterpart of `Sound`: it plays an `SfxDefinition` (or the id of one in a
|
|
110
|
+
* bank) through a backend, with the same `play(gain, pitch)` shape so a `SoundSource` can
|
|
111
|
+
* attenuate it, and a pan on top. It applies the definition's retrigger rule, asks the shared
|
|
112
|
+
* arbiter for a voice, fires the caption, and hands the backend a deterministic seed.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { SfxArbiter, SfxBank, SfxSound, SoundSource, AudioListener, type SfxBackend } from '@datamoc/mw_games/audio';
|
|
117
|
+
*
|
|
118
|
+
* declare const chip: SfxBackend;
|
|
119
|
+
*
|
|
120
|
+
* const bank = new SfxBank([{ id: 'arrow', priority: 5, caption: 'an arrow flies' }]);
|
|
121
|
+
* const arrow = new SfxSound(chip, 'arrow', { bank, arbiter: new SfxArbiter(), frame: () => 0 });
|
|
122
|
+
* new SoundSource(arrow, { x: 3, y: 4 }).playFor(new AudioListener());
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export declare class SfxSound {
|
|
126
|
+
volume: number;
|
|
127
|
+
/** how many plays reached the backend, for a test to assert on without a speaker */
|
|
128
|
+
plays: number;
|
|
129
|
+
private readonly backend;
|
|
130
|
+
private readonly definition;
|
|
131
|
+
private readonly options;
|
|
132
|
+
private readonly live;
|
|
133
|
+
private suspended_;
|
|
134
|
+
private lastFrame;
|
|
135
|
+
private ordinal;
|
|
136
|
+
constructor(backend: SfxBackend, effect: SfxDefinition | string, options?: SfxSoundOptions);
|
|
137
|
+
get isSuspended(): boolean;
|
|
138
|
+
/** voices that still sound */
|
|
139
|
+
get activeVoices(): number;
|
|
140
|
+
/** plays the effect; returns false when it was ignored, refused by the arbiter or the backend had no voice */
|
|
141
|
+
play(gain?: number, pitch?: number, pan?: number): boolean;
|
|
142
|
+
stopAll(): void;
|
|
143
|
+
suspend(): void;
|
|
144
|
+
resume(): void;
|
|
145
|
+
/** drops voices that finished by themselves, returning their arbiter slots */
|
|
146
|
+
private prune;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=Sfx.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Sfx.d.ts","sourceRoot":"","sources":["../../src/audio/Sfx.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { onCaption } from './Captions.js';
|
|
2
|
+
/** effects by id, so a sound can be made from an id the content named */
|
|
3
|
+
export class SfxBank {
|
|
4
|
+
constructor(definitions = []) {
|
|
5
|
+
this.definitions = new Map();
|
|
6
|
+
for (const definition of definitions)
|
|
7
|
+
this.define(definition);
|
|
8
|
+
}
|
|
9
|
+
define(definition) {
|
|
10
|
+
if (!definition.id)
|
|
11
|
+
throw new Error('a sound effect needs an id');
|
|
12
|
+
this.definitions.set(definition.id, definition);
|
|
13
|
+
}
|
|
14
|
+
has(id) {
|
|
15
|
+
return this.definitions.has(id);
|
|
16
|
+
}
|
|
17
|
+
get(id) {
|
|
18
|
+
const definition = this.definitions.get(id);
|
|
19
|
+
if (!definition)
|
|
20
|
+
throw new Error(`no sound effect "${id}" in the bank`);
|
|
21
|
+
return definition;
|
|
22
|
+
}
|
|
23
|
+
ids() {
|
|
24
|
+
return [...this.definitions.keys()];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The shared voice limit between chip effects and the bus's `'se'` jingles: both ask here before
|
|
29
|
+
* they sound, and when the limit is reached the lowest-priority voice (the oldest among equals)
|
|
30
|
+
* is cut for a new one of equal or higher priority; a new one below everything sounding is
|
|
31
|
+
* refused. One arbiter handed to a bus and to every `SfxSound` makes them a single pool.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { SfxArbiter } from '@datamoc/mw_games/audio';
|
|
36
|
+
*
|
|
37
|
+
* const arbiter = new SfxArbiter({ maxVoices: 2 });
|
|
38
|
+
* const a = arbiter.request(1, () => console.log('a cut'));
|
|
39
|
+
* arbiter.request(1, () => console.log('b cut'));
|
|
40
|
+
* arbiter.request(5, () => console.log('c cut')); // cuts a, the oldest of the lowest priority
|
|
41
|
+
* a?.release();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export class SfxArbiter {
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
this.voices = [];
|
|
47
|
+
this.maxVoices = Math.max(1, Math.floor(options.maxVoices ?? 8));
|
|
48
|
+
}
|
|
49
|
+
get active() {
|
|
50
|
+
return this.voices.length;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Asks for a slot. `stop` cuts the voice when a better one needs its place; `active` says
|
|
54
|
+
* whether it still sounds, so a voice that ended by itself frees its slot for any asker, not
|
|
55
|
+
* only for the sound that owned it.
|
|
56
|
+
*/
|
|
57
|
+
request(priority, stop, active = () => true) {
|
|
58
|
+
for (let i = this.voices.length - 1; i >= 0; i--)
|
|
59
|
+
if (!this.voices[i].active())
|
|
60
|
+
this.voices.splice(i, 1);
|
|
61
|
+
if (this.voices.length >= this.maxVoices) {
|
|
62
|
+
let victim = 0;
|
|
63
|
+
for (let i = 1; i < this.voices.length; i++) {
|
|
64
|
+
if (this.voices[i].priority < this.voices[victim].priority)
|
|
65
|
+
victim = i;
|
|
66
|
+
}
|
|
67
|
+
if (this.voices[victim].priority > priority)
|
|
68
|
+
return null;
|
|
69
|
+
const [cut] = this.voices.splice(victim, 1);
|
|
70
|
+
cut.stop();
|
|
71
|
+
}
|
|
72
|
+
const entry = { priority, stop, active };
|
|
73
|
+
this.voices.push(entry);
|
|
74
|
+
return {
|
|
75
|
+
release: () => {
|
|
76
|
+
const at = this.voices.indexOf(entry);
|
|
77
|
+
if (at !== -1)
|
|
78
|
+
this.voices.splice(at, 1);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A reproducible seed for an effect: the same id on the same game frame (and the same ordinal
|
|
85
|
+
* within that frame) always gives the same number, so a replay or a lockstep peer hears exactly
|
|
86
|
+
* what the original run heard, and audio never perturbs the simulation.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { sfxSeed } from '@datamoc/mw_games/audio';
|
|
91
|
+
*
|
|
92
|
+
* sfxSeed('arrow', 120) === sfxSeed('arrow', 120); // true
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function sfxSeed(id, frame, ordinal = 0) {
|
|
96
|
+
let hash = 0x811c9dc5;
|
|
97
|
+
const mix = (value) => {
|
|
98
|
+
hash = Math.imul(hash ^ (value & 0xff), 0x01000193);
|
|
99
|
+
hash = Math.imul(hash ^ ((value >>> 8) & 0xff), 0x01000193);
|
|
100
|
+
hash = Math.imul(hash ^ ((value >>> 16) & 0xff), 0x01000193);
|
|
101
|
+
hash = Math.imul(hash ^ ((value >>> 24) & 0xff), 0x01000193);
|
|
102
|
+
};
|
|
103
|
+
for (let i = 0; i < id.length; i++)
|
|
104
|
+
mix(id.charCodeAt(i));
|
|
105
|
+
mix(Math.floor(frame));
|
|
106
|
+
mix(ordinal);
|
|
107
|
+
return hash >>> 0;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The data-driven counterpart of `Sound`: it plays an `SfxDefinition` (or the id of one in a
|
|
111
|
+
* bank) through a backend, with the same `play(gain, pitch)` shape so a `SoundSource` can
|
|
112
|
+
* attenuate it, and a pan on top. It applies the definition's retrigger rule, asks the shared
|
|
113
|
+
* arbiter for a voice, fires the caption, and hands the backend a deterministic seed.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* import { SfxArbiter, SfxBank, SfxSound, SoundSource, AudioListener, type SfxBackend } from '@datamoc/mw_games/audio';
|
|
118
|
+
*
|
|
119
|
+
* declare const chip: SfxBackend;
|
|
120
|
+
*
|
|
121
|
+
* const bank = new SfxBank([{ id: 'arrow', priority: 5, caption: 'an arrow flies' }]);
|
|
122
|
+
* const arrow = new SfxSound(chip, 'arrow', { bank, arbiter: new SfxArbiter(), frame: () => 0 });
|
|
123
|
+
* new SoundSource(arrow, { x: 3, y: 4 }).playFor(new AudioListener());
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export class SfxSound {
|
|
127
|
+
constructor(backend, effect, options = {}) {
|
|
128
|
+
/** how many plays reached the backend, for a test to assert on without a speaker */
|
|
129
|
+
this.plays = 0;
|
|
130
|
+
this.live = [];
|
|
131
|
+
this.suspended_ = false;
|
|
132
|
+
this.lastFrame = Number.NaN;
|
|
133
|
+
this.ordinal = 0;
|
|
134
|
+
this.backend = backend;
|
|
135
|
+
this.options = options;
|
|
136
|
+
this.volume = options.volume ?? 1;
|
|
137
|
+
if (typeof effect === 'string') {
|
|
138
|
+
if (!options.bank)
|
|
139
|
+
throw new Error(`a sound effect given by id ("${effect}") needs a bank`);
|
|
140
|
+
this.definition = options.bank.get(effect);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.definition = effect;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
get isSuspended() {
|
|
147
|
+
return this.suspended_;
|
|
148
|
+
}
|
|
149
|
+
/** voices that still sound */
|
|
150
|
+
get activeVoices() {
|
|
151
|
+
this.prune();
|
|
152
|
+
return this.live.length;
|
|
153
|
+
}
|
|
154
|
+
/** plays the effect; returns false when it was ignored, refused by the arbiter or the backend had no voice */
|
|
155
|
+
play(gain = 1, pitch = 1, pan = 0) {
|
|
156
|
+
if (this.suspended_)
|
|
157
|
+
return false;
|
|
158
|
+
this.prune();
|
|
159
|
+
const rule = this.definition.retrigger ?? 'restart';
|
|
160
|
+
if (this.live.length > 0 && rule === 'ignore')
|
|
161
|
+
return false;
|
|
162
|
+
if (this.live.length > 0 && rule === 'restart')
|
|
163
|
+
this.stopAll();
|
|
164
|
+
const frame = this.options.frame?.() ?? 0;
|
|
165
|
+
this.ordinal = frame === this.lastFrame ? this.ordinal + 1 : 0;
|
|
166
|
+
this.lastFrame = frame;
|
|
167
|
+
let voice = null;
|
|
168
|
+
const grant = this.options.arbiter?.request(this.definition.priority ?? 0, () => {
|
|
169
|
+
voice?.stop();
|
|
170
|
+
}, () => voice?.active ?? true) ?? null;
|
|
171
|
+
if (this.options.arbiter && !grant)
|
|
172
|
+
return false;
|
|
173
|
+
voice = this.backend.play(this.definition, {
|
|
174
|
+
gain: Math.max(0, Math.min(1, gain * this.volume)),
|
|
175
|
+
pan: Math.max(-1, Math.min(1, pan)),
|
|
176
|
+
pitch,
|
|
177
|
+
seed: sfxSeed(this.definition.id, frame, this.ordinal),
|
|
178
|
+
});
|
|
179
|
+
if (!voice) {
|
|
180
|
+
grant?.release();
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
this.live.push({ voice, grant });
|
|
184
|
+
this.plays++;
|
|
185
|
+
const caption = this.definition.caption ?? this.options.caption;
|
|
186
|
+
if (caption)
|
|
187
|
+
onCaption.dispatch({ text: caption });
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
stopAll() {
|
|
191
|
+
for (const { voice, grant } of this.live.splice(0)) {
|
|
192
|
+
voice.stop();
|
|
193
|
+
grant?.release();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
suspend() {
|
|
197
|
+
this.suspended_ = true;
|
|
198
|
+
this.stopAll();
|
|
199
|
+
}
|
|
200
|
+
resume() {
|
|
201
|
+
this.suspended_ = false;
|
|
202
|
+
}
|
|
203
|
+
/** drops voices that finished by themselves, returning their arbiter slots */
|
|
204
|
+
prune() {
|
|
205
|
+
for (let i = this.live.length - 1; i >= 0; i--) {
|
|
206
|
+
if (!this.live[i].voice.active) {
|
|
207
|
+
this.live[i].grant?.release();
|
|
208
|
+
this.live.splice(i, 1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=Sfx.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Sfx.js","sourceRoot":"","sources":["../../src/audio/Sfx.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type StereoFrames } from './Resample.ts';
|
|
2
|
+
export interface SpectralDistanceOptions {
|
|
3
|
+
/** analysis window in frames, a power of two; 2048 when omitted */
|
|
4
|
+
windowSize?: number;
|
|
5
|
+
/** frames between windows; half the window when omitted */
|
|
6
|
+
hop?: number;
|
|
7
|
+
/** bins quieter than this many dB below the loudest are ignored as noise floor; 80 when omitted */
|
|
8
|
+
floorDb?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface SpectralDistanceResult {
|
|
11
|
+
/** mean absolute difference of the log spectra in decibels: 0 for identical audio, a few dB for the same timbre played slightly differently */
|
|
12
|
+
decibels: number;
|
|
13
|
+
/** windows compared */
|
|
14
|
+
windows: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* How far apart two renders sound, as the mean absolute difference of their log spectra: the
|
|
18
|
+
* number to watch when one synthesis engine is tuned toward another's reference, or when a change
|
|
19
|
+
* must not alter a render. Both are folded to mono and brought to the first one's sample rate; the
|
|
20
|
+
* shorter is padded with silence. Bins more than `floorDb` below the loudest of the pair do not
|
|
21
|
+
* count, so inaudible noise cannot dominate the figure.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { spectralDistance } from '@datamoc/mw_games/audio';
|
|
26
|
+
*
|
|
27
|
+
* declare const reference: { sampleRate: number; left: Float32Array; right: Float32Array };
|
|
28
|
+
* declare const candidate: { sampleRate: number; left: Float32Array; right: Float32Array };
|
|
29
|
+
*
|
|
30
|
+
* console.log(spectralDistance(reference, candidate).decibels);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function spectralDistance(a: StereoFrames, b: StereoFrames, options?: SpectralDistanceOptions): SpectralDistanceResult;
|
|
34
|
+
//# sourceMappingURL=SpectralDistance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpectralDistance.d.ts","sourceRoot":"","sources":["../../src/audio/SpectralDistance.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAElE,MAAM,WAAW,uBAAuB;IACvC,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mGAAmG;IACnG,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACtC,+IAA+I;IAC/I,QAAQ,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;CAChB;AAgBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC/B,CAAC,EAAE,YAAY,EACf,CAAC,EAAE,YAAY,EACf,OAAO,GAAE,uBAA4B,GACnC,sBAAsB,CA0BxB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { fft } from './Fft.js';
|
|
2
|
+
import { resampleStereo } from './Resample.js';
|
|
3
|
+
/** the Hann-windowed magnitude spectrum, in dB, of one mono window starting at `start` (zero padded past the end) */
|
|
4
|
+
function logSpectrum(samples, start, size) {
|
|
5
|
+
const re = new Float64Array(size);
|
|
6
|
+
const im = new Float64Array(size);
|
|
7
|
+
for (let i = 0; i < size; i++) {
|
|
8
|
+
const sample = start + i < samples.length ? samples[start + i] : 0;
|
|
9
|
+
re[i] = sample * (0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (size - 1)));
|
|
10
|
+
}
|
|
11
|
+
fft(re, im);
|
|
12
|
+
const bins = new Float64Array(size / 2);
|
|
13
|
+
for (let k = 0; k < bins.length; k++)
|
|
14
|
+
bins[k] = 20 * Math.log10(Math.hypot(re[k], im[k]) + 1e-12);
|
|
15
|
+
return bins;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* How far apart two renders sound, as the mean absolute difference of their log spectra: the
|
|
19
|
+
* number to watch when one synthesis engine is tuned toward another's reference, or when a change
|
|
20
|
+
* must not alter a render. Both are folded to mono and brought to the first one's sample rate; the
|
|
21
|
+
* shorter is padded with silence. Bins more than `floorDb` below the loudest of the pair do not
|
|
22
|
+
* count, so inaudible noise cannot dominate the figure.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { spectralDistance } from '@datamoc/mw_games/audio';
|
|
27
|
+
*
|
|
28
|
+
* declare const reference: { sampleRate: number; left: Float32Array; right: Float32Array };
|
|
29
|
+
* declare const candidate: { sampleRate: number; left: Float32Array; right: Float32Array };
|
|
30
|
+
*
|
|
31
|
+
* console.log(spectralDistance(reference, candidate).decibels);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function spectralDistance(a, b, options = {}) {
|
|
35
|
+
const size = options.windowSize ?? 2048;
|
|
36
|
+
if (size < 4 || (size & (size - 1)) !== 0)
|
|
37
|
+
throw new Error('window size must be a power of two of at least 4');
|
|
38
|
+
const hop = Math.max(1, Math.floor(options.hop ?? size / 2));
|
|
39
|
+
const floor = options.floorDb ?? 80;
|
|
40
|
+
const matched = resampleStereo(b, a.sampleRate);
|
|
41
|
+
const mono = (frames) => frames.left.map((value, i) => (value + frames.right[i]) / 2);
|
|
42
|
+
const first = mono(a);
|
|
43
|
+
const second = mono(matched);
|
|
44
|
+
const length = Math.max(first.length, second.length);
|
|
45
|
+
let total = 0;
|
|
46
|
+
let counted = 0;
|
|
47
|
+
let windows = 0;
|
|
48
|
+
for (let start = 0; start === 0 || start + size / 2 < length; start += hop) {
|
|
49
|
+
const x = logSpectrum(first, start, size);
|
|
50
|
+
const y = logSpectrum(second, start, size);
|
|
51
|
+
let peak = -Infinity;
|
|
52
|
+
for (let k = 0; k < x.length; k++)
|
|
53
|
+
peak = Math.max(peak, x[k], y[k]);
|
|
54
|
+
for (let k = 0; k < x.length; k++) {
|
|
55
|
+
if (Math.max(x[k], y[k]) < peak - floor)
|
|
56
|
+
continue;
|
|
57
|
+
total += Math.abs(x[k] - y[k]);
|
|
58
|
+
counted++;
|
|
59
|
+
}
|
|
60
|
+
windows++;
|
|
61
|
+
}
|
|
62
|
+
return { decibels: counted === 0 ? 0 : total / counted, windows };
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=SpectralDistance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpectralDistance.js","sourceRoot":"","sources":["../../src/audio/SpectralDistance.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Signal } from '../core/Signal.ts';
|
|
2
|
+
import type { LoopRegion } from './Channels.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Audio that is produced as it is played instead of decoded up front: a tracker, a chip
|
|
5
|
+
* player, a synthesiser. The consumer pulls frames in order; it can also ask for a different
|
|
6
|
+
* position, which a stateful source answers by skipping or rewinding as it can.
|
|
7
|
+
*/
|
|
8
|
+
export interface BlockSource {
|
|
9
|
+
readonly sampleRate: number;
|
|
10
|
+
/** total frames, or undefined for a source with no end (a chip tune that never finishes) */
|
|
11
|
+
readonly length?: number;
|
|
12
|
+
/** writes up to `count` frames at `start` of both buffers and returns how many it wrote; fewer than asked means the source ended */
|
|
13
|
+
read(left: Float32Array, right: Float32Array, start: number, count: number): number;
|
|
14
|
+
/** makes the next `read` return frames from this position */
|
|
15
|
+
seek(frame: number): void;
|
|
16
|
+
}
|
|
17
|
+
export interface StreamChannelOptions {
|
|
18
|
+
/** seconds of audio per scheduled chunk; 0.5 when omitted */
|
|
19
|
+
chunkSeconds?: number;
|
|
20
|
+
/** how far ahead of the clock chunks are queued; 2 seconds when omitted, enough to ride out a throttled background timer */
|
|
21
|
+
lookaheadSeconds?: number;
|
|
22
|
+
/** milliseconds between self-driven pumps; 100 when omitted, 0 to turn the timer off and call `update()` yourself */
|
|
23
|
+
pumpMs?: number;
|
|
24
|
+
/** replaces `setTimeout` for the pump, so a test drives time by hand */
|
|
25
|
+
schedule?: (callback: () => void, ms: number) => unknown;
|
|
26
|
+
/** cancels what `schedule` returned; `clearTimeout` when omitted */
|
|
27
|
+
unschedule?: (handle: unknown) => void;
|
|
28
|
+
}
|
|
29
|
+
export interface StreamPlayOptions {
|
|
30
|
+
/** level, 0 to 1 */
|
|
31
|
+
volume?: number;
|
|
32
|
+
/** playback ratio, 1 unchanged: speed and pitch together, resampled linearly */
|
|
33
|
+
pitch?: number;
|
|
34
|
+
/** stereo position, -1 (left) to 1 (right) */
|
|
35
|
+
pan?: number;
|
|
36
|
+
/** start offset in seconds */
|
|
37
|
+
pos?: number;
|
|
38
|
+
/** true repeats through `loop`, or the whole source when it has a length; false plays once */
|
|
39
|
+
repeat?: boolean;
|
|
40
|
+
/** seconds the repeat jumps back to and wraps from; the whole source when omitted */
|
|
41
|
+
loop?: LoopRegion | null;
|
|
42
|
+
/** stop after this many seconds played, for a source with no end; unlimited when omitted */
|
|
43
|
+
endAfterSeconds?: number;
|
|
44
|
+
/** seconds of fade ahead of `endAfterSeconds`'s end; 2 when omitted */
|
|
45
|
+
endFadeSeconds?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The streaming twin of `Channel`: it queues short buffers on the audio clock ahead of the
|
|
49
|
+
* playhead from a `BlockSource` instead of owning one decoded buffer, so a long or endless
|
|
50
|
+
* track costs a couple of seconds of memory rather than the whole render. It loops on a
|
|
51
|
+
* region, seeks, retunes volume, pitch and pan, fades an endless source out after a limit, and
|
|
52
|
+
* reports `onUnderrun` when the clock got ahead of what was queued.
|
|
53
|
+
*
|
|
54
|
+
* Chunks are queued by a timer it runs itself (`pumpMs`), so a game that never calls `update()`
|
|
55
|
+
* still hears sound; a timer throttled in a background tab is the reason the lookahead is two
|
|
56
|
+
* seconds. A pitch change applies from the next chunk, up to the lookahead later.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { StreamChannel, type BlockSource } from '@datamoc/mw_games/audio';
|
|
61
|
+
*
|
|
62
|
+
* declare const context: AudioContext;
|
|
63
|
+
* declare const tune: BlockSource;
|
|
64
|
+
*
|
|
65
|
+
* const channel = new StreamChannel(context);
|
|
66
|
+
* channel.onEnded.add(() => console.log('done'));
|
|
67
|
+
* channel.play(tune, { repeat: false, volume: 0.8 });
|
|
68
|
+
* console.log(channel.position());
|
|
69
|
+
* channel.stop(1);
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare class StreamChannel {
|
|
73
|
+
/** fires once when a source that does not repeat has played out, or an endless one reached its limit */
|
|
74
|
+
readonly onEnded: Signal<void>;
|
|
75
|
+
/** fires when the clock passed the end of the queue before the next chunk was ready, with how late it was in seconds */
|
|
76
|
+
readonly onUnderrun: Signal<number>;
|
|
77
|
+
/** fires when the source threw while being read: the voice stops instead of letting the error escape from a timer */
|
|
78
|
+
readonly onError: Signal<Error>;
|
|
79
|
+
private readonly context;
|
|
80
|
+
private readonly destination;
|
|
81
|
+
private readonly chunkSeconds;
|
|
82
|
+
private readonly lookahead;
|
|
83
|
+
private readonly pumpMs;
|
|
84
|
+
private readonly schedule;
|
|
85
|
+
private readonly unschedule;
|
|
86
|
+
private timer;
|
|
87
|
+
private source;
|
|
88
|
+
private gainNode;
|
|
89
|
+
private pannerNode;
|
|
90
|
+
private params;
|
|
91
|
+
private repeat;
|
|
92
|
+
private loopStart;
|
|
93
|
+
private loopEnd;
|
|
94
|
+
private endAfterFrames;
|
|
95
|
+
private endFadeFrames;
|
|
96
|
+
private chunks;
|
|
97
|
+
private nextWhen;
|
|
98
|
+
private active;
|
|
99
|
+
private drained;
|
|
100
|
+
private lastTimeline;
|
|
101
|
+
private outputFrames;
|
|
102
|
+
private underruns;
|
|
103
|
+
private pendingLeft;
|
|
104
|
+
private pendingRight;
|
|
105
|
+
private pendingLength;
|
|
106
|
+
private cursor;
|
|
107
|
+
private readFrame;
|
|
108
|
+
private timeline;
|
|
109
|
+
private sourceEnded;
|
|
110
|
+
constructor(context: AudioContext, destination?: AudioNode, options?: StreamChannelOptions);
|
|
111
|
+
get isPlaying(): boolean;
|
|
112
|
+
/** how many times the queue ran dry since the channel was made */
|
|
113
|
+
get underrunCount(): number;
|
|
114
|
+
/** seconds of the source, or Infinity when it has no end */
|
|
115
|
+
get duration(): number;
|
|
116
|
+
/** starts the source with these parameters, stopping whatever played before */
|
|
117
|
+
play(source: BlockSource, options?: StreamPlayOptions): void;
|
|
118
|
+
/** retunes the playing voice in place: no restart, no seek */
|
|
119
|
+
update(params?: {
|
|
120
|
+
volume?: number;
|
|
121
|
+
pitch?: number;
|
|
122
|
+
pan?: number;
|
|
123
|
+
}): void;
|
|
124
|
+
/** stops the voice, fading out over `fadeSeconds` first when positive; the playhead is remembered for a later `play` with the same `pos` */
|
|
125
|
+
stop(fadeSeconds?: number): void;
|
|
126
|
+
/** seconds into the source, wrapping inside the loop region while it repeats */
|
|
127
|
+
position(): number;
|
|
128
|
+
/** the playhead as a source frame, the exact unit a save stores and a restore skips to */
|
|
129
|
+
positionFrames(): number;
|
|
130
|
+
/** moves the playhead, restarting the queue from there */
|
|
131
|
+
seek(seconds: number): void;
|
|
132
|
+
/** queues whatever chunks the lookahead allows; called by the timer and safe to call each frame */
|
|
133
|
+
pump(): void;
|
|
134
|
+
private wire;
|
|
135
|
+
private cancelTimer;
|
|
136
|
+
private begin;
|
|
137
|
+
/** a position inside the loop region while repeating, else as given */
|
|
138
|
+
private timelineAt;
|
|
139
|
+
private queueChunk;
|
|
140
|
+
/** marks the stream as fully queued; the ending fires when the last chunk has sounded */
|
|
141
|
+
private finish;
|
|
142
|
+
/** resamples source frames into the two output arrays; returns how many it wrote */
|
|
143
|
+
private fill;
|
|
144
|
+
/** appends the next block of source frames, wrapping to the loop start at its end */
|
|
145
|
+
private readMore;
|
|
146
|
+
private ensureCapacity;
|
|
147
|
+
/** drops consumed frames so the buffer does not grow with the length of the track */
|
|
148
|
+
private compact;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=StreamChannel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StreamChannel.d.ts","sourceRoot":"","sources":["../../src/audio/StreamChannel.ts"],"names":[],"mappings":"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"}
|