@kubohiroya/turbowarp-asset-cache 0.2.1 → 0.3.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.
@@ -1,3 +1,418 @@
1
+ //#region src/audio-output.ts
2
+ /** Media volume handles attenuation and fades; this shared graph adds amplification. */
3
+ var shared;
4
+ var maximumFloat = 34028234663852886e22;
5
+ function createAudioAmplifier(audio) {
6
+ if (typeof globalThis.AudioContext !== "function") throw new Error("Audio amplification requires Web Audio support.");
7
+ if (!shared || shared.context.state === "closed") shared = {
8
+ context: new AudioContext(),
9
+ users: 0
10
+ };
11
+ const owner = shared;
12
+ owner.users++;
13
+ let source;
14
+ let gain;
15
+ let released = false;
16
+ const dispose = () => {
17
+ if (released) return;
18
+ released = true;
19
+ source?.disconnect();
20
+ gain?.disconnect();
21
+ owner.users--;
22
+ if (owner.users === 0) {
23
+ if (shared === owner) shared = void 0;
24
+ owner.context.close().catch(() => {});
25
+ }
26
+ };
27
+ try {
28
+ source = owner.context.createMediaElementSource(audio);
29
+ gain = owner.context.createGain();
30
+ source.connect(gain);
31
+ gain.connect(owner.context.destination);
32
+ } catch (error) {
33
+ dispose();
34
+ throw error;
35
+ }
36
+ const node = gain;
37
+ return {
38
+ setGain(value) {
39
+ node.gain.value = Math.min(maximumFloat, Math.max(1, value));
40
+ },
41
+ resume() {
42
+ return owner.context.state === "running" ? Promise.resolve() : owner.context.resume();
43
+ },
44
+ dispose
45
+ };
46
+ }
47
+ //#endregion
48
+ //#region src/audio-playback.ts
49
+ function playbackGain(value) {
50
+ const gain = value === void 0 ? 1 : value;
51
+ if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0) throw new TypeError("Audio gain must be a finite non-negative number.");
52
+ return gain;
53
+ }
54
+ function rate(value) {
55
+ if (typeof value !== "number" || !Number.isFinite(value) || value < .25 || value > 4) throw new TypeError("Audio playback rate must be from 0.25 to 4.");
56
+ return value;
57
+ }
58
+ function validateAudioPlaybackOptions(options) {
59
+ if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => ![
60
+ "gain",
61
+ "muted",
62
+ "playbackRate",
63
+ "fadeIn",
64
+ "fadeOut"
65
+ ].includes(key))) throw new TypeError("Invalid audio playback options.");
66
+ playbackGain(options.gain);
67
+ if (options.muted !== void 0 && typeof options.muted !== "boolean") throw new TypeError("Audio muted must be a boolean.");
68
+ rate(options.playbackRate ?? 1);
69
+ fadeSeconds(options.fadeIn, "fadeIn");
70
+ fadeSeconds(options.fadeOut, "fadeOut");
71
+ }
72
+ function fadeSeconds(value, label) {
73
+ const seconds = value === void 0 ? 0 : value;
74
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) throw new TypeError(`Audio ${label} must be a finite non-negative number of seconds.`);
75
+ return seconds;
76
+ }
77
+ function aborted() {
78
+ const error = /* @__PURE__ */ new Error("Audio playback was cancelled.");
79
+ error.name = "AbortError";
80
+ return error;
81
+ }
82
+ /** One reusable native element; the caller owns source resolution and resource release. */
83
+ function createBrowserAudioPlayback(audio, options, release, playbackError) {
84
+ validateAudioPlaybackOptions(options);
85
+ let gain = playbackGain(options.gain);
86
+ audio.volume = Math.min(1, gain);
87
+ audio.muted = options.muted ?? false;
88
+ audio.playbackRate = options.playbackRate ?? 1;
89
+ audio.loop = false;
90
+ const defaultFadeIn = fadeSeconds(options.fadeIn, "fadeIn");
91
+ const defaultFadeOut = fadeSeconds(options.fadeOut, "fadeOut");
92
+ let activeFadeIn = defaultFadeIn;
93
+ let activeFadeOut = defaultFadeOut;
94
+ let envelope = 1;
95
+ let fadeStarted = 0;
96
+ let playing = false;
97
+ let fadeTimer;
98
+ let stopping;
99
+ let amplifier;
100
+ const pendingGain = /* @__PURE__ */ new Set();
101
+ let disposed = false;
102
+ let generation = 0;
103
+ let ended = Promise.resolve();
104
+ let settle;
105
+ let rejectStart;
106
+ let detachAbort;
107
+ const pendingMetadata = /* @__PURE__ */ new Set();
108
+ const requireActive = () => {
109
+ if (disposed) throw new Error("Audio playback is disposed.");
110
+ };
111
+ const clearFadeTimer = () => {
112
+ if (fadeTimer !== void 0) clearTimeout(fadeTimer);
113
+ fadeTimer = void 0;
114
+ };
115
+ const finish = (error) => {
116
+ playing = false;
117
+ for (const reject of pendingGain) reject(error ?? aborted());
118
+ pendingGain.clear();
119
+ clearFadeTimer();
120
+ const previousStop = stopping;
121
+ stopping = void 0;
122
+ previousStop?.detach();
123
+ previousStop?.reject(error ?? aborted());
124
+ generation++;
125
+ rejectStart?.(error ?? aborted());
126
+ rejectStart = void 0;
127
+ detachAbort?.();
128
+ detachAbort = void 0;
129
+ settle?.(error);
130
+ settle = void 0;
131
+ };
132
+ const writeVolume = () => {
133
+ audio.volume = Math.min(1, gain) * envelope;
134
+ amplifier?.setGain(gain);
135
+ };
136
+ const prepareAmplifier = () => {
137
+ if (!amplifier && gain > 1) amplifier = createAudioAmplifier(audio);
138
+ writeVolume();
139
+ };
140
+ const stopNow = () => {
141
+ const completed = stopping;
142
+ stopping = void 0;
143
+ completed?.detach();
144
+ audio.pause();
145
+ audio.loop = false;
146
+ audio.currentTime = 0;
147
+ finish();
148
+ envelope = 1;
149
+ writeVolume();
150
+ completed?.resolve();
151
+ };
152
+ const refreshFade = () => {
153
+ clearFadeTimer();
154
+ if (disposed || !playing && !stopping) return;
155
+ const now = performance.now();
156
+ let delay = 16;
157
+ if (stopping) {
158
+ const progress = Math.min(1, (now - stopping.started) / (stopping.seconds * 1e3));
159
+ envelope = stopping.from * (1 - progress);
160
+ writeVolume();
161
+ if (progress >= 1) {
162
+ stopNow();
163
+ return;
164
+ }
165
+ } else {
166
+ const fadeIn = activeFadeIn === 0 ? 1 : Math.min(1, (now - fadeStarted) / (activeFadeIn * 1e3));
167
+ const remaining = Number.isFinite(audio.duration) ? Math.max(0, audio.duration - audio.currentTime) / audio.playbackRate : Infinity;
168
+ const fadeOut = !audio.loop && activeFadeOut > 0 ? Math.min(1, remaining / activeFadeOut) : 1;
169
+ envelope = Math.max(0, Math.min(fadeIn, fadeOut));
170
+ writeVolume();
171
+ if (fadeIn >= 1) {
172
+ if (audio.loop || activeFadeOut === 0) return;
173
+ if (!Number.isFinite(remaining)) delay = 250;
174
+ else if (remaining > activeFadeOut) delay = Math.min(250, Math.max(16, (remaining - activeFadeOut) * 1e3));
175
+ }
176
+ }
177
+ fadeTimer = setTimeout(refreshFade, delay);
178
+ };
179
+ const onEnded = () => {
180
+ if (stopping) {
181
+ stopNow();
182
+ return;
183
+ }
184
+ finish();
185
+ if (activeFadeOut > 0 && !audio.loop) {
186
+ envelope = 0;
187
+ writeVolume();
188
+ }
189
+ };
190
+ const onError = () => {
191
+ audio.pause();
192
+ const error = playbackError(audio.error ?? /* @__PURE__ */ new Error("The browser audio element reported a playback error."));
193
+ finish(error);
194
+ for (const complete of pendingMetadata) complete(error);
195
+ };
196
+ audio.addEventListener("ended", onEnded);
197
+ audio.addEventListener("error", onError);
198
+ for (const event of [
199
+ "durationchange",
200
+ "ratechange",
201
+ "seeked"
202
+ ]) audio.addEventListener(event, refreshFade);
203
+ return Object.freeze({
204
+ get ended() {
205
+ return ended;
206
+ },
207
+ get currentTime() {
208
+ return audio.currentTime;
209
+ },
210
+ get paused() {
211
+ return audio.paused;
212
+ },
213
+ get loop() {
214
+ return audio.loop;
215
+ },
216
+ get gain() {
217
+ return gain;
218
+ },
219
+ get muted() {
220
+ return audio.muted;
221
+ },
222
+ get playbackRate() {
223
+ return audio.playbackRate;
224
+ },
225
+ async play(playOptions = {}) {
226
+ requireActive();
227
+ if (!playOptions || typeof playOptions !== "object" || Array.isArray(playOptions) || Object.keys(playOptions).some((key) => ![
228
+ "restart",
229
+ "loop",
230
+ "signal",
231
+ "fadeIn",
232
+ "fadeOut",
233
+ "gain"
234
+ ].includes(key)) || playOptions.restart !== void 0 && typeof playOptions.restart !== "boolean" || playOptions.loop !== void 0 && typeof playOptions.loop !== "boolean") throw new TypeError("Invalid audio play options.");
235
+ const fadeIn = fadeSeconds(playOptions.fadeIn === void 0 ? defaultFadeIn : playOptions.fadeIn, "fadeIn");
236
+ const fadeOut = fadeSeconds(playOptions.fadeOut === void 0 ? defaultFadeOut : playOptions.fadeOut, "fadeOut");
237
+ const nextGain = playOptions.gain === void 0 ? gain : playbackGain(playOptions.gain);
238
+ const signal = playOptions.signal;
239
+ if (signal?.aborted) throw aborted();
240
+ finish();
241
+ const run = generation;
242
+ gain = nextGain;
243
+ if (playOptions.restart) audio.currentTime = 0;
244
+ audio.loop = playOptions.loop ?? audio.loop;
245
+ activeFadeIn = fadeIn;
246
+ activeFadeOut = fadeOut;
247
+ envelope = fadeIn > 0 ? 0 : 1;
248
+ writeVolume();
249
+ ended = new Promise((resolve, reject) => {
250
+ settle = (error) => error === void 0 ? resolve() : reject(error);
251
+ });
252
+ ended.catch(() => {});
253
+ const onAbort = () => {
254
+ audio.pause();
255
+ finish(aborted());
256
+ };
257
+ signal?.addEventListener("abort", onAbort, { once: true });
258
+ detachAbort = () => signal?.removeEventListener("abort", onAbort);
259
+ try {
260
+ prepareAmplifier();
261
+ const started = audio.play();
262
+ const ready = amplifier ? Promise.all([started, amplifier.resume()]) : Promise.resolve(started);
263
+ await Promise.race([ready, new Promise((_resolve, reject) => {
264
+ rejectStart = reject;
265
+ })]);
266
+ if (run !== generation) throw aborted();
267
+ rejectStart = void 0;
268
+ playing = true;
269
+ fadeStarted = performance.now();
270
+ refreshFade();
271
+ } catch (cause) {
272
+ if (run !== generation) throw cause;
273
+ audio.pause();
274
+ const error = playbackError(cause);
275
+ finish(error);
276
+ throw error;
277
+ }
278
+ },
279
+ pause() {
280
+ requireActive();
281
+ audio.pause();
282
+ finish();
283
+ },
284
+ stop(stopOptions = {}) {
285
+ requireActive();
286
+ if (!stopOptions || typeof stopOptions !== "object" || Array.isArray(stopOptions) || Object.keys(stopOptions).some((key) => !["fadeOut", "signal"].includes(key))) throw new TypeError("Invalid audio stop options.");
287
+ const seconds = fadeSeconds(stopOptions.fadeOut === void 0 ? activeFadeOut : stopOptions.fadeOut, "fadeOut");
288
+ const signal = stopOptions.signal;
289
+ if (signal?.aborted) throw aborted();
290
+ if (seconds === 0 || audio.paused || !playing) {
291
+ stopNow();
292
+ return;
293
+ }
294
+ const previous = stopping;
295
+ stopping = void 0;
296
+ previous?.detach();
297
+ previous?.reject(aborted());
298
+ return new Promise((resolve, reject) => {
299
+ const onAbort = () => {
300
+ const current = stopping;
301
+ stopping = void 0;
302
+ current?.detach();
303
+ stopNow();
304
+ reject(aborted());
305
+ };
306
+ stopping = {
307
+ started: performance.now(),
308
+ seconds,
309
+ from: envelope,
310
+ resolve,
311
+ reject,
312
+ detach: () => signal?.removeEventListener("abort", onAbort)
313
+ };
314
+ signal?.addEventListener("abort", onAbort, { once: true });
315
+ if (signal?.aborted) onAbort();
316
+ else refreshFade();
317
+ });
318
+ },
319
+ async seek(seconds, signal) {
320
+ requireActive();
321
+ if (!Number.isFinite(seconds) || seconds < 0) throw new TypeError("Audio seek time must be non-negative.");
322
+ if (signal?.aborted) throw aborted();
323
+ if (audio.readyState < 1) await new Promise((resolve, reject) => {
324
+ const complete = (error) => {
325
+ clearTimeout(timer);
326
+ audio.removeEventListener("loadedmetadata", ready);
327
+ signal?.removeEventListener("abort", cancel);
328
+ pendingMetadata.delete(complete);
329
+ if (error === void 0) resolve();
330
+ else reject(error);
331
+ };
332
+ const ready = () => complete();
333
+ const cancel = () => complete(aborted());
334
+ const timer = setTimeout(() => complete(/* @__PURE__ */ new Error("Audio metadata did not become ready.")), 1e4);
335
+ pendingMetadata.add(complete);
336
+ audio.addEventListener("loadedmetadata", ready, { once: true });
337
+ signal?.addEventListener("abort", cancel, { once: true });
338
+ if (signal?.aborted) cancel();
339
+ else if (audio.readyState >= 1) ready();
340
+ });
341
+ requireActive();
342
+ if (signal?.aborted) throw aborted();
343
+ if (Number.isFinite(audio.duration) && seconds > audio.duration) throw new RangeError("Audio seek exceeds its duration.");
344
+ audio.currentTime = seconds;
345
+ refreshFade();
346
+ },
347
+ setGain(value) {
348
+ const normalized = playbackGain(value);
349
+ requireActive();
350
+ const previous = gain;
351
+ gain = normalized;
352
+ try {
353
+ if (playing) prepareAmplifier();
354
+ else writeVolume();
355
+ } catch (error) {
356
+ gain = previous;
357
+ writeVolume();
358
+ throw playbackError(error);
359
+ }
360
+ if (playing && amplifier) {
361
+ const run = generation;
362
+ let rejectResume;
363
+ const cancelled = new Promise((_resolve, reject) => {
364
+ rejectResume = reject;
365
+ });
366
+ pendingGain.add(rejectResume);
367
+ return Promise.race([amplifier.resume(), cancelled]).catch((cause) => {
368
+ if (run !== generation) throw cause;
369
+ audio.pause();
370
+ const error = playbackError(cause);
371
+ finish(error);
372
+ throw error;
373
+ }).finally(() => pendingGain.delete(rejectResume));
374
+ }
375
+ },
376
+ setMuted(value) {
377
+ if (typeof value !== "boolean") throw new TypeError("Audio muted must be a boolean.");
378
+ requireActive();
379
+ audio.muted = value;
380
+ },
381
+ setPlaybackRate(value) {
382
+ const normalized = rate(value);
383
+ requireActive();
384
+ audio.playbackRate = normalized;
385
+ refreshFade();
386
+ },
387
+ dispose() {
388
+ if (disposed) return;
389
+ disposed = true;
390
+ try {
391
+ audio.pause();
392
+ audio.currentTime = 0;
393
+ } catch {}
394
+ finish();
395
+ for (const complete of pendingMetadata) complete(aborted());
396
+ audio.removeEventListener("ended", onEnded);
397
+ audio.removeEventListener("error", onError);
398
+ for (const event of [
399
+ "durationchange",
400
+ "ratechange",
401
+ "seeked"
402
+ ]) audio.removeEventListener(event, refreshFade);
403
+ try {
404
+ audio.removeAttribute?.("src");
405
+ audio.load?.();
406
+ } finally {
407
+ try {
408
+ amplifier?.dispose();
409
+ } finally {
410
+ release();
411
+ }
412
+ }
413
+ }
414
+ });
415
+ }
1
416
  var block_definitions_default = {
2
417
  extensionName: "Asset Cache",
3
418
  blocks: [
@@ -278,7 +693,404 @@ var block_definitions_default = {
278
693
  ]
279
694
  };
280
695
  //#endregion
281
- //#region node_modules/.pnpm/@kubohiroya+turbowarp-named-data@0.1.0/node_modules/@kubohiroya/turbowarp-named-data/dist/composition.js
696
+ //#region node_modules/.pnpm/@kubohiroya+turbowarp-extension-data-editor@0.2.1_react@18.3.1/node_modules/@kubohiroya/turbowarp-extension-data-editor/dist/fixed-input-schema.js
697
+ var numberToken = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/u;
698
+ var invalidNumber = Object.freeze({
699
+ kind: "invalid",
700
+ code: "fixed-number-input-invalid"
701
+ });
702
+ /**
703
+ * Parse a NEW numeric draft according to NP-finite-new-input-v1.
704
+ * Never apply this to opening/no-op raw values or use it as an owner validator.
705
+ * Keep the validated token so negative zero, precision and exponent spelling survive.
706
+ */
707
+ function parseFixedNumberInput(input) {
708
+ if (typeof input === "number") {
709
+ if (!Number.isFinite(input)) return invalidNumber;
710
+ return Object.freeze({
711
+ kind: "valid",
712
+ token: Object.is(input, -0) ? "-0" : String(input),
713
+ value: input
714
+ });
715
+ }
716
+ if (typeof input !== "string") return invalidNumber;
717
+ const token = input.trim();
718
+ if (!numberToken.test(token)) return invalidNumber;
719
+ const value = Number(token);
720
+ return Number.isFinite(value) ? Object.freeze({
721
+ kind: "valid",
722
+ token,
723
+ value
724
+ }) : invalidNumber;
725
+ }
726
+ var block_editor_fixed_inputs_schema_default = {
727
+ $schema: "https://json-schema.org/draft/2020-12/schema",
728
+ $id: "https://kubohiroya.github.io/turbowarp-asset-cache/schemas/block-editor-fixed-inputs.schema.json",
729
+ $defs: {
730
+ "setTextStyle": {
731
+ "type": "object",
732
+ "properties": {
733
+ "PROPERTY": {
734
+ "type": "string",
735
+ "enum": [
736
+ "animation",
737
+ "font",
738
+ "color",
739
+ "width",
740
+ "align"
741
+ ]
742
+ },
743
+ "VALUE": { "type": "string" }
744
+ },
745
+ "additionalProperties": false
746
+ },
747
+ "animation": {
748
+ "type": "string",
749
+ "enum": [
750
+ "",
751
+ "none",
752
+ "type",
753
+ "rainbow",
754
+ "zoom",
755
+ "shake"
756
+ ]
757
+ },
758
+ "font": { "type": "string" },
759
+ "color": {
760
+ "type": "string",
761
+ "pattern": "^(?:|#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})$"
762
+ },
763
+ "width": {
764
+ "type": "string",
765
+ "$comment": "Empty/trimmed empty resets; otherwise normalizeTextStyleValue requires Number(value) finite and positive. Preserve the existing STRING grammar, including hexadecimal and leading plus."
766
+ },
767
+ "align": {
768
+ "type": "string",
769
+ "enum": [
770
+ "",
771
+ "left",
772
+ "center",
773
+ "right"
774
+ ]
775
+ },
776
+ "loadingCostumeAt": {
777
+ "type": "object",
778
+ "properties": { "INDEX": { "type": "number" } },
779
+ "additionalProperties": false
780
+ }
781
+ }
782
+ };
783
+ //#endregion
784
+ //#region src/fixed-input-schema.ts
785
+ /** Canonical owner schema fragments; no network resources or runtime providers. */
786
+ function fixedInputBundle(fragment, schema) {
787
+ const source = {
788
+ repository: "turbowarp-asset-cache",
789
+ path: "schemas/block-editor-fixed-inputs.schema.json",
790
+ symbol: {
791
+ "setTextStyle-PROPERTY": "#/$defs/setTextStyle/properties/PROPERTY",
792
+ "setTextStyle-VALUE": "#/$defs/setTextStyle/properties/VALUE",
793
+ "loadingCostumeAt-INDEX": "#/$defs/loadingCostumeAt/properties/INDEX"
794
+ }[fragment] ?? `#/$defs/${fragment}`
795
+ };
796
+ return {
797
+ identity: {
798
+ ownerId: "turbowarp-asset-cache",
799
+ contractId: "fixed-inputs",
800
+ revision: "1",
801
+ variantId: fragment
802
+ },
803
+ dialect: "2020-12",
804
+ root: {
805
+ retrievalUri: block_editor_fixed_inputs_schema_default.$id,
806
+ document: schema,
807
+ artifact: {
808
+ role: "canonical-schema",
809
+ source
810
+ }
811
+ },
812
+ resources: [],
813
+ provenance: {
814
+ canonicalSources: [
815
+ source,
816
+ {
817
+ repository: "turbowarp-asset-cache",
818
+ path: "src/text-style.ts",
819
+ symbol: "normalizeTextStyleProperty / normalizeTextStyleValue"
820
+ },
821
+ {
822
+ repository: "turbowarp-asset-cache",
823
+ path: "src/extension.ts",
824
+ symbol: "getInfo / loadingCostumeAt"
825
+ }
826
+ ],
827
+ generatedFiles: [],
828
+ referenceSchemas: [],
829
+ generationSources: []
830
+ }
831
+ };
832
+ }
833
+ var fixedSchemas = block_editor_fixed_inputs_schema_default.$defs;
834
+ //#endregion
835
+ //#region src/text-style.ts
836
+ var TEXT_RUNTIME_NAMESPACE = "text";
837
+ var TEXT_STYLE_RUNTIME_NAMESPACE = "textStyle";
838
+ var TEXT_STYLE_PROPERTIES = [
839
+ "animation",
840
+ "font",
841
+ "color",
842
+ "width",
843
+ "align"
844
+ ];
845
+ var DEFAULT_STAGE_WIDTH = 480;
846
+ var DEFAULT_FONT = "Handwriting";
847
+ var DEFAULT_COLOR = "#ffffff";
848
+ var DEFAULT_ALIGNMENT = "center";
849
+ var DEFAULT_OUTLINE_COLOR = "#000000";
850
+ function textRuntimeVariableName(name) {
851
+ return `${TEXT_RUNTIME_NAMESPACE}:${name}`;
852
+ }
853
+ function textStyleRuntimeVariableName(name, property) {
854
+ return `${TEXT_STYLE_RUNTIME_NAMESPACE}:${name}:${property}`;
855
+ }
856
+ function normalizeTextStyleProperty(value) {
857
+ const property = String(value ?? "").trim().toLowerCase();
858
+ if (TEXT_STYLE_PROPERTIES.includes(property)) return property;
859
+ throw new Error(`Unknown text style property: ${property || "(empty)"}`);
860
+ }
861
+ /**
862
+ * Normalize a DSL-facing style value for runtime-variable storage.
863
+ * An empty value intentionally resets the property to its default.
864
+ */
865
+ function normalizeTextStyleValue(property, value) {
866
+ const raw = String(value ?? "").trim();
867
+ if (!raw) return "";
868
+ switch (property) {
869
+ case "animation": {
870
+ const animation = raw.toLowerCase() === "typing" ? "type" : raw.toLowerCase();
871
+ if (animation === "none" || animation === "type" || animation === "rainbow" || animation === "zoom" || animation === "shake") return animation;
872
+ throw new Error(`Invalid text animation: ${raw}`);
873
+ }
874
+ case "font": return raw;
875
+ case "color": {
876
+ if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
877
+ const shortColor = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(raw);
878
+ if (shortColor) return `#${shortColor[1]}${shortColor[1]}${shortColor[2]}${shortColor[2]}${shortColor[3]}${shortColor[3]}`.toLowerCase();
879
+ throw new Error(`Invalid text color: ${raw}`);
880
+ }
881
+ case "width": {
882
+ const width = Number(raw);
883
+ if (!Number.isFinite(width) || width <= 0) throw new Error(`Text width must be a positive number: ${raw}`);
884
+ return String(width);
885
+ }
886
+ case "align": {
887
+ const align = raw.toLowerCase();
888
+ if (align === "left" || align === "center" || align === "right") return align;
889
+ throw new Error(`Invalid text alignment: ${raw}`);
890
+ }
891
+ }
892
+ }
893
+ function resolveTextStyle(name, stageWidth, getRuntimeVariable) {
894
+ const width = Number(stageWidth);
895
+ const defaults = {
896
+ animation: "none",
897
+ font: DEFAULT_FONT,
898
+ color: DEFAULT_COLOR,
899
+ width: Number.isFinite(width) && width > 0 ? width : DEFAULT_STAGE_WIDTH,
900
+ align: DEFAULT_ALIGNMENT
901
+ };
902
+ const read = (property) => normalizeTextStyleValue(property, getRuntimeVariable(textStyleRuntimeVariableName(name, property)));
903
+ const animation = read("animation");
904
+ const font = read("font");
905
+ const color = read("color");
906
+ const configuredWidth = read("width");
907
+ const align = read("align");
908
+ return {
909
+ animation: animation ? animation : defaults.animation,
910
+ font: font || defaults.font,
911
+ color: color || defaults.color,
912
+ width: configuredWidth ? Number(configuredWidth) : defaults.width,
913
+ align: align ? align : defaults.align
914
+ };
915
+ }
916
+ //#endregion
917
+ //#region src/fixed-input-editors.ts
918
+ var extensionId = "kubohiroyaassetcache";
919
+ var choices = (values) => values.map((value) => ({ value }));
920
+ var context = () => ({
921
+ consumerId: "turbowarp-asset-cache",
922
+ nowMs: 0,
923
+ settings: null
924
+ });
925
+ var string = (value) => {
926
+ if (typeof value !== "string") throw new TypeError("Expected a string literal.");
927
+ return value;
928
+ };
929
+ var issue = () => [{ message: "Invalid asset style value." }];
930
+ /** New edits preserve the candidate token; existing aliases are display projections only. */
931
+ function styleBinding(property) {
932
+ const values = property === "animation" ? [
933
+ "",
934
+ "none",
935
+ "type",
936
+ "rainbow",
937
+ "zoom",
938
+ "shake"
939
+ ] : property === "align" ? [
940
+ "",
941
+ "left",
942
+ "center",
943
+ "right"
944
+ ] : void 0;
945
+ return {
946
+ argument: "VALUE",
947
+ schemaBundle: fixedInputBundle(property, fixedSchemas[property]),
948
+ control: {
949
+ kind: values ? "enum" : property === "color" ? "color" : property === "width" ? "number" : "text",
950
+ reset: "",
951
+ ...values ? { choices: choices(values) } : {}
952
+ },
953
+ read(value) {
954
+ return normalizeTextStyleValue(property, string(value));
955
+ },
956
+ encode(value) {
957
+ const raw = string(value);
958
+ if (property !== "width" || raw === "") return raw;
959
+ normalizeTextStyleValue(property, raw);
960
+ return raw;
961
+ },
962
+ validateCandidate(value) {
963
+ try {
964
+ normalizeTextStyleValue(property, string(value));
965
+ return [];
966
+ } catch {
967
+ return issue();
968
+ }
969
+ }
970
+ };
971
+ }
972
+ var fixedInputEditors = [{
973
+ apiVersion: 1,
974
+ extensionId,
975
+ editorId: "text-style-fixed",
976
+ opcodes: [`${extensionId}_setTextStyle`],
977
+ contractId: "CP-asset-cache",
978
+ contractRevision: "1",
979
+ validationContext: context,
980
+ bindings: [{
981
+ argument: "PROPERTY",
982
+ schemaBundle: fixedInputBundle("setTextStyle-PROPERTY", fixedSchemas.setTextStyle.properties.PROPERTY),
983
+ control: {
984
+ kind: "enum",
985
+ choices: choices(TEXT_STYLE_PROPERTIES)
986
+ },
987
+ read(value) {
988
+ const raw = string(value);
989
+ try {
990
+ return normalizeTextStyleProperty(raw);
991
+ } catch {
992
+ return raw;
993
+ }
994
+ },
995
+ encode: (value) => string(value),
996
+ validateCandidate(value) {
997
+ try {
998
+ normalizeTextStyleProperty(value);
999
+ return [];
1000
+ } catch {
1001
+ return issue();
1002
+ }
1003
+ }
1004
+ }, {
1005
+ argument: "VALUE",
1006
+ schemaBundle: fixedInputBundle("setTextStyle-VALUE", fixedSchemas.setTextStyle.properties.VALUE),
1007
+ read: (value) => string(value),
1008
+ encode: (value) => string(value),
1009
+ validateCandidate: () => []
1010
+ }],
1011
+ dependencies: [{
1012
+ selector: "PROPERTY",
1013
+ dependents: ["VALUE"]
1014
+ }],
1015
+ branchPolicies: [{
1016
+ selector: "PROPERTY",
1017
+ normalize: {
1018
+ trim: true,
1019
+ lowercase: true
1020
+ },
1021
+ cases: TEXT_STYLE_PROPERTIES.map((property) => ({
1022
+ values: [property],
1023
+ activeDependents: ["VALUE"],
1024
+ bindings: [styleBinding(property)]
1025
+ }))
1026
+ }]
1027
+ }, {
1028
+ apiVersion: 1,
1029
+ extensionId,
1030
+ editorId: "loading-costume-index-fixed",
1031
+ opcodes: [`${extensionId}_loadingCostumeAt`],
1032
+ contractId: "turbowarp-asset-cache/loadingCostumeAt.INDEX",
1033
+ contractRevision: "1",
1034
+ validationContext: context,
1035
+ bindings: [{
1036
+ argument: "INDEX",
1037
+ schemaBundle: fixedInputBundle("loadingCostumeAt-INDEX", fixedSchemas.loadingCostumeAt.properties.INDEX),
1038
+ control: { kind: "number" },
1039
+ read(value) {
1040
+ if (typeof value !== "number" && typeof value !== "string" || typeof value === "string" && value.trim() === "") throw new TypeError("Non-numeric index.");
1041
+ const number = Number(value);
1042
+ if (!Number.isFinite(number)) throw new TypeError("Nonfinite index.");
1043
+ return number;
1044
+ },
1045
+ encode(value) {
1046
+ const parsed = parseFixedNumberInput(value);
1047
+ if (parsed.kind !== "valid") throw new TypeError("Invalid index.");
1048
+ return parsed.token;
1049
+ },
1050
+ validateCandidate(value) {
1051
+ return Number.isFinite(Number(value)) ? [] : [{ message: "Invalid index." }];
1052
+ }
1053
+ }]
1054
+ }];
1055
+ /** Per-instance, idempotent registration. Legacy hosts never acquire the fixed capability. */
1056
+ function fixedInputRegistration(scratch) {
1057
+ const host = scratch;
1058
+ let active;
1059
+ let removers = [];
1060
+ let disposed = false;
1061
+ const dispose = () => {
1062
+ host.vm?.runtime?.off?.("RUNTIME_DISPOSED", shutdown);
1063
+ for (const remove of removers.splice(0).reverse()) remove();
1064
+ active = void 0;
1065
+ };
1066
+ const shutdown = () => {
1067
+ disposed = true;
1068
+ dispose();
1069
+ };
1070
+ return {
1071
+ ensure(opcodes) {
1072
+ const api = host.gui?.blockEditors;
1073
+ if (disposed || api === active) return;
1074
+ dispose();
1075
+ if (api?.apiVersion !== 1 || typeof api.registerFixedInputEditor !== "function") return;
1076
+ try {
1077
+ for (const definition of fixedInputEditors) if (definition.opcodes.every((opcode) => opcodes.includes(opcode))) {
1078
+ const remove = api.registerFixedInputEditor.call(api, definition);
1079
+ if (typeof remove !== "function") throw new TypeError("Invalid fixed registration cleanup.");
1080
+ removers.push(remove);
1081
+ }
1082
+ active = api;
1083
+ host.vm?.runtime?.on?.("RUNTIME_DISPOSED", shutdown);
1084
+ } catch (error) {
1085
+ dispose();
1086
+ throw error;
1087
+ }
1088
+ },
1089
+ dispose: shutdown
1090
+ };
1091
+ }
1092
+ //#endregion
1093
+ //#region node_modules/.pnpm/@kubohiroya+turbowarp-named-data@0.3.1/node_modules/@kubohiroya/turbowarp-named-data/dist/composition.js
282
1094
  var NAMED_DATA_REGISTRY_SYMBOL_KEY = "@kubohiroya/turbowarp-named-data/registry";
283
1095
  var NAMED_DATA_REGISTRY_SYMBOL = Symbol.for(NAMED_DATA_REGISTRY_SYMBOL_KEY);
284
1096
  var NAMED_DATA_NAMESPACE_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u;
@@ -299,6 +1111,8 @@ var NAMED_DATA_REPRESENTATIONS = [
299
1111
  "markdown",
300
1112
  "raw"
301
1113
  ];
1114
+ var NAMED_DATA_SCHEMA_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u;
1115
+ var NAMED_DATA_SCHEMA_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;
302
1116
  var NAMED_DATA_ERROR_CODES = [
303
1117
  "NAMED_DATA_INVALID_REF",
304
1118
  "NAMED_DATA_INVALID_REGISTRY",
@@ -307,6 +1121,9 @@ var NAMED_DATA_ERROR_CODES = [
307
1121
  "NAMED_DATA_NOT_FOUND",
308
1122
  "NAMED_DATA_KIND_MISMATCH",
309
1123
  "NAMED_DATA_SCOPE_MISMATCH",
1124
+ "NAMED_DATA_SCHEMA_NOT_FOUND",
1125
+ "NAMED_DATA_SCHEMA_VERSION_MISMATCH",
1126
+ "NAMED_DATA_SCHEMA_UNRESOLVED",
310
1127
  "NAMED_DATA_REPRESENTATION_UNSUPPORTED",
311
1128
  "NAMED_DATA_INVALID_METADATA",
312
1129
  "NAMED_DATA_BODY_TOO_LARGE",
@@ -527,9 +1344,21 @@ function validateReferenceShape(reference, representation) {
527
1344
  if (!reference || typeof reference !== "object") throw invalidReference$1();
528
1345
  requireNamespace(reference.namespace);
529
1346
  if (typeof reference.name !== "string" || reference.name.length === 0 || reference.name.length > 256 || containsControlCharacter(reference.name) || !NAMED_DATA_KINDS.includes(reference.kind) || !NAMED_DATA_SCOPES.includes(reference.scope) || !NAMED_DATA_REPRESENTATIONS.includes(representation)) throw invalidReference$1();
1347
+ if (reference.schemaRef !== void 0) validateSchemaRef(reference.kind, reference.schemaRef);
530
1348
  }
531
1349
  function validateMetadata(metadata, reference, representation) {
532
1350
  if (metadata.representation !== representation || !isNativeRepresentation(metadata.reference.kind, metadata.nativeRepresentation) || metadata.reference.namespace !== reference.namespace || metadata.reference.name !== reference.name || metadata.reference.kind !== reference.kind || metadata.reference.scope !== reference.scope || typeof metadata.mediaType !== "string" || metadata.mediaType.length === 0 || typeof metadata.revision !== "string" || metadata.revision.length === 0 || typeof metadata.replayable !== "boolean" || metadata.byteLength !== void 0 && (!Number.isSafeInteger(metadata.byteLength) || metadata.byteLength < 0) || metadata.digest !== void 0 && !/^sha256-[A-Za-z0-9_-]+$/u.test(metadata.digest)) throw new NamedDataError("NAMED_DATA_INVALID_METADATA", "Provider returned invalid metadata.");
1351
+ const expectedSchema = reference.schemaRef;
1352
+ const actualSchema = metadata.schemaRef;
1353
+ if (actualSchema !== void 0) validateSchemaRef(metadata.reference.kind, actualSchema, "NAMED_DATA_INVALID_METADATA");
1354
+ if (metadata.reference.schemaRef !== void 0) validateSchemaRef(metadata.reference.kind, metadata.reference.schemaRef, "NAMED_DATA_INVALID_METADATA");
1355
+ if (!sameSchemaRef(expectedSchema, actualSchema) || !sameSchemaRef(expectedSchema, metadata.reference.schemaRef)) throw new NamedDataError("NAMED_DATA_SCHEMA_VERSION_MISMATCH", "Provider schemaRef does not match the requested reference.");
1356
+ }
1357
+ function validateSchemaRef(kind, schemaRef, errorCode = "NAMED_DATA_INVALID_REF") {
1358
+ if (kind !== "structured" || typeof schemaRef !== "object" || schemaRef === null || !NAMED_DATA_SCHEMA_ID_PATTERN.test(String(schemaRef.id ?? "")) || !NAMED_DATA_SCHEMA_VERSION_PATTERN.test(String(schemaRef.version ?? "")) || Object.keys(schemaRef).some((key) => key !== "id" && key !== "version")) throw new NamedDataError(errorCode, "Invalid schemaRef; schema references require a structured value, stable id, and full SemVer version.");
1359
+ }
1360
+ function sameSchemaRef(left, right) {
1361
+ return left === void 0 ? right === void 0 : right !== void 0 && left.id === right.id && left.version === right.version;
533
1362
  }
534
1363
  function isNativeRepresentation(kind, representation) {
535
1364
  if (kind === "structured") return representation === "json" || representation === "yaml";
@@ -566,7 +1395,10 @@ function configuredFlag$1(name) {
566
1395
  const value = globalThis.__TW_NAMED_DATA_FEATURE_FLAGS__?.[name];
567
1396
  return value === true || value === "true";
568
1397
  }
569
- Object.freeze({ NAMED_DATA_REGISTRY_MVP: configuredFlag$1("NAMED_DATA_REGISTRY_MVP") });
1398
+ Object.freeze({
1399
+ NAMED_DATA_REGISTRY_MVP: configuredFlag$1("NAMED_DATA_REGISTRY_MVP"),
1400
+ NAMED_DATA_SCHEMA_REF: configuredFlag$1("NAMED_DATA_SCHEMA_REF")
1401
+ });
570
1402
  //#endregion
571
1403
  //#region src/asset-cache-error.ts
572
1404
  var AssetManagerError = class extends Error {
@@ -948,88 +1780,6 @@ function throwIfNamedBodyAborted(signal) {
948
1780
  if (signal?.aborted) throw namedBodyAbortError();
949
1781
  }
950
1782
  //#endregion
951
- //#region src/text-style.ts
952
- var TEXT_RUNTIME_NAMESPACE = "text";
953
- var TEXT_STYLE_RUNTIME_NAMESPACE = "textStyle";
954
- var TEXT_STYLE_PROPERTIES = [
955
- "animation",
956
- "font",
957
- "color",
958
- "width",
959
- "align"
960
- ];
961
- var DEFAULT_STAGE_WIDTH = 480;
962
- var DEFAULT_FONT = "Handwriting";
963
- var DEFAULT_COLOR = "#ffffff";
964
- var DEFAULT_ALIGNMENT = "center";
965
- var DEFAULT_OUTLINE_COLOR = "#000000";
966
- function textRuntimeVariableName(name) {
967
- return `${TEXT_RUNTIME_NAMESPACE}:${name}`;
968
- }
969
- function textStyleRuntimeVariableName(name, property) {
970
- return `${TEXT_STYLE_RUNTIME_NAMESPACE}:${name}:${property}`;
971
- }
972
- function normalizeTextStyleProperty(value) {
973
- const property = String(value ?? "").trim().toLowerCase();
974
- if (TEXT_STYLE_PROPERTIES.includes(property)) return property;
975
- throw new Error(`Unknown text style property: ${property || "(empty)"}`);
976
- }
977
- /**
978
- * Normalize a DSL-facing style value for runtime-variable storage.
979
- * An empty value intentionally resets the property to its default.
980
- */
981
- function normalizeTextStyleValue(property, value) {
982
- const raw = String(value ?? "").trim();
983
- if (!raw) return "";
984
- switch (property) {
985
- case "animation": {
986
- const animation = raw.toLowerCase() === "typing" ? "type" : raw.toLowerCase();
987
- if (animation === "none" || animation === "type" || animation === "rainbow" || animation === "zoom" || animation === "shake") return animation;
988
- throw new Error(`Invalid text animation: ${raw}`);
989
- }
990
- case "font": return raw;
991
- case "color": {
992
- if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
993
- const shortColor = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(raw);
994
- if (shortColor) return `#${shortColor[1]}${shortColor[1]}${shortColor[2]}${shortColor[2]}${shortColor[3]}${shortColor[3]}`.toLowerCase();
995
- throw new Error(`Invalid text color: ${raw}`);
996
- }
997
- case "width": {
998
- const width = Number(raw);
999
- if (!Number.isFinite(width) || width <= 0) throw new Error(`Text width must be a positive number: ${raw}`);
1000
- return String(width);
1001
- }
1002
- case "align": {
1003
- const align = raw.toLowerCase();
1004
- if (align === "left" || align === "center" || align === "right") return align;
1005
- throw new Error(`Invalid text alignment: ${raw}`);
1006
- }
1007
- }
1008
- }
1009
- function resolveTextStyle(name, stageWidth, getRuntimeVariable) {
1010
- const width = Number(stageWidth);
1011
- const defaults = {
1012
- animation: "none",
1013
- font: DEFAULT_FONT,
1014
- color: DEFAULT_COLOR,
1015
- width: Number.isFinite(width) && width > 0 ? width : DEFAULT_STAGE_WIDTH,
1016
- align: DEFAULT_ALIGNMENT
1017
- };
1018
- const read = (property) => normalizeTextStyleValue(property, getRuntimeVariable(textStyleRuntimeVariableName(name, property)));
1019
- const animation = read("animation");
1020
- const font = read("font");
1021
- const color = read("color");
1022
- const configuredWidth = read("width");
1023
- const align = read("align");
1024
- return {
1025
- animation: animation ? animation : defaults.animation,
1026
- font: font || defaults.font,
1027
- color: color || defaults.color,
1028
- width: configuredWidth ? Number(configuredWidth) : defaults.width,
1029
- align: align ? align : defaults.align
1030
- };
1031
- }
1032
- //#endregion
1033
1783
  //#region src/extension.ts
1034
1784
  var EXTENSION_ID = "kubohiroyaassetcache";
1035
1785
  var EXTENSION_VERSION = "0.14.0";
@@ -1493,6 +2243,7 @@ var AssetManagerExtension = class {
1493
2243
  textAssets = /* @__PURE__ */ new Map();
1494
2244
  assetRegistry = /* @__PURE__ */ new Map();
1495
2245
  displayedAssets = /* @__PURE__ */ new Map();
2246
+ explicitAudio = /* @__PURE__ */ new WeakSet();
1496
2247
  playingAudio = /* @__PURE__ */ new Map();
1497
2248
  audioVoiceStops = /* @__PURE__ */ new WeakMap();
1498
2249
  registrationVersions = /* @__PURE__ */ new Map();
@@ -1534,6 +2285,7 @@ var AssetManagerExtension = class {
1534
2285
  this.unbindNamedDataRegistryLifecycle?.();
1535
2286
  this.unbindNamedDataRegistryLifecycle = void 0;
1536
2287
  };
2288
+ fixedEditors = fixedInputRegistration(Scratch);
1537
2289
  constructor(featureFlags = FEATURE_FLAGS) {
1538
2290
  this.featureFlags = Object.freeze({ ...featureFlags });
1539
2291
  if (this.featureFlags.NAMED_ASSET_BODY_PROVIDER) {
@@ -1600,6 +2352,8 @@ var AssetManagerExtension = class {
1600
2352
  return loadingCostumes[((Number.isFinite(numericIndex) ? Math.max(1, Math.trunc(numericIndex)) : 1) - 1) % loadingCostumes.length];
1601
2353
  }
1602
2354
  getInfo() {
2355
+ const blocks = blockDefinitions.map((block) => this.toScratchBlock(block));
2356
+ this.fixedEditors.ensure(blocks.map((block) => `${EXTENSION_ID}_${block.opcode}`));
1603
2357
  return {
1604
2358
  id: EXTENSION_ID,
1605
2359
  name: Scratch.translate(block_definitions_default.extensionName),
@@ -1608,9 +2362,13 @@ var AssetManagerExtension = class {
1608
2362
  color1: "#5b7cfa",
1609
2363
  color2: "#425ed8",
1610
2364
  color3: "#2f46aa",
1611
- blocks: blockDefinitions.map((block) => this.toScratchBlock(block))
2365
+ blocks
1612
2366
  };
1613
2367
  }
2368
+ /** Release only this optional editor registration; runtime data remains unchanged. */
2369
+ dispose() {
2370
+ this.fixedEditors.dispose();
2371
+ }
1614
2372
  /**
1615
2373
  * Exposes the stock extension registry through a host-neutral DOM image
1616
2374
  * resource contract for other unsandboxed extensions.
@@ -1858,6 +2616,50 @@ var AssetManagerExtension = class {
1858
2616
  await playback.started;
1859
2617
  return playback.voice;
1860
2618
  }
2619
+ async createAudioPlayback(source, options = {}, onDispose = () => {}) {
2620
+ validateAudioPlaybackOptions(options);
2621
+ if (!source || typeof source !== "object" || Array.isArray(source) || Object.keys(source).length !== 1 || Object.hasOwn(source, "asset") === Object.hasOwn(source, "url")) throw new TypeError("Audio requires exactly one asset or URL.");
2622
+ if (Object.hasOwn(source, "url")) {
2623
+ if (typeof source.url !== "string" || !/^https?:\/\/[^\s]+$/u.test(source.url)) throw new TypeError("Audio URL must use HTTP(S) without credentials.");
2624
+ const url = new URL(source.url);
2625
+ if (!url.hostname || url.username || url.password) throw new TypeError("Invalid audio URL.");
2626
+ return this.prepareBrowserAudio("", source.url, options, onDispose, "createAudioPlayback");
2627
+ }
2628
+ const name = normalizeName(source.asset);
2629
+ const version = this.registrationVersions.get(name);
2630
+ const resource = await this.resolveAudioBytes(name);
2631
+ if (!this.assetRegistry.has(name) || version !== this.registrationVersions.get(name)) throw new Error("Audio source was released or replaced while preparing.");
2632
+ const bytes = resource.bytes instanceof Uint8Array ? new Uint8Array(resource.bytes).buffer : resource.bytes;
2633
+ const url = URL.createObjectURL(new Blob([bytes], { type: resource.mimeType }));
2634
+ return this.prepareBrowserAudio(name, url, options, () => {
2635
+ URL.revokeObjectURL(url);
2636
+ onDispose();
2637
+ }, "createAudioPlayback");
2638
+ }
2639
+ prepareBrowserAudio(name, url, options, release, operation) {
2640
+ let audio;
2641
+ let controller;
2642
+ try {
2643
+ const directAudio = operation === "createAudioPlayback" && /^https?:/u.test(url);
2644
+ audio = directAudio ? new Audio() : new Audio(url);
2645
+ if (directAudio) {
2646
+ audio.crossOrigin = "anonymous";
2647
+ audio.src = url;
2648
+ }
2649
+ controller = createBrowserAudioPlayback(audio, options, () => {
2650
+ this.playingAudio.delete(audio);
2651
+ this.audioVoiceStops.delete(audio);
2652
+ release();
2653
+ }, (cause) => this.playbackError(name, cause, operation));
2654
+ } catch (cause) {
2655
+ release();
2656
+ throw this.playbackError(name, cause, operation);
2657
+ }
2658
+ if (operation === "createAudioPlayback") this.explicitAudio.add(audio);
2659
+ this.playingAudio.set(audio, name);
2660
+ this.audioVoiceStops.set(audio, () => controller.dispose());
2661
+ return controller;
2662
+ }
1861
2663
  stopSound(args) {
1862
2664
  const name = normalizeName(args.NAME);
1863
2665
  const kind = this.assetRegistry.get(name);
@@ -1867,12 +2669,12 @@ var AssetManagerExtension = class {
1867
2669
  if (!asset) throw this.assetNotRegistered("stopSound", name);
1868
2670
  asset.mimeType = normalizeMimeType(asset.mimeType, asset.url || name);
1869
2671
  if (!asset.mimeType.startsWith("audio/")) throw this.assetTypeMismatch("stopSound", name, "audio", `external/${this.externalMediaKind(asset)}`);
1870
- for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
2672
+ for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name && !this.explicitAudio.has(audio)) this.stopExternalAudio(audio);
1871
2673
  return;
1872
2674
  }
1873
2675
  if (kind === "sound") {
1874
2676
  const { target, sound, soundBank } = this.resolveSoundReference(name);
1875
- for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
2677
+ for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name && !this.explicitAudio.has(audio)) this.stopExternalAudio(audio);
1876
2678
  soundBank.stop(target, sound.soundId);
1877
2679
  return;
1878
2680
  }
@@ -2120,6 +2922,7 @@ var AssetManagerExtension = class {
2120
2922
  }
2121
2923
  unregisterAsset(name) {
2122
2924
  this.cancelRegistrations(name);
2925
+ for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
2123
2926
  this.invalidateDOMImageResources(name);
2124
2927
  const kind = this.assetRegistry.get(name);
2125
2928
  if (!kind) return;
@@ -2935,73 +3738,33 @@ var AssetManagerExtension = class {
2935
3738
  }
2936
3739
  startBrowserAudioVoice(name, bytes, mimeType, initialGain, operation) {
2937
3740
  const blobBytes = bytes instanceof Uint8Array ? new Uint8Array(bytes).buffer : bytes;
2938
- const objectUrl = URL.createObjectURL(new Blob([blobBytes], { type: mimeType }));
2939
- let audio;
2940
- try {
2941
- audio = new Audio(objectUrl);
2942
- audio.volume = initialGain;
2943
- } catch (error) {
2944
- URL.revokeObjectURL(objectUrl);
2945
- throw this.playbackError(name, error, operation);
2946
- }
3741
+ const url = URL.createObjectURL(new Blob([blobBytes], { type: mimeType }));
3742
+ const controller = this.prepareBrowserAudio(name, url, { gain: initialGain }, () => URL.revokeObjectURL(url), operation);
2947
3743
  let active = true;
2948
- let resolveEnded;
2949
- let rejectEnded;
2950
- const ended = new Promise((resolve, reject) => {
2951
- resolveEnded = resolve;
2952
- rejectEnded = reject;
2953
- });
2954
- ended.catch(() => {});
2955
- const cleanup = (error) => {
2956
- if (!active) return;
2957
- active = false;
2958
- audio.removeEventListener("ended", handleEnded);
2959
- audio.removeEventListener("error", handleError);
2960
- this.playingAudio.delete(audio);
2961
- this.audioVoiceStops.delete(audio);
2962
- URL.revokeObjectURL(objectUrl);
2963
- if (error === void 0) resolveEnded();
2964
- else rejectEnded(error);
2965
- };
2966
- const handleEnded = () => cleanup();
2967
- const handleError = () => cleanup(operation === "createAudioVoice" ? this.playbackError(name, /* @__PURE__ */ new Error("The browser audio element reported a playback error."), operation) : void 0);
2968
3744
  const stop = () => {
2969
3745
  if (!active) return;
2970
- try {
2971
- audio.pause();
2972
- audio.currentTime = 0;
2973
- } catch {}
2974
- cleanup();
3746
+ active = false;
3747
+ controller.dispose();
2975
3748
  };
2976
- const voice = Object.freeze({
2977
- ended,
2978
- setGain(value) {
2979
- const gain = normalizeAudioVoiceGain(value);
2980
- if (active) audio.volume = gain;
2981
- },
2982
- stop
3749
+ const started = controller.play();
3750
+ const ended = controller.ended.then(() => stop(), (error) => {
3751
+ stop();
3752
+ if (operation === "createAudioVoice") throw error;
2983
3753
  });
2984
- audio.addEventListener("ended", handleEnded, { once: true });
2985
- audio.addEventListener("error", handleError, { once: true });
2986
- this.playingAudio.set(audio, name);
2987
- this.audioVoiceStops.set(audio, stop);
2988
- let playResult;
2989
- try {
2990
- playResult = audio.play();
2991
- } catch (error) {
2992
- const playbackError = this.playbackError(name, error, operation);
2993
- cleanup(playbackError);
2994
- return {
2995
- voice,
2996
- started: Promise.reject(playbackError)
2997
- };
2998
- }
3754
+ ended.catch(() => {});
2999
3755
  return {
3000
- voice,
3001
- started: Promise.resolve(playResult).catch((error) => {
3002
- const playbackError = this.playbackError(name, error, operation);
3003
- cleanup(playbackError);
3004
- throw playbackError;
3756
+ voice: Object.freeze({
3757
+ ended,
3758
+ setGain(value) {
3759
+ const gain = normalizeAudioVoiceGain(value);
3760
+ if (active) controller.setGain(gain);
3761
+ },
3762
+ stop
3763
+ }),
3764
+ started: started.catch((error) => {
3765
+ const legacyStop = operation === "playSound" && !active && error instanceof Error && error.name === "AbortError";
3766
+ stop();
3767
+ if (!legacyStop) throw error;
3005
3768
  })
3006
3769
  };
3007
3770
  }
@@ -8041,6 +8804,8 @@ function createAssetManagerComposition(featureFlags, options = {}) {
8041
8804
  }
8042
8805
  });
8043
8806
  }
8807
+ const audioPlaybacks = /* @__PURE__ */ new Set();
8808
+ let audioGeneration = 0;
8044
8809
  function releaseAsset(name) {
8045
8810
  const owned = ownedName(name);
8046
8811
  if (!owned) return;
@@ -8221,7 +8986,13 @@ function createAssetManagerComposition(featureFlags, options = {}) {
8221
8986
  releaseAsset,
8222
8987
  releaseAll() {
8223
8988
  releaseAllDOMImageResources();
8989
+ audioGeneration++;
8224
8990
  const errors = [];
8991
+ for (const playback of audioPlaybacks) try {
8992
+ playback.dispose();
8993
+ } catch (error) {
8994
+ errors.push(error);
8995
+ }
8225
8996
  for (const name of [...ownedNames.keys()].reverse()) try {
8226
8997
  releaseAsset(name);
8227
8998
  } catch (error) {
@@ -8267,6 +9038,21 @@ function createAssetManagerComposition(featureFlags, options = {}) {
8267
9038
  createAudioVoice(name, options) {
8268
9039
  return extension.createAudioVoice(ownedName(name)?.internal ?? name, options);
8269
9040
  },
9041
+ async createAudioPlayback(source, options) {
9042
+ if (!source || typeof source !== "object" || Array.isArray(source) || Object.keys(source).length !== 1) throw new TypeError("Invalid audio source.");
9043
+ const generation = audioGeneration;
9044
+ const normalizedSource = Object.hasOwn(source, "asset") ? { asset: ownedName(source.asset)?.internal ?? source.asset } : source;
9045
+ let playback;
9046
+ playback = await extension.createAudioPlayback(normalizedSource, options, () => {
9047
+ if (playback) audioPlaybacks.delete(playback);
9048
+ });
9049
+ if (generation !== audioGeneration) {
9050
+ playback.dispose();
9051
+ throw new Error("Audio composition was released while preparing.");
9052
+ }
9053
+ audioPlaybacks.add(playback);
9054
+ return playback;
9055
+ },
8270
9056
  stopSound(name) {
8271
9057
  extension.stopSound({ NAME: ownedName(name)?.internal ?? name });
8272
9058
  },