@replayablejs/runtime 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/adapters/applovin.d.ts +13 -0
- package/dist/adapters/applovin.js +16 -0
- package/dist/adapters/applovin.js.map +1 -0
- package/dist/adapters/browser.d.ts +12 -0
- package/dist/adapters/browser.js +17 -0
- package/dist/adapters/browser.js.map +1 -0
- package/dist/adapters/google.d.ts +12 -0
- package/dist/adapters/google.js +17 -0
- package/dist/adapters/google.js.map +1 -0
- package/dist/adapters/liftoff.d.ts +13 -0
- package/dist/adapters/liftoff.js +47 -0
- package/dist/adapters/liftoff.js.map +1 -0
- package/dist/adapters/meta.d.ts +12 -0
- package/dist/adapters/meta.js +16 -0
- package/dist/adapters/meta.js.map +1 -0
- package/dist/adapters/mintegral.d.ts +13 -0
- package/dist/adapters/mintegral.js +59 -0
- package/dist/adapters/mintegral.js.map +1 -0
- package/dist/adapters/moloco.d.ts +11 -0
- package/dist/adapters/moloco.js +15 -0
- package/dist/adapters/moloco.js.map +1 -0
- package/dist/adapters/unity.d.ts +12 -0
- package/dist/adapters/unity.js +15 -0
- package/dist/adapters/unity.js.map +1 -0
- package/dist/api-BgI_ycxT.js +13 -0
- package/dist/api-BgI_ycxT.js.map +1 -0
- package/dist/assets-BD68MQTL.d.ts +107 -0
- package/dist/assets.d.ts +2 -0
- package/dist/assets.js +8 -0
- package/dist/assets.js.map +1 -0
- package/dist/audio/disabled.d.ts +12 -0
- package/dist/audio/disabled.js +44 -0
- package/dist/audio/disabled.js.map +1 -0
- package/dist/audio/enabled.d.ts +28 -0
- package/dist/audio/enabled.js +426 -0
- package/dist/audio/enabled.js.map +1 -0
- package/dist/audio-CyLjSreN.d.ts +66 -0
- package/dist/categories-BbQSRMmw.js +21 -0
- package/dist/categories-BbQSRMmw.js.map +1 -0
- package/dist/create-adapter-4oub3UeP.js +22 -0
- package/dist/create-adapter-4oub3UeP.js.map +1 -0
- package/dist/create-adapter-ByJx4UTX.js +43 -0
- package/dist/create-adapter-ByJx4UTX.js.map +1 -0
- package/dist/events-C85TpS5f.js +69 -0
- package/dist/events-C85TpS5f.js.map +1 -0
- package/dist/events-CYG_LvaH.js +85 -0
- package/dist/events-CYG_LvaH.js.map +1 -0
- package/dist/host-adapter-vFX6DBcY.d.ts +43 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +8876 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-Czz-x3KH.d.ts +61 -0
- package/dist/screen-DXf2FPtL.d.ts +70 -0
- package/dist/shell.d.ts +10 -0
- package/dist/shell.js +11 -0
- package/dist/shell.js.map +1 -0
- package/package.json +75 -0
- package/src/screen/gpu/benchmarks/LICENSE +21 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { t as AudioController } from "../audio-CyLjSreN.js";
|
|
2
|
+
import { s as AssetLoader } from "../loader-Czz-x3KH.js";
|
|
3
|
+
//#region src/audio/enabled.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates Howler-backed sound playback for an audio-enabled variant.
|
|
6
|
+
*
|
|
7
|
+
* This controller coordinates three lower-level components:
|
|
8
|
+
*
|
|
9
|
+
* - The asset loader turns generated sound URLs into loaded `Howl` instances.
|
|
10
|
+
* - The lifecycle applies host volume, visibility, and browser-unlock policy.
|
|
11
|
+
* - Managed playbacks own individual voices, fades, and final cleanup.
|
|
12
|
+
*
|
|
13
|
+
* There is only one playback implementation (`ManagedAudioPlayback`).
|
|
14
|
+
* The two internal collections below represent states of the same type:
|
|
15
|
+
* 1) `managedPlaybacks` keeps every live handle.
|
|
16
|
+
* 2) `unresolvedPlaybacks` indexes pending handles still missing a loaded sound.
|
|
17
|
+
*
|
|
18
|
+
* A managed `play()` request is retained until both gates open:
|
|
19
|
+
* - sound is loaded
|
|
20
|
+
* - runtime audio is allowed
|
|
21
|
+
*
|
|
22
|
+
* Either gate can open first. A transient `playOneShot()` is dropped while
|
|
23
|
+
* audio is blocked, preventing stale effects from playing later.
|
|
24
|
+
*/
|
|
25
|
+
declare function createAudio(loader: AssetLoader): AudioController;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { createAudio };
|
|
28
|
+
//# sourceMappingURL=enabled.d.ts.map
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { Howl, Howler } from "howler/dist/howler.core.min.js";
|
|
2
|
+
//#region src/audio/howler-lifecycle.ts
|
|
3
|
+
const CONTEXT_RESUME_DELAY_MS = 200;
|
|
4
|
+
/**
|
|
5
|
+
* Coordinates Howler with Replayable's normalized host lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* Howler's automatic suspension is disabled because iOS Safari can leave its
|
|
8
|
+
* `AudioContext` suspended after an ad becomes visible again. The WebKit
|
|
9
|
+
* workaround suspends immediately and schedules an independent resume 200 ms
|
|
10
|
+
* later. The recovery deliberately does not sequence those operations through
|
|
11
|
+
* promise settlement: affected WebKit versions can leave an audio-context
|
|
12
|
+
* transition promise unsettled indefinitely.
|
|
13
|
+
*
|
|
14
|
+
* A subsequent hidden transition cancels the pending resume and suspends the
|
|
15
|
+
* context again, preventing the delayed recovery from starting background
|
|
16
|
+
* audio.
|
|
17
|
+
*
|
|
18
|
+
* @see https://bugs.webkit.org/show_bug.cgi?id=276016
|
|
19
|
+
* @see https://bugs.webkit.org/show_bug.cgi?id=281566
|
|
20
|
+
*/
|
|
21
|
+
function createHowlerLifecycle() {
|
|
22
|
+
let recoveryTimeout;
|
|
23
|
+
let visible = false;
|
|
24
|
+
Howler.autoSuspend = false;
|
|
25
|
+
Howler.mute(true);
|
|
26
|
+
return {
|
|
27
|
+
setVisible(nextVisible) {
|
|
28
|
+
visible = nextVisible;
|
|
29
|
+
clearRecoveryTimeout();
|
|
30
|
+
if (!Howler.usingWebAudio) return;
|
|
31
|
+
Howler.ctx.suspend().catch(() => {});
|
|
32
|
+
if (!visible) return;
|
|
33
|
+
recoveryTimeout = window.setTimeout(() => {
|
|
34
|
+
recoveryTimeout = void 0;
|
|
35
|
+
if (!visible) return;
|
|
36
|
+
Howler.ctx.resume().catch(() => {});
|
|
37
|
+
}, CONTEXT_RESUME_DELAY_MS);
|
|
38
|
+
},
|
|
39
|
+
update({ allowed, volume }, muted) {
|
|
40
|
+
Howler.volume(volume);
|
|
41
|
+
Howler.mute(!allowed || muted);
|
|
42
|
+
},
|
|
43
|
+
unlock() {
|
|
44
|
+
if (!Howler.usingWebAudio || Howler.ctx.state === "running") return;
|
|
45
|
+
Howler.ctx.resume().catch(() => {});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function clearRecoveryTimeout() {
|
|
49
|
+
if (recoveryTimeout === void 0) return;
|
|
50
|
+
window.clearTimeout(recoveryTimeout);
|
|
51
|
+
recoveryTimeout = void 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/audio/resolve-options.ts
|
|
56
|
+
const DEFAULT_FADE_DURATION = 0;
|
|
57
|
+
const DEFAULT_VOLUME = 1;
|
|
58
|
+
/**
|
|
59
|
+
* Produces the complete options used to create one managed playback.
|
|
60
|
+
*
|
|
61
|
+
* Defaults are applied once at the public API boundary so the playback state
|
|
62
|
+
* machine never needs to distinguish omitted values from explicit values:
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* resolveAudioPlaybackOptions();
|
|
66
|
+
* // { volume: 1, loop: false, fadeIn: 0 }
|
|
67
|
+
*
|
|
68
|
+
* resolveAudioPlaybackOptions({ loop: true, volume: 0.5 });
|
|
69
|
+
* // { volume: 0.5, loop: true, fadeIn: 0 }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* Invalid numeric values throw before a Howler voice is created.
|
|
73
|
+
*/
|
|
74
|
+
function resolveAudioPlaybackOptions(options = {}) {
|
|
75
|
+
return {
|
|
76
|
+
volume: resolveVolume(options.volume),
|
|
77
|
+
loop: options.loop ?? false,
|
|
78
|
+
fadeIn: resolveFadeDuration("fade-in", options.fadeIn)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Produces the complete options used by one transient sound effect.
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* resolveAudioOneShotOptions();
|
|
86
|
+
* // { volume: 1 }
|
|
87
|
+
*
|
|
88
|
+
* resolveAudioOneShotOptions({ volume: 0.8 });
|
|
89
|
+
* // { volume: 0.8 }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
function resolveAudioOneShotOptions(options = {}) {
|
|
93
|
+
return { volume: resolveVolume(options.volume) };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Produces the complete options used to stop managed playback.
|
|
97
|
+
*
|
|
98
|
+
* ```ts
|
|
99
|
+
* resolveAudioStopOptions();
|
|
100
|
+
* // { fadeOut: 0 }
|
|
101
|
+
*
|
|
102
|
+
* resolveAudioStopOptions({ fadeOut: 0.4 });
|
|
103
|
+
* // { fadeOut: 0.4 }
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* A zero duration stops immediately; a positive duration is converted to
|
|
107
|
+
* milliseconds only when the managed playback calls Howler.
|
|
108
|
+
*/
|
|
109
|
+
function resolveAudioStopOptions(options = {}) {
|
|
110
|
+
return { fadeOut: resolveFadeDuration("fade-out", options.fadeOut) };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Resolves public volume onto Howler's normalized linear gain range.
|
|
114
|
+
*
|
|
115
|
+
* `Number.isFinite` deliberately rejects `NaN` and both infinities in addition
|
|
116
|
+
* to values outside the inclusive `0..1` range.
|
|
117
|
+
*/
|
|
118
|
+
function resolveVolume(volume = DEFAULT_VOLUME) {
|
|
119
|
+
if (!Number.isFinite(volume) || volume < 0 || volume > 1) throw new RangeError(`Audio volume must be a finite number from 0 through 1; received ${String(volume)}.`);
|
|
120
|
+
return volume;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolves one public fade duration expressed in Replayable-standard seconds.
|
|
124
|
+
*
|
|
125
|
+
* Keeping seconds in the public API matches Replayable's update and fixed-update
|
|
126
|
+
* timing contracts. Only the Howler integration converts this value to
|
|
127
|
+
* milliseconds. `Number.isFinite` also prevents `NaN` and infinity from
|
|
128
|
+
* reaching browser timers.
|
|
129
|
+
*/
|
|
130
|
+
function resolveFadeDuration(name, duration = DEFAULT_FADE_DURATION) {
|
|
131
|
+
if (!Number.isFinite(duration) || duration < 0) throw new RangeError(`Audio ${name} duration must be a finite, non-negative number of seconds; received ${String(duration)}.`);
|
|
132
|
+
return duration;
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/audio/managed-playback.ts
|
|
136
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
137
|
+
/**
|
|
138
|
+
* Creates one managed Howler voice that may wait for both its sound and unlock.
|
|
139
|
+
*
|
|
140
|
+
* A playback exists as a single object and moves through this lifecycle:
|
|
141
|
+
*
|
|
142
|
+
* 1. It starts pending, without requiring a loaded sound or allocating a voice.
|
|
143
|
+
* 2. `setSound()` stores the loaded `Howl`; playback is still pending.
|
|
144
|
+
* 3. `start()` allocates one voice and records its ID.
|
|
145
|
+
* 4. A non-looping voice finishes through Howler's `end` event, while a loop
|
|
146
|
+
* remains active until the application calls `stop()`.
|
|
147
|
+
* 5. Stopping (immediate or faded) removes listeners, stops the exact voice,
|
|
148
|
+
* and calls `onFinished` so the orchestrator can forget the handle.
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* const playback = createManagedPlayback({
|
|
152
|
+
* fadeIn: 0.5,
|
|
153
|
+
* loop: true,
|
|
154
|
+
* volume: 0.6,
|
|
155
|
+
* }, removePlayback);
|
|
156
|
+
*
|
|
157
|
+
* playback.setSound(sound);
|
|
158
|
+
* playback.start();
|
|
159
|
+
* playback.stop({ fadeOut: 0.4 });
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* Calling `stop()` before `start()` simply finishes the pending handle: no voice
|
|
163
|
+
* is created and later load/unlock events will not restart it. Howler operations
|
|
164
|
+
* are routed by this handle's playback ID so shared source `Howl`s do not mix
|
|
165
|
+
* control.
|
|
166
|
+
*
|
|
167
|
+
* `start` is invoked by the audio controller when loading or permission opens a gate.
|
|
168
|
+
* `stop` is the stable public handle returned to playable application code.
|
|
169
|
+
*/
|
|
170
|
+
function createManagedPlayback(options, onFinished) {
|
|
171
|
+
let activeVoice;
|
|
172
|
+
let endListener;
|
|
173
|
+
let sound;
|
|
174
|
+
let status = "pending";
|
|
175
|
+
const playback = {
|
|
176
|
+
setSound(loadedSound) {
|
|
177
|
+
if (status !== "pending" || sound !== void 0) return;
|
|
178
|
+
sound = loadedSound;
|
|
179
|
+
},
|
|
180
|
+
start() {
|
|
181
|
+
if (status !== "pending" || sound === void 0) return;
|
|
182
|
+
const playbackId = sound.play();
|
|
183
|
+
activeVoice = {
|
|
184
|
+
id: playbackId,
|
|
185
|
+
sound
|
|
186
|
+
};
|
|
187
|
+
status = "playing";
|
|
188
|
+
const fadeInMilliseconds = options.fadeIn * MILLISECONDS_PER_SECOND;
|
|
189
|
+
const shouldFadeIn = fadeInMilliseconds > 0 && options.volume > 0;
|
|
190
|
+
sound.volume(shouldFadeIn ? 0 : options.volume, playbackId);
|
|
191
|
+
sound.loop(options.loop, playbackId);
|
|
192
|
+
if (shouldFadeIn) sound.fade(0, options.volume, fadeInMilliseconds, playbackId);
|
|
193
|
+
if (!options.loop) listenForEnd();
|
|
194
|
+
},
|
|
195
|
+
stop(stopOptions) {
|
|
196
|
+
if (status === "stopping" || status === "finished") return;
|
|
197
|
+
const { fadeOut } = resolveAudioStopOptions(stopOptions);
|
|
198
|
+
if (activeVoice === void 0 || fadeOut === 0) {
|
|
199
|
+
finish();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
status = "stopping";
|
|
203
|
+
removeEndListener();
|
|
204
|
+
fadeOutPlayback(fadeOut);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
return playback;
|
|
208
|
+
/** Stops this voice after fading from its current volume to silence. */
|
|
209
|
+
function fadeOutPlayback(duration) {
|
|
210
|
+
if (activeVoice === void 0) return;
|
|
211
|
+
const { id, sound: activeSound } = activeVoice;
|
|
212
|
+
const currentVolume = activeSound.volume(id);
|
|
213
|
+
if (typeof currentVolume !== "number") throw new Error(`Unable to read volume for Howler playback ${id}.`);
|
|
214
|
+
if (currentVolume === 0) {
|
|
215
|
+
finish();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const handleFade = (fadedPlaybackId) => {
|
|
219
|
+
if (fadedPlaybackId !== activeVoice?.id) return;
|
|
220
|
+
finish();
|
|
221
|
+
};
|
|
222
|
+
activeSound.fade(currentVolume, 0, duration * MILLISECONDS_PER_SECOND, id);
|
|
223
|
+
activeSound.once("fade", handleFade, id);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Retires this handle and releases its pending or active Howler voice.
|
|
227
|
+
*
|
|
228
|
+
* `onFinished` removes the handle from the orchestrator's retained set. It is
|
|
229
|
+
* called for pending cancellation, natural completion, and explicit stopping.
|
|
230
|
+
*/
|
|
231
|
+
function finish() {
|
|
232
|
+
status = "finished";
|
|
233
|
+
stopActivePlayback();
|
|
234
|
+
onFinished(playback);
|
|
235
|
+
}
|
|
236
|
+
/** Lets a finite, non-looping sound release itself after natural completion. */
|
|
237
|
+
function listenForEnd() {
|
|
238
|
+
if (activeVoice === void 0) return;
|
|
239
|
+
const { id, sound: activeSound } = activeVoice;
|
|
240
|
+
const handleEnd = (endedPlaybackId) => {
|
|
241
|
+
if (endedPlaybackId !== activeVoice?.id) return;
|
|
242
|
+
endListener = void 0;
|
|
243
|
+
finish();
|
|
244
|
+
};
|
|
245
|
+
endListener = handleEnd;
|
|
246
|
+
activeSound.once("end", handleEnd, id);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Stops the active voice and releases Replayable's Howler listeners.
|
|
250
|
+
*
|
|
251
|
+
* Howler emits `stop`, not `end`, for this operation. The `end` listener is
|
|
252
|
+
* removed because its ID-scoped callback is no longer useful and would
|
|
253
|
+
* otherwise remain retained by the shared `Howl`.
|
|
254
|
+
*/
|
|
255
|
+
function stopActivePlayback() {
|
|
256
|
+
if (activeVoice === void 0) return;
|
|
257
|
+
const { id, sound: activeSound } = activeVoice;
|
|
258
|
+
removeEndListener();
|
|
259
|
+
activeVoice = void 0;
|
|
260
|
+
activeSound.stop(id);
|
|
261
|
+
}
|
|
262
|
+
function removeEndListener() {
|
|
263
|
+
if (endListener === void 0 || activeVoice === void 0) return;
|
|
264
|
+
activeVoice.sound.off("end", endListener, activeVoice.id);
|
|
265
|
+
endListener = void 0;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/audio/sound-loader.ts
|
|
270
|
+
/** Returns one loaded sound, or `undefined` while its bundle remains unloaded. */
|
|
271
|
+
function getLoadedSound(loader, id) {
|
|
272
|
+
const sound = loader.cache.sounds?.[id];
|
|
273
|
+
return sound instanceof Howl ? sound : void 0;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Converts one generated sound URL into a completely loaded Howler sound.
|
|
277
|
+
*
|
|
278
|
+
* Replayable's asset loader calls this handler once for every sound in the
|
|
279
|
+
* requested bundle. Given this context:
|
|
280
|
+
*
|
|
281
|
+
* ```ts
|
|
282
|
+
* {
|
|
283
|
+
* category: 'sounds',
|
|
284
|
+
* id: 'ui-click',
|
|
285
|
+
* source: '/assets/ui-click.m4a',
|
|
286
|
+
* }
|
|
287
|
+
* ```
|
|
288
|
+
*
|
|
289
|
+
* the promise resolves with a `Howl` only after its `load` event. The asset
|
|
290
|
+
* loader then stores that value at `cache.sounds['ui-click']`. Consequently,
|
|
291
|
+
* `loader.load('primary')` does not finish until every primary sound is ready
|
|
292
|
+
* for synchronous `play()` calls.
|
|
293
|
+
*
|
|
294
|
+
* A load failure unloads the partial Howler resource before rejecting the
|
|
295
|
+
* containing bundle load with the generated asset ID in the error message.
|
|
296
|
+
*/
|
|
297
|
+
function loadSound({ id, source }) {
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
const sound = new Howl({
|
|
300
|
+
preload: false,
|
|
301
|
+
src: source
|
|
302
|
+
});
|
|
303
|
+
const handleLoad = () => {
|
|
304
|
+
sound.off("loaderror", handleLoadError);
|
|
305
|
+
resolve(sound);
|
|
306
|
+
};
|
|
307
|
+
const handleLoadError = (_soundId, error) => {
|
|
308
|
+
sound.off("load", handleLoad);
|
|
309
|
+
sound.unload();
|
|
310
|
+
reject(/* @__PURE__ */ new Error(`Unable to load sound ${JSON.stringify(id)}: ${String(error)}.`));
|
|
311
|
+
};
|
|
312
|
+
sound.once("load", handleLoad);
|
|
313
|
+
sound.once("loaderror", handleLoadError);
|
|
314
|
+
sound.load();
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/audio/enabled.ts
|
|
319
|
+
/**
|
|
320
|
+
* Creates Howler-backed sound playback for an audio-enabled variant.
|
|
321
|
+
*
|
|
322
|
+
* This controller coordinates three lower-level components:
|
|
323
|
+
*
|
|
324
|
+
* - The asset loader turns generated sound URLs into loaded `Howl` instances.
|
|
325
|
+
* - The lifecycle applies host volume, visibility, and browser-unlock policy.
|
|
326
|
+
* - Managed playbacks own individual voices, fades, and final cleanup.
|
|
327
|
+
*
|
|
328
|
+
* There is only one playback implementation (`ManagedAudioPlayback`).
|
|
329
|
+
* The two internal collections below represent states of the same type:
|
|
330
|
+
* 1) `managedPlaybacks` keeps every live handle.
|
|
331
|
+
* 2) `unresolvedPlaybacks` indexes pending handles still missing a loaded sound.
|
|
332
|
+
*
|
|
333
|
+
* A managed `play()` request is retained until both gates open:
|
|
334
|
+
* - sound is loaded
|
|
335
|
+
* - runtime audio is allowed
|
|
336
|
+
*
|
|
337
|
+
* Either gate can open first. A transient `playOneShot()` is dropped while
|
|
338
|
+
* audio is blocked, preventing stale effects from playing later.
|
|
339
|
+
*/
|
|
340
|
+
function createAudio(loader) {
|
|
341
|
+
const lifecycle = createHowlerLifecycle();
|
|
342
|
+
const managedPlaybacks = /* @__PURE__ */ new Set();
|
|
343
|
+
const unresolvedPlaybacks = /* @__PURE__ */ new Map();
|
|
344
|
+
let allowed = false;
|
|
345
|
+
let muted = false;
|
|
346
|
+
let runtimeState;
|
|
347
|
+
loader.register("sounds", async (context) => {
|
|
348
|
+
const sound = await loadSound(context);
|
|
349
|
+
resolveSound(context.id, sound);
|
|
350
|
+
return sound;
|
|
351
|
+
});
|
|
352
|
+
return {
|
|
353
|
+
get muted() {
|
|
354
|
+
return muted;
|
|
355
|
+
},
|
|
356
|
+
setMuted(nextMuted) {
|
|
357
|
+
if (muted === nextMuted) return;
|
|
358
|
+
muted = nextMuted;
|
|
359
|
+
if (runtimeState !== void 0) lifecycle.update(runtimeState, muted);
|
|
360
|
+
},
|
|
361
|
+
play(id, options) {
|
|
362
|
+
const playback = createManagedPlayback(resolveAudioPlaybackOptions(options), (finishedPlayback) => {
|
|
363
|
+
removeManagedPlayback(id, finishedPlayback);
|
|
364
|
+
});
|
|
365
|
+
const sound = getLoadedSound(loader, id);
|
|
366
|
+
if (sound === void 0) retainUnresolvedPlayback(id, playback);
|
|
367
|
+
else playback.setSound(sound);
|
|
368
|
+
managedPlaybacks.add(playback);
|
|
369
|
+
startIfAllowed(playback);
|
|
370
|
+
return playback;
|
|
371
|
+
},
|
|
372
|
+
playOneShot(id, options) {
|
|
373
|
+
if (!allowed) return;
|
|
374
|
+
const sound = getLoadedSound(loader, id);
|
|
375
|
+
if (sound === void 0) return;
|
|
376
|
+
const { volume } = resolveAudioOneShotOptions(options);
|
|
377
|
+
const playbackId = sound.play();
|
|
378
|
+
sound.volume(volume, playbackId);
|
|
379
|
+
},
|
|
380
|
+
setVisible(visible) {
|
|
381
|
+
lifecycle.setVisible(visible);
|
|
382
|
+
},
|
|
383
|
+
update(state) {
|
|
384
|
+
const becameAllowed = !allowed && state.allowed;
|
|
385
|
+
allowed = state.allowed;
|
|
386
|
+
runtimeState = state;
|
|
387
|
+
lifecycle.update(state, muted);
|
|
388
|
+
if (becameAllowed) for (const playback of managedPlaybacks) playback.start();
|
|
389
|
+
},
|
|
390
|
+
unlock() {
|
|
391
|
+
lifecycle.unlock();
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
/** Tracks a pending request until the sound loader resolves this asset ID. */
|
|
395
|
+
function retainUnresolvedPlayback(id, playback) {
|
|
396
|
+
const playbacks = unresolvedPlaybacks.get(id) ?? /* @__PURE__ */ new Set();
|
|
397
|
+
playbacks.add(playback);
|
|
398
|
+
unresolvedPlaybacks.set(id, playbacks);
|
|
399
|
+
}
|
|
400
|
+
/** Binds every pending playback for this sound ID to its loaded `Howl`. */
|
|
401
|
+
function resolveSound(id, sound) {
|
|
402
|
+
const playbacks = unresolvedPlaybacks.get(id);
|
|
403
|
+
if (playbacks === void 0) return;
|
|
404
|
+
unresolvedPlaybacks.delete(id);
|
|
405
|
+
for (const playback of playbacks) {
|
|
406
|
+
playback.setSound(sound);
|
|
407
|
+
startIfAllowed(playback);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/** Starts a managed handle only when permission is already allowed. */
|
|
411
|
+
function startIfAllowed(playback) {
|
|
412
|
+
if (allowed) playback.start();
|
|
413
|
+
}
|
|
414
|
+
/** Removes the handle from both registries; pending index only if still unresolved. */
|
|
415
|
+
function removeManagedPlayback(id, playback) {
|
|
416
|
+
managedPlaybacks.delete(playback);
|
|
417
|
+
const playbacks = unresolvedPlaybacks.get(id);
|
|
418
|
+
if (playbacks === void 0) return;
|
|
419
|
+
playbacks.delete(playback);
|
|
420
|
+
if (playbacks.size === 0) unresolvedPlaybacks.delete(id);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
export { createAudio };
|
|
425
|
+
|
|
426
|
+
//# sourceMappingURL=enabled.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enabled.js","names":[],"sources":["../../src/audio/howler-lifecycle.ts","../../src/audio/resolve-options.ts","../../src/audio/managed-playback.ts","../../src/audio/sound-loader.ts","../../src/audio/enabled.ts"],"sourcesContent":["import { Howler } from '#audio/howler.js';\nimport type { RuntimeAudioState } from '#types/audio.js';\n\nconst CONTEXT_RESUME_DELAY_MS = 200;\n\n/**\n * Coordinates Howler with Replayable's normalized host lifecycle.\n *\n * Howler's automatic suspension is disabled because iOS Safari can leave its\n * `AudioContext` suspended after an ad becomes visible again. The WebKit\n * workaround suspends immediately and schedules an independent resume 200 ms\n * later. The recovery deliberately does not sequence those operations through\n * promise settlement: affected WebKit versions can leave an audio-context\n * transition promise unsettled indefinitely.\n *\n * A subsequent hidden transition cancels the pending resume and suspends the\n * context again, preventing the delayed recovery from starting background\n * audio.\n *\n * @see https://bugs.webkit.org/show_bug.cgi?id=276016\n * @see https://bugs.webkit.org/show_bug.cgi?id=281566\n */\nexport function createHowlerLifecycle() {\n let recoveryTimeout: number | undefined;\n let visible = false;\n\n Howler.autoSuspend = false;\n Howler.mute(true);\n\n return {\n setVisible(nextVisible: boolean): void {\n visible = nextVisible;\n clearRecoveryTimeout();\n\n if (!Howler.usingWebAudio) {\n return;\n }\n\n // Do not await this promise. The following resume must remain\n // independently scheduled rather than depending on WebKit settling the\n // preceding audio-context transition.\n void Howler.ctx.suspend().catch(() => {\n // The context may already be closed or unavailable.\n });\n\n if (!visible) {\n return;\n }\n\n // WebKit's published reproduction uses 200 ms to let the visibility\n // transition settle before attempting to resume the context.\n recoveryTimeout = window.setTimeout(() => {\n recoveryTimeout = undefined;\n\n if (!visible) {\n return;\n }\n\n void Howler.ctx.resume().catch(() => {\n // A later trusted interaction or visibility transition can retry.\n });\n }, CONTEXT_RESUME_DELAY_MS);\n },\n\n update({ allowed, volume }: RuntimeAudioState, muted: boolean): void {\n Howler.volume(volume);\n Howler.mute(!allowed || muted);\n },\n\n unlock(): void {\n if (!Howler.usingWebAudio || Howler.ctx.state === 'running') {\n return;\n }\n\n // Invocation—not promise settlement—must occur in the trusted event task.\n // Howler also installs an unlock listener; this explicit attempt makes the\n // runtime contract independent of that internal implementation detail.\n void Howler.ctx.resume().catch(() => {\n // A later trusted interaction or visibility recovery can retry.\n });\n },\n };\n\n function clearRecoveryTimeout(): void {\n if (recoveryTimeout === undefined) {\n return;\n }\n\n window.clearTimeout(recoveryTimeout);\n recoveryTimeout = undefined;\n }\n}\n","import type { AudioOneShotOptions, AudioPlaybackOptions, AudioStopOptions } from '#types/audio.js';\n\nconst DEFAULT_FADE_DURATION = 0;\nconst DEFAULT_VOLUME = 1;\n\n/**\n * Produces the complete options used to create one managed playback.\n *\n * Defaults are applied once at the public API boundary so the playback state\n * machine never needs to distinguish omitted values from explicit values:\n *\n * ```ts\n * resolveAudioPlaybackOptions();\n * // { volume: 1, loop: false, fadeIn: 0 }\n *\n * resolveAudioPlaybackOptions({ loop: true, volume: 0.5 });\n * // { volume: 0.5, loop: true, fadeIn: 0 }\n * ```\n *\n * Invalid numeric values throw before a Howler voice is created.\n */\nexport function resolveAudioPlaybackOptions(\n options: AudioPlaybackOptions = {},\n): Required<AudioPlaybackOptions> {\n return {\n volume: resolveVolume(options.volume),\n loop: options.loop ?? false,\n fadeIn: resolveFadeDuration('fade-in', options.fadeIn),\n };\n}\n\n/**\n * Produces the complete options used by one transient sound effect.\n *\n * ```ts\n * resolveAudioOneShotOptions();\n * // { volume: 1 }\n *\n * resolveAudioOneShotOptions({ volume: 0.8 });\n * // { volume: 0.8 }\n * ```\n */\nexport function resolveAudioOneShotOptions(\n options: AudioOneShotOptions = {},\n): Required<AudioOneShotOptions> {\n return {\n volume: resolveVolume(options.volume),\n };\n}\n\n/**\n * Produces the complete options used to stop managed playback.\n *\n * ```ts\n * resolveAudioStopOptions();\n * // { fadeOut: 0 }\n *\n * resolveAudioStopOptions({ fadeOut: 0.4 });\n * // { fadeOut: 0.4 }\n * ```\n *\n * A zero duration stops immediately; a positive duration is converted to\n * milliseconds only when the managed playback calls Howler.\n */\nexport function resolveAudioStopOptions(\n options: AudioStopOptions = {},\n): Required<AudioStopOptions> {\n return {\n fadeOut: resolveFadeDuration('fade-out', options.fadeOut),\n };\n}\n\n/**\n * Resolves public volume onto Howler's normalized linear gain range.\n *\n * `Number.isFinite` deliberately rejects `NaN` and both infinities in addition\n * to values outside the inclusive `0..1` range.\n */\nfunction resolveVolume(volume = DEFAULT_VOLUME): number {\n if (!Number.isFinite(volume) || volume < 0 || volume > 1) {\n throw new RangeError(\n `Audio volume must be a finite number from 0 through 1; received ${String(volume)}.`,\n );\n }\n\n return volume;\n}\n\n/**\n * Resolves one public fade duration expressed in Replayable-standard seconds.\n *\n * Keeping seconds in the public API matches Replayable's update and fixed-update\n * timing contracts. Only the Howler integration converts this value to\n * milliseconds. `Number.isFinite` also prevents `NaN` and infinity from\n * reaching browser timers.\n */\nfunction resolveFadeDuration(\n name: 'fade-in' | 'fade-out',\n duration = DEFAULT_FADE_DURATION,\n): number {\n if (!Number.isFinite(duration) || duration < 0) {\n throw new RangeError(\n `Audio ${name} duration must be a finite, non-negative number of seconds; received ${String(duration)}.`,\n );\n }\n\n return duration;\n}\n","import type { Howl, HowlCallback } from '#audio/howler.js';\nimport { resolveAudioStopOptions } from '#audio/resolve-options.js';\nimport type {\n PlaybackStatus,\n ActiveVoice,\n AudioPlaybackOptions,\n AudioStopOptions,\n ManagedAudioPlayback,\n} from '#types/audio.js';\n\nconst MILLISECONDS_PER_SECOND = 1000;\n\n/**\n * Creates one managed Howler voice that may wait for both its sound and unlock.\n *\n * A playback exists as a single object and moves through this lifecycle:\n *\n * 1. It starts pending, without requiring a loaded sound or allocating a voice.\n * 2. `setSound()` stores the loaded `Howl`; playback is still pending.\n * 3. `start()` allocates one voice and records its ID.\n * 4. A non-looping voice finishes through Howler's `end` event, while a loop\n * remains active until the application calls `stop()`.\n * 5. Stopping (immediate or faded) removes listeners, stops the exact voice,\n * and calls `onFinished` so the orchestrator can forget the handle.\n *\n * ```ts\n * const playback = createManagedPlayback({\n * fadeIn: 0.5,\n * loop: true,\n * volume: 0.6,\n * }, removePlayback);\n *\n * playback.setSound(sound);\n * playback.start();\n * playback.stop({ fadeOut: 0.4 });\n * ```\n *\n * Calling `stop()` before `start()` simply finishes the pending handle: no voice\n * is created and later load/unlock events will not restart it. Howler operations\n * are routed by this handle's playback ID so shared source `Howl`s do not mix\n * control.\n *\n * `start` is invoked by the audio controller when loading or permission opens a gate.\n * `stop` is the stable public handle returned to playable application code.\n */\nexport function createManagedPlayback(\n options: Required<AudioPlaybackOptions>,\n onFinished: (playback: ManagedAudioPlayback) => void,\n): ManagedAudioPlayback {\n let activeVoice: ActiveVoice | undefined;\n let endListener: HowlCallback | undefined;\n let sound: Howl | undefined;\n let status: PlaybackStatus = 'pending';\n\n const playback: ManagedAudioPlayback = {\n setSound(loadedSound): void {\n if (status !== 'pending' || sound !== undefined) {\n return;\n }\n\n sound = loadedSound;\n },\n\n start(): void {\n if (status !== 'pending' || sound === undefined) {\n return;\n }\n\n // Howler returns a new ID for this voice even when another handle is\n // already playing the same source sound.\n const playbackId = sound.play();\n\n activeVoice = { id: playbackId, sound };\n status = 'playing';\n\n const fadeInMilliseconds = options.fadeIn * MILLISECONDS_PER_SECOND;\n const shouldFadeIn = fadeInMilliseconds > 0 && options.volume > 0;\n\n // Volume and looping are scoped to this ID rather than the shared Howl.\n sound.volume(shouldFadeIn ? 0 : options.volume, playbackId);\n sound.loop(options.loop, playbackId);\n\n if (shouldFadeIn) {\n sound.fade(0, options.volume, fadeInMilliseconds, playbackId);\n }\n\n if (!options.loop) {\n listenForEnd();\n }\n },\n\n stop(stopOptions?: AudioStopOptions): void {\n if (status === 'stopping' || status === 'finished') {\n return;\n }\n\n const { fadeOut } = resolveAudioStopOptions(stopOptions);\n\n if (activeVoice === undefined || fadeOut === 0) {\n finish();\n\n return;\n }\n\n // The handle rejects repeated commands while the underlying voice remains\n // audible until its fade completes.\n status = 'stopping';\n removeEndListener();\n fadeOutPlayback(fadeOut);\n },\n };\n\n return playback;\n\n /** Stops this voice after fading from its current volume to silence. */\n function fadeOutPlayback(duration: number): void {\n if (activeVoice === undefined) {\n return;\n }\n\n const { id, sound: activeSound } = activeVoice;\n const currentVolume = activeSound.volume(id);\n\n if (typeof currentVolume !== 'number') {\n throw new Error(`Unable to read volume for Howler playback ${id}.`);\n }\n\n if (currentVolume === 0) {\n finish();\n\n return;\n }\n\n const handleFade = (fadedPlaybackId: number): void => {\n if (fadedPlaybackId !== activeVoice?.id) {\n return;\n }\n\n finish();\n };\n\n // Starting the new fade first replaces an in-progress fade-in. Howler emits\n // `fade` when cancelling that old transition, so Replayable subscribes only\n // after the replacement begins and waits for the fade-out itself.\n activeSound.fade(currentVolume, 0, duration * MILLISECONDS_PER_SECOND, id);\n activeSound.once('fade', handleFade, id);\n }\n\n /**\n * Retires this handle and releases its pending or active Howler voice.\n *\n * `onFinished` removes the handle from the orchestrator's retained set. It is\n * called for pending cancellation, natural completion, and explicit stopping.\n */\n function finish(): void {\n status = 'finished';\n stopActivePlayback();\n onFinished(playback);\n }\n\n /** Lets a finite, non-looping sound release itself after natural completion. */\n function listenForEnd(): void {\n if (activeVoice === undefined) {\n return;\n }\n\n const { id, sound: activeSound } = activeVoice;\n const handleEnd = (endedPlaybackId: number): void => {\n if (endedPlaybackId !== activeVoice?.id) {\n return;\n }\n\n endListener = undefined;\n finish();\n };\n\n endListener = handleEnd;\n activeSound.once('end', handleEnd, id);\n }\n\n /**\n * Stops the active voice and releases Replayable's Howler listeners.\n *\n * Howler emits `stop`, not `end`, for this operation. The `end` listener is\n * removed because its ID-scoped callback is no longer useful and would\n * otherwise remain retained by the shared `Howl`.\n */\n function stopActivePlayback(): void {\n if (activeVoice === undefined) {\n return;\n }\n\n const { id, sound: activeSound } = activeVoice;\n\n removeEndListener();\n activeVoice = undefined;\n activeSound.stop(id);\n }\n\n function removeEndListener(): void {\n if (endListener === undefined || activeVoice === undefined) {\n return;\n }\n\n activeVoice.sound.off('end', endListener, activeVoice.id);\n endListener = undefined;\n }\n}\n","import { Howl, type HowlErrorCallback } from '#audio/howler.js';\nimport type { AssetLoadContext, AssetLoader } from '#types/loader.js';\n\n/** Returns one loaded sound, or `undefined` while its bundle remains unloaded. */\nexport function getLoadedSound(loader: AssetLoader, id: string): Howl | undefined {\n const sound = loader.cache.sounds?.[id];\n\n return sound instanceof Howl ? sound : undefined;\n}\n\n/**\n * Converts one generated sound URL into a completely loaded Howler sound.\n *\n * Replayable's asset loader calls this handler once for every sound in the\n * requested bundle. Given this context:\n *\n * ```ts\n * {\n * category: 'sounds',\n * id: 'ui-click',\n * source: '/assets/ui-click.m4a',\n * }\n * ```\n *\n * the promise resolves with a `Howl` only after its `load` event. The asset\n * loader then stores that value at `cache.sounds['ui-click']`. Consequently,\n * `loader.load('primary')` does not finish until every primary sound is ready\n * for synchronous `play()` calls.\n *\n * A load failure unloads the partial Howler resource before rejecting the\n * containing bundle load with the generated asset ID in the error message.\n */\nexport function loadSound({ id, source }: AssetLoadContext<'sounds'>): Promise<Howl> {\n return new Promise((resolve, reject) => {\n // Manual loading lets Replayable attach both terminal listeners before any\n // network or data-URL decoding work can complete.\n const sound = new Howl({ preload: false, src: source });\n\n const handleLoad = (): void => {\n // The opposite terminal event can no longer occur for this load. Removing\n // it releases the settled promise closure retained by the cached Howl.\n sound.off('loaderror', handleLoadError);\n resolve(sound);\n };\n\n const handleLoadError: HowlErrorCallback = (_soundId, error): void => {\n sound.off('load', handleLoad);\n\n // Release any partially allocated HTMLAudio or Web Audio resources.\n sound.unload();\n reject(new Error(`Unable to load sound ${JSON.stringify(id)}: ${String(error)}.`));\n };\n\n sound.once('load', handleLoad);\n sound.once('loaderror', handleLoadError);\n sound.load();\n });\n}\n","import { createHowlerLifecycle } from '#audio/howler-lifecycle.js';\nimport type { Howl } from '#audio/howler.js';\nimport { createManagedPlayback } from '#audio/managed-playback.js';\nimport { resolveAudioOneShotOptions, resolveAudioPlaybackOptions } from '#audio/resolve-options.js';\nimport { getLoadedSound, loadSound } from '#audio/sound-loader.js';\nimport type {\n AudioController,\n AudioPlayback,\n ManagedAudioPlayback,\n RuntimeAudioState,\n} from '#types/audio.js';\nimport type { AssetLoader } from '#types/loader.js';\n\n/**\n * Creates Howler-backed sound playback for an audio-enabled variant.\n *\n * This controller coordinates three lower-level components:\n *\n * - The asset loader turns generated sound URLs into loaded `Howl` instances.\n * - The lifecycle applies host volume, visibility, and browser-unlock policy.\n * - Managed playbacks own individual voices, fades, and final cleanup.\n *\n * There is only one playback implementation (`ManagedAudioPlayback`).\n * The two internal collections below represent states of the same type:\n * 1) `managedPlaybacks` keeps every live handle.\n * 2) `unresolvedPlaybacks` indexes pending handles still missing a loaded sound.\n *\n * A managed `play()` request is retained until both gates open:\n * - sound is loaded\n * - runtime audio is allowed\n *\n * Either gate can open first. A transient `playOneShot()` is dropped while\n * audio is blocked, preventing stale effects from playing later.\n */\nexport function createAudio(loader: AssetLoader): AudioController {\n const lifecycle = createHowlerLifecycle();\n // All live handles from `play()`: pending or active.\n const managedPlaybacks = new Set<ManagedAudioPlayback>();\n // Pending-by-sound-id index for quick binding when a sound finishes loading.\n const unresolvedPlaybacks = new Map<string, Set<ManagedAudioPlayback>>();\n let allowed = false;\n let muted = false;\n let runtimeState: RuntimeAudioState | undefined;\n\n // Registering this loader callback keeps primary/secondary asset timing safe.\n loader.register('sounds', async (context) => {\n const sound = await loadSound(context);\n\n resolveSound(context.id, sound);\n\n return sound;\n });\n\n return {\n get muted(): boolean {\n return muted;\n },\n\n setMuted(nextMuted): void {\n if (muted === nextMuted) {\n return;\n }\n\n muted = nextMuted;\n\n if (runtimeState !== undefined) {\n lifecycle.update(runtimeState, muted);\n }\n },\n\n play(id, options): AudioPlayback {\n const resolvedOptions = resolveAudioPlaybackOptions(options);\n const playback = createManagedPlayback(resolvedOptions, (finishedPlayback) => {\n removeManagedPlayback(id, finishedPlayback);\n });\n const sound = getLoadedSound(loader, id);\n\n if (sound === undefined) {\n retainUnresolvedPlayback(id, playback);\n } else {\n playback.setSound(sound);\n }\n\n managedPlaybacks.add(playback);\n\n // `start()` re-checks gates for both already-ready and newly-ready cases.\n startIfAllowed(playback);\n\n return playback;\n },\n\n playOneShot(id, options): void {\n // A one-shot has no retained handle: blocked effects are intentionally\n // discarded rather than queued and replayed out of context later.\n if (!allowed) {\n return;\n }\n\n const sound = getLoadedSound(loader, id);\n\n // Unloaded one-shots are dropped intentionally and are never deferred.\n if (sound === undefined) {\n return;\n }\n\n const { volume } = resolveAudioOneShotOptions(options);\n const playbackId = sound.play();\n\n sound.volume(volume, playbackId);\n },\n\n setVisible(visible): void {\n lifecycle.setVisible(visible);\n },\n\n update(state): void {\n const becameAllowed = !allowed && state.allowed;\n allowed = state.allowed;\n runtimeState = state;\n\n // Howler volume and global muting are updated before any deferred voice\n // starts. Existing managed voices keep the same playback ID while blocked;\n // visibility policy may suspend their shared audio context, but Replayable\n // never stops and recreates them during an allowed-state transition.\n lifecycle.update(state, muted);\n\n if (becameAllowed) {\n // Start any pending voices; active voices ignore repeated start attempts.\n for (const playback of managedPlaybacks) {\n playback.start();\n }\n }\n },\n\n unlock(): void {\n lifecycle.unlock();\n },\n };\n\n /** Tracks a pending request until the sound loader resolves this asset ID. */\n function retainUnresolvedPlayback(id: string, playback: ManagedAudioPlayback): void {\n const playbacks = unresolvedPlaybacks.get(id) ?? new Set();\n\n playbacks.add(playback);\n unresolvedPlaybacks.set(id, playbacks);\n }\n\n /** Binds every pending playback for this sound ID to its loaded `Howl`. */\n function resolveSound(id: string, sound: Howl): void {\n const playbacks = unresolvedPlaybacks.get(id);\n\n if (playbacks === undefined) {\n return;\n }\n\n // Remove the index entry before notifying playbacks; a callback can clean up\n // the same playback while handling its own completion.\n unresolvedPlaybacks.delete(id);\n\n for (const playback of playbacks) {\n playback.setSound(sound);\n startIfAllowed(playback);\n }\n }\n\n /** Starts a managed handle only when permission is already allowed. */\n function startIfAllowed(playback: ManagedAudioPlayback): void {\n if (allowed) {\n playback.start();\n }\n }\n\n /** Removes the handle from both registries; pending index only if still unresolved. */\n function removeManagedPlayback(id: string, playback: ManagedAudioPlayback): void {\n managedPlaybacks.delete(playback);\n\n const playbacks = unresolvedPlaybacks.get(id);\n\n if (playbacks === undefined) {\n return;\n }\n\n playbacks.delete(playback);\n\n if (playbacks.size === 0) {\n unresolvedPlaybacks.delete(id);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import "howler/dist/howler.core.min.js";
|
|
2
|
+
//#region src/types/audio.d.ts
|
|
3
|
+
/** Options for one transient sound effect. */
|
|
4
|
+
interface AudioOneShotOptions {
|
|
5
|
+
/** Playback volume from silent (`0`) to full source volume (`1`). */
|
|
6
|
+
readonly volume?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Options for one managed sound playback. */
|
|
9
|
+
interface AudioPlaybackOptions {
|
|
10
|
+
/** Playback volume from silent (`0`) to full source volume (`1`). */
|
|
11
|
+
readonly volume?: number;
|
|
12
|
+
/** Repeats the sound until its playback handle is stopped. */
|
|
13
|
+
readonly loop?: boolean;
|
|
14
|
+
/** Seconds spent fading from silence to the configured volume. */
|
|
15
|
+
readonly fadeIn?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Options for stopping managed sound playback. */
|
|
18
|
+
interface AudioStopOptions {
|
|
19
|
+
/** Seconds spent fading from the current volume to silence before stopping. */
|
|
20
|
+
readonly fadeOut?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Controls one managed playback while pending or active. */
|
|
23
|
+
interface AudioPlayback {
|
|
24
|
+
/** Cancels deferred start or stops the active sound. */
|
|
25
|
+
stop(options?: AudioStopOptions): void;
|
|
26
|
+
}
|
|
27
|
+
/** Capability-aware sound playback exposed to playable application code. */
|
|
28
|
+
interface PlayableAudio {
|
|
29
|
+
/** Whether application audio is currently muted by the playable itself. */
|
|
30
|
+
readonly muted: boolean;
|
|
31
|
+
/** Changes application mute state without stopping active managed playback. */
|
|
32
|
+
setMuted(muted: boolean): void;
|
|
33
|
+
/**
|
|
34
|
+
* Creates managed playback that starts after loading and audio permission.
|
|
35
|
+
*
|
|
36
|
+
* The returned handle remains valid while deferred, so `stop()` before both
|
|
37
|
+
* conditions are met cancels the pending sound.
|
|
38
|
+
*/
|
|
39
|
+
play(id: string, options?: AudioPlaybackOptions): AudioPlayback;
|
|
40
|
+
/**
|
|
41
|
+
* Plays one transient effect immediately, or drops it when unavailable.
|
|
42
|
+
*
|
|
43
|
+
* Dropping blocked or unloaded one-shots prevents stale sounds from replaying
|
|
44
|
+
* after later load/unlock.
|
|
45
|
+
*/
|
|
46
|
+
playOneShot(id: string, options?: AudioOneShotOptions): void;
|
|
47
|
+
}
|
|
48
|
+
/** Effective audio conditions derived from capability and host lifecycle state. */
|
|
49
|
+
interface RuntimeAudioState {
|
|
50
|
+
/** Whether sounds may currently be audible. */
|
|
51
|
+
readonly allowed: boolean;
|
|
52
|
+
/** Host-provided volume normalized from silent (`0`) to full volume (`1`). */
|
|
53
|
+
readonly volume: number;
|
|
54
|
+
}
|
|
55
|
+
/** Internal audio facade controls retained by the runtime lifecycle. */
|
|
56
|
+
interface AudioController extends PlayableAudio {
|
|
57
|
+
/** Applies a host visibility transition to the underlying audio context. */
|
|
58
|
+
setVisible(visible: boolean): void;
|
|
59
|
+
/** Applies the latest effective host and interaction conditions. */
|
|
60
|
+
update(state: RuntimeAudioState): void;
|
|
61
|
+
/** Unlocks browser audio synchronously inside the first trusted interaction. */
|
|
62
|
+
unlock(): void;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { AudioStopOptions as a, AudioPlaybackOptions as i, AudioOneShotOptions as n, PlayableAudio as o, AudioPlayback as r, RuntimeAudioState as s, AudioController as t };
|
|
66
|
+
//# sourceMappingURL=audio-CyLjSreN.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/assets/categories.ts
|
|
2
|
+
/** Categories produced from one selected source file per logical asset. */
|
|
3
|
+
const simpleAssetCategories = [
|
|
4
|
+
"sprites",
|
|
5
|
+
"textures",
|
|
6
|
+
"sounds",
|
|
7
|
+
"fonts",
|
|
8
|
+
"locales"
|
|
9
|
+
];
|
|
10
|
+
/** Categories assembled from several related source files. */
|
|
11
|
+
const groupedAssetCategories = [
|
|
12
|
+
"atlases",
|
|
13
|
+
"spines",
|
|
14
|
+
"shaders"
|
|
15
|
+
];
|
|
16
|
+
/** Every generated asset category understood by the playable runtime. */
|
|
17
|
+
const assetCategories = [...simpleAssetCategories, ...groupedAssetCategories];
|
|
18
|
+
//#endregion
|
|
19
|
+
export { groupedAssetCategories as n, simpleAssetCategories as r, assetCategories as t };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=categories-BbQSRMmw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"categories-BbQSRMmw.js","names":[],"sources":["../src/assets/categories.ts"],"sourcesContent":["/** Categories produced from one selected source file per logical asset. */\nexport const simpleAssetCategories = ['sprites', 'textures', 'sounds', 'fonts', 'locales'] as const;\n\n/** Categories assembled from several related source files. */\nexport const groupedAssetCategories = ['atlases', 'spines', 'shaders'] as const;\n\n/** Every generated asset category understood by the playable runtime. */\nexport const assetCategories = [...simpleAssetCategories, ...groupedAssetCategories] as const;\n"],"mappings":";;AACA,MAAa,wBAAwB;CAAC;CAAW;CAAY;CAAU;CAAS;AAAS;;AAGzF,MAAa,yBAAyB;CAAC;CAAW;CAAU;AAAS;;AAGrE,MAAa,kBAAkB,CAAC,GAAG,uBAAuB,GAAG,sBAAsB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { a as waitForDocumentReady, n as createBrowserSnapshot, t as subscribeToBrowserUpdates } from "./events-C85TpS5f.js";
|
|
2
|
+
//#region src/adapters/browser/create-adapter.ts
|
|
3
|
+
/**
|
|
4
|
+
* Creates a browser-backed host adapter with caller-provided store behavior.
|
|
5
|
+
*
|
|
6
|
+
* Preview and browser-based network adapters share document readiness,
|
|
7
|
+
* visibility, and viewport handling. Only their click-through APIs differ.
|
|
8
|
+
*/
|
|
9
|
+
function createBrowserAdapter(openStore) {
|
|
10
|
+
return {
|
|
11
|
+
async initialize(listener) {
|
|
12
|
+
await waitForDocumentReady();
|
|
13
|
+
subscribeToBrowserUpdates(listener);
|
|
14
|
+
return createBrowserSnapshot();
|
|
15
|
+
},
|
|
16
|
+
openStore
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { createBrowserAdapter as t };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=create-adapter-4oub3UeP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-adapter-4oub3UeP.js","names":[],"sources":["../src/adapters/browser/create-adapter.ts"],"sourcesContent":["import type { HostAdapter, HostSnapshot } from '#types/host-adapter.js';\n\nimport { createBrowserSnapshot, waitForDocumentReady } from './api.js';\nimport { subscribeToBrowserUpdates } from './events.js';\n\n/**\n * Creates a browser-backed host adapter with caller-provided store behavior.\n *\n * Preview and browser-based network adapters share document readiness,\n * visibility, and viewport handling. Only their click-through APIs differ.\n */\nexport function createBrowserAdapter(openStore: HostAdapter['openStore']): HostAdapter {\n return {\n async initialize(listener): Promise<HostSnapshot> {\n await waitForDocumentReady();\n\n subscribeToBrowserUpdates(listener);\n\n return createBrowserSnapshot();\n },\n\n openStore,\n };\n}\n"],"mappings":";;;;;;;;AAWA,SAAgB,qBAAqB,WAAkD;CACrF,OAAO;EACL,MAAM,WAAW,UAAiC;GAChD,MAAM,qBAAqB;GAE3B,0BAA0B,QAAQ;GAElC,OAAO,sBAAsB;EAC/B;EAEA;CACF;AACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { i as waitForMraidReady, n as readMraidViewport, r as requireMraid, t as subscribeToMraidUpdates } from "./events-CYG_LvaH.js";
|
|
2
|
+
//#region src/mraid/create-adapter.ts
|
|
3
|
+
/**
|
|
4
|
+
* Creates the standard MRAID host behavior shared by networks whose CTA opens
|
|
5
|
+
* the authored store URL directly through `mraid.open(url)`.
|
|
6
|
+
*/
|
|
7
|
+
function createMraidHostAdapter() {
|
|
8
|
+
return {
|
|
9
|
+
async initialize(listener) {
|
|
10
|
+
requireMraid();
|
|
11
|
+
await waitForMraidReady();
|
|
12
|
+
subscribeToMraidUpdates(listener);
|
|
13
|
+
subscribeToBrowserViewportChanges(listener);
|
|
14
|
+
return {
|
|
15
|
+
visible: mraid.isViewable(),
|
|
16
|
+
viewport: readMraidViewport(),
|
|
17
|
+
volume: 1
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
openStore(url) {
|
|
21
|
+
requireMraid();
|
|
22
|
+
mraid.open(url);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Supplements MRAID sizeChange with browser signals emitted by host WebViews
|
|
28
|
+
* during device rotation. Runtime state deduplicates equivalent dimensions.
|
|
29
|
+
*/
|
|
30
|
+
function subscribeToBrowserViewportChanges(listener) {
|
|
31
|
+
const handleViewportChange = () => {
|
|
32
|
+
listener({
|
|
33
|
+
type: "resize",
|
|
34
|
+
viewport: readMraidViewport()
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
window.addEventListener("resize", handleViewportChange);
|
|
38
|
+
window.addEventListener("orientationchange", handleViewportChange);
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { createMraidHostAdapter as t };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=create-adapter-ByJx4UTX.js.map
|