@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.
@@ -7,6 +7,421 @@
7
7
  (function (Scratch) {
8
8
  'use strict';
9
9
 
10
+ //#region src/audio-output.ts
11
+ /** Media volume handles attenuation and fades; this shared graph adds amplification. */
12
+ var shared;
13
+ var maximumFloat = 34028234663852886e22;
14
+ function createAudioAmplifier(audio) {
15
+ if (typeof globalThis.AudioContext !== "function") throw new Error("Audio amplification requires Web Audio support.");
16
+ if (!shared || shared.context.state === "closed") shared = {
17
+ context: new AudioContext(),
18
+ users: 0
19
+ };
20
+ const owner = shared;
21
+ owner.users++;
22
+ let source;
23
+ let gain;
24
+ let released = false;
25
+ const dispose = () => {
26
+ if (released) return;
27
+ released = true;
28
+ source?.disconnect();
29
+ gain?.disconnect();
30
+ owner.users--;
31
+ if (owner.users === 0) {
32
+ if (shared === owner) shared = void 0;
33
+ owner.context.close().catch(() => {});
34
+ }
35
+ };
36
+ try {
37
+ source = owner.context.createMediaElementSource(audio);
38
+ gain = owner.context.createGain();
39
+ source.connect(gain);
40
+ gain.connect(owner.context.destination);
41
+ } catch (error) {
42
+ dispose();
43
+ throw error;
44
+ }
45
+ const node = gain;
46
+ return {
47
+ setGain(value) {
48
+ node.gain.value = Math.min(maximumFloat, Math.max(1, value));
49
+ },
50
+ resume() {
51
+ return owner.context.state === "running" ? Promise.resolve() : owner.context.resume();
52
+ },
53
+ dispose
54
+ };
55
+ }
56
+ //#endregion
57
+ //#region src/audio-playback.ts
58
+ function playbackGain(value) {
59
+ const gain = value === void 0 ? 1 : value;
60
+ if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0) throw new TypeError("Audio gain must be a finite non-negative number.");
61
+ return gain;
62
+ }
63
+ function rate(value) {
64
+ if (typeof value !== "number" || !Number.isFinite(value) || value < .25 || value > 4) throw new TypeError("Audio playback rate must be from 0.25 to 4.");
65
+ return value;
66
+ }
67
+ function validateAudioPlaybackOptions(options) {
68
+ if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => ![
69
+ "gain",
70
+ "muted",
71
+ "playbackRate",
72
+ "fadeIn",
73
+ "fadeOut"
74
+ ].includes(key))) throw new TypeError("Invalid audio playback options.");
75
+ playbackGain(options.gain);
76
+ if (options.muted !== void 0 && typeof options.muted !== "boolean") throw new TypeError("Audio muted must be a boolean.");
77
+ rate(options.playbackRate ?? 1);
78
+ fadeSeconds(options.fadeIn, "fadeIn");
79
+ fadeSeconds(options.fadeOut, "fadeOut");
80
+ }
81
+ function fadeSeconds(value, label) {
82
+ const seconds = value === void 0 ? 0 : value;
83
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) throw new TypeError(`Audio ${label} must be a finite non-negative number of seconds.`);
84
+ return seconds;
85
+ }
86
+ function aborted() {
87
+ const error = /* @__PURE__ */ new Error("Audio playback was cancelled.");
88
+ error.name = "AbortError";
89
+ return error;
90
+ }
91
+ /** One reusable native element; the caller owns source resolution and resource release. */
92
+ function createBrowserAudioPlayback(audio, options, release, playbackError) {
93
+ validateAudioPlaybackOptions(options);
94
+ let gain = playbackGain(options.gain);
95
+ audio.volume = Math.min(1, gain);
96
+ audio.muted = options.muted ?? false;
97
+ audio.playbackRate = options.playbackRate ?? 1;
98
+ audio.loop = false;
99
+ const defaultFadeIn = fadeSeconds(options.fadeIn, "fadeIn");
100
+ const defaultFadeOut = fadeSeconds(options.fadeOut, "fadeOut");
101
+ let activeFadeIn = defaultFadeIn;
102
+ let activeFadeOut = defaultFadeOut;
103
+ let envelope = 1;
104
+ let fadeStarted = 0;
105
+ let playing = false;
106
+ let fadeTimer;
107
+ let stopping;
108
+ let amplifier;
109
+ const pendingGain = /* @__PURE__ */ new Set();
110
+ let disposed = false;
111
+ let generation = 0;
112
+ let ended = Promise.resolve();
113
+ let settle;
114
+ let rejectStart;
115
+ let detachAbort;
116
+ const pendingMetadata = /* @__PURE__ */ new Set();
117
+ const requireActive = () => {
118
+ if (disposed) throw new Error("Audio playback is disposed.");
119
+ };
120
+ const clearFadeTimer = () => {
121
+ if (fadeTimer !== void 0) clearTimeout(fadeTimer);
122
+ fadeTimer = void 0;
123
+ };
124
+ const finish = (error) => {
125
+ playing = false;
126
+ for (const reject of pendingGain) reject(error ?? aborted());
127
+ pendingGain.clear();
128
+ clearFadeTimer();
129
+ const previousStop = stopping;
130
+ stopping = void 0;
131
+ previousStop?.detach();
132
+ previousStop?.reject(error ?? aborted());
133
+ generation++;
134
+ rejectStart?.(error ?? aborted());
135
+ rejectStart = void 0;
136
+ detachAbort?.();
137
+ detachAbort = void 0;
138
+ settle?.(error);
139
+ settle = void 0;
140
+ };
141
+ const writeVolume = () => {
142
+ audio.volume = Math.min(1, gain) * envelope;
143
+ amplifier?.setGain(gain);
144
+ };
145
+ const prepareAmplifier = () => {
146
+ if (!amplifier && gain > 1) amplifier = createAudioAmplifier(audio);
147
+ writeVolume();
148
+ };
149
+ const stopNow = () => {
150
+ const completed = stopping;
151
+ stopping = void 0;
152
+ completed?.detach();
153
+ audio.pause();
154
+ audio.loop = false;
155
+ audio.currentTime = 0;
156
+ finish();
157
+ envelope = 1;
158
+ writeVolume();
159
+ completed?.resolve();
160
+ };
161
+ const refreshFade = () => {
162
+ clearFadeTimer();
163
+ if (disposed || !playing && !stopping) return;
164
+ const now = performance.now();
165
+ let delay = 16;
166
+ if (stopping) {
167
+ const progress = Math.min(1, (now - stopping.started) / (stopping.seconds * 1e3));
168
+ envelope = stopping.from * (1 - progress);
169
+ writeVolume();
170
+ if (progress >= 1) {
171
+ stopNow();
172
+ return;
173
+ }
174
+ } else {
175
+ const fadeIn = activeFadeIn === 0 ? 1 : Math.min(1, (now - fadeStarted) / (activeFadeIn * 1e3));
176
+ const remaining = Number.isFinite(audio.duration) ? Math.max(0, audio.duration - audio.currentTime) / audio.playbackRate : Infinity;
177
+ const fadeOut = !audio.loop && activeFadeOut > 0 ? Math.min(1, remaining / activeFadeOut) : 1;
178
+ envelope = Math.max(0, Math.min(fadeIn, fadeOut));
179
+ writeVolume();
180
+ if (fadeIn >= 1) {
181
+ if (audio.loop || activeFadeOut === 0) return;
182
+ if (!Number.isFinite(remaining)) delay = 250;
183
+ else if (remaining > activeFadeOut) delay = Math.min(250, Math.max(16, (remaining - activeFadeOut) * 1e3));
184
+ }
185
+ }
186
+ fadeTimer = setTimeout(refreshFade, delay);
187
+ };
188
+ const onEnded = () => {
189
+ if (stopping) {
190
+ stopNow();
191
+ return;
192
+ }
193
+ finish();
194
+ if (activeFadeOut > 0 && !audio.loop) {
195
+ envelope = 0;
196
+ writeVolume();
197
+ }
198
+ };
199
+ const onError = () => {
200
+ audio.pause();
201
+ const error = playbackError(audio.error ?? /* @__PURE__ */ new Error("The browser audio element reported a playback error."));
202
+ finish(error);
203
+ for (const complete of pendingMetadata) complete(error);
204
+ };
205
+ audio.addEventListener("ended", onEnded);
206
+ audio.addEventListener("error", onError);
207
+ for (const event of [
208
+ "durationchange",
209
+ "ratechange",
210
+ "seeked"
211
+ ]) audio.addEventListener(event, refreshFade);
212
+ return Object.freeze({
213
+ get ended() {
214
+ return ended;
215
+ },
216
+ get currentTime() {
217
+ return audio.currentTime;
218
+ },
219
+ get paused() {
220
+ return audio.paused;
221
+ },
222
+ get loop() {
223
+ return audio.loop;
224
+ },
225
+ get gain() {
226
+ return gain;
227
+ },
228
+ get muted() {
229
+ return audio.muted;
230
+ },
231
+ get playbackRate() {
232
+ return audio.playbackRate;
233
+ },
234
+ async play(playOptions = {}) {
235
+ requireActive();
236
+ if (!playOptions || typeof playOptions !== "object" || Array.isArray(playOptions) || Object.keys(playOptions).some((key) => ![
237
+ "restart",
238
+ "loop",
239
+ "signal",
240
+ "fadeIn",
241
+ "fadeOut",
242
+ "gain"
243
+ ].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.");
244
+ const fadeIn = fadeSeconds(playOptions.fadeIn === void 0 ? defaultFadeIn : playOptions.fadeIn, "fadeIn");
245
+ const fadeOut = fadeSeconds(playOptions.fadeOut === void 0 ? defaultFadeOut : playOptions.fadeOut, "fadeOut");
246
+ const nextGain = playOptions.gain === void 0 ? gain : playbackGain(playOptions.gain);
247
+ const signal = playOptions.signal;
248
+ if (signal?.aborted) throw aborted();
249
+ finish();
250
+ const run = generation;
251
+ gain = nextGain;
252
+ if (playOptions.restart) audio.currentTime = 0;
253
+ audio.loop = playOptions.loop ?? audio.loop;
254
+ activeFadeIn = fadeIn;
255
+ activeFadeOut = fadeOut;
256
+ envelope = fadeIn > 0 ? 0 : 1;
257
+ writeVolume();
258
+ ended = new Promise((resolve, reject) => {
259
+ settle = (error) => error === void 0 ? resolve() : reject(error);
260
+ });
261
+ ended.catch(() => {});
262
+ const onAbort = () => {
263
+ audio.pause();
264
+ finish(aborted());
265
+ };
266
+ signal?.addEventListener("abort", onAbort, { once: true });
267
+ detachAbort = () => signal?.removeEventListener("abort", onAbort);
268
+ try {
269
+ prepareAmplifier();
270
+ const started = audio.play();
271
+ const ready = amplifier ? Promise.all([started, amplifier.resume()]) : Promise.resolve(started);
272
+ await Promise.race([ready, new Promise((_resolve, reject) => {
273
+ rejectStart = reject;
274
+ })]);
275
+ if (run !== generation) throw aborted();
276
+ rejectStart = void 0;
277
+ playing = true;
278
+ fadeStarted = performance.now();
279
+ refreshFade();
280
+ } catch (cause) {
281
+ if (run !== generation) throw cause;
282
+ audio.pause();
283
+ const error = playbackError(cause);
284
+ finish(error);
285
+ throw error;
286
+ }
287
+ },
288
+ pause() {
289
+ requireActive();
290
+ audio.pause();
291
+ finish();
292
+ },
293
+ stop(stopOptions = {}) {
294
+ requireActive();
295
+ if (!stopOptions || typeof stopOptions !== "object" || Array.isArray(stopOptions) || Object.keys(stopOptions).some((key) => !["fadeOut", "signal"].includes(key))) throw new TypeError("Invalid audio stop options.");
296
+ const seconds = fadeSeconds(stopOptions.fadeOut === void 0 ? activeFadeOut : stopOptions.fadeOut, "fadeOut");
297
+ const signal = stopOptions.signal;
298
+ if (signal?.aborted) throw aborted();
299
+ if (seconds === 0 || audio.paused || !playing) {
300
+ stopNow();
301
+ return;
302
+ }
303
+ const previous = stopping;
304
+ stopping = void 0;
305
+ previous?.detach();
306
+ previous?.reject(aborted());
307
+ return new Promise((resolve, reject) => {
308
+ const onAbort = () => {
309
+ const current = stopping;
310
+ stopping = void 0;
311
+ current?.detach();
312
+ stopNow();
313
+ reject(aborted());
314
+ };
315
+ stopping = {
316
+ started: performance.now(),
317
+ seconds,
318
+ from: envelope,
319
+ resolve,
320
+ reject,
321
+ detach: () => signal?.removeEventListener("abort", onAbort)
322
+ };
323
+ signal?.addEventListener("abort", onAbort, { once: true });
324
+ if (signal?.aborted) onAbort();
325
+ else refreshFade();
326
+ });
327
+ },
328
+ async seek(seconds, signal) {
329
+ requireActive();
330
+ if (!Number.isFinite(seconds) || seconds < 0) throw new TypeError("Audio seek time must be non-negative.");
331
+ if (signal?.aborted) throw aborted();
332
+ if (audio.readyState < 1) await new Promise((resolve, reject) => {
333
+ const complete = (error) => {
334
+ clearTimeout(timer);
335
+ audio.removeEventListener("loadedmetadata", ready);
336
+ signal?.removeEventListener("abort", cancel);
337
+ pendingMetadata.delete(complete);
338
+ if (error === void 0) resolve();
339
+ else reject(error);
340
+ };
341
+ const ready = () => complete();
342
+ const cancel = () => complete(aborted());
343
+ const timer = setTimeout(() => complete(/* @__PURE__ */ new Error("Audio metadata did not become ready.")), 1e4);
344
+ pendingMetadata.add(complete);
345
+ audio.addEventListener("loadedmetadata", ready, { once: true });
346
+ signal?.addEventListener("abort", cancel, { once: true });
347
+ if (signal?.aborted) cancel();
348
+ else if (audio.readyState >= 1) ready();
349
+ });
350
+ requireActive();
351
+ if (signal?.aborted) throw aborted();
352
+ if (Number.isFinite(audio.duration) && seconds > audio.duration) throw new RangeError("Audio seek exceeds its duration.");
353
+ audio.currentTime = seconds;
354
+ refreshFade();
355
+ },
356
+ setGain(value) {
357
+ const normalized = playbackGain(value);
358
+ requireActive();
359
+ const previous = gain;
360
+ gain = normalized;
361
+ try {
362
+ if (playing) prepareAmplifier();
363
+ else writeVolume();
364
+ } catch (error) {
365
+ gain = previous;
366
+ writeVolume();
367
+ throw playbackError(error);
368
+ }
369
+ if (playing && amplifier) {
370
+ const run = generation;
371
+ let rejectResume;
372
+ const cancelled = new Promise((_resolve, reject) => {
373
+ rejectResume = reject;
374
+ });
375
+ pendingGain.add(rejectResume);
376
+ return Promise.race([amplifier.resume(), cancelled]).catch((cause) => {
377
+ if (run !== generation) throw cause;
378
+ audio.pause();
379
+ const error = playbackError(cause);
380
+ finish(error);
381
+ throw error;
382
+ }).finally(() => pendingGain.delete(rejectResume));
383
+ }
384
+ },
385
+ setMuted(value) {
386
+ if (typeof value !== "boolean") throw new TypeError("Audio muted must be a boolean.");
387
+ requireActive();
388
+ audio.muted = value;
389
+ },
390
+ setPlaybackRate(value) {
391
+ const normalized = rate(value);
392
+ requireActive();
393
+ audio.playbackRate = normalized;
394
+ refreshFade();
395
+ },
396
+ dispose() {
397
+ if (disposed) return;
398
+ disposed = true;
399
+ try {
400
+ audio.pause();
401
+ audio.currentTime = 0;
402
+ } catch {}
403
+ finish();
404
+ for (const complete of pendingMetadata) complete(aborted());
405
+ audio.removeEventListener("ended", onEnded);
406
+ audio.removeEventListener("error", onError);
407
+ for (const event of [
408
+ "durationchange",
409
+ "ratechange",
410
+ "seeked"
411
+ ]) audio.removeEventListener(event, refreshFade);
412
+ try {
413
+ audio.removeAttribute?.("src");
414
+ audio.load?.();
415
+ } finally {
416
+ try {
417
+ amplifier?.dispose();
418
+ } finally {
419
+ release();
420
+ }
421
+ }
422
+ }
423
+ });
424
+ }
10
425
  var block_definitions_default = {
11
426
  extensionName: "Asset Cache",
12
427
  blocks: [
@@ -287,7 +702,404 @@
287
702
  ]
288
703
  };
289
704
  //#endregion
290
- //#region node_modules/.pnpm/@kubohiroya+turbowarp-named-data@0.1.0/node_modules/@kubohiroya/turbowarp-named-data/dist/composition.js
705
+ //#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
706
+ var numberToken = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/u;
707
+ var invalidNumber = Object.freeze({
708
+ kind: "invalid",
709
+ code: "fixed-number-input-invalid"
710
+ });
711
+ /**
712
+ * Parse a NEW numeric draft according to NP-finite-new-input-v1.
713
+ * Never apply this to opening/no-op raw values or use it as an owner validator.
714
+ * Keep the validated token so negative zero, precision and exponent spelling survive.
715
+ */
716
+ function parseFixedNumberInput(input) {
717
+ if (typeof input === "number") {
718
+ if (!Number.isFinite(input)) return invalidNumber;
719
+ return Object.freeze({
720
+ kind: "valid",
721
+ token: Object.is(input, -0) ? "-0" : String(input),
722
+ value: input
723
+ });
724
+ }
725
+ if (typeof input !== "string") return invalidNumber;
726
+ const token = input.trim();
727
+ if (!numberToken.test(token)) return invalidNumber;
728
+ const value = Number(token);
729
+ return Number.isFinite(value) ? Object.freeze({
730
+ kind: "valid",
731
+ token,
732
+ value
733
+ }) : invalidNumber;
734
+ }
735
+ var block_editor_fixed_inputs_schema_default = {
736
+ $schema: "https://json-schema.org/draft/2020-12/schema",
737
+ $id: "https://kubohiroya.github.io/turbowarp-asset-cache/schemas/block-editor-fixed-inputs.schema.json",
738
+ $defs: {
739
+ "setTextStyle": {
740
+ "type": "object",
741
+ "properties": {
742
+ "PROPERTY": {
743
+ "type": "string",
744
+ "enum": [
745
+ "animation",
746
+ "font",
747
+ "color",
748
+ "width",
749
+ "align"
750
+ ]
751
+ },
752
+ "VALUE": { "type": "string" }
753
+ },
754
+ "additionalProperties": false
755
+ },
756
+ "animation": {
757
+ "type": "string",
758
+ "enum": [
759
+ "",
760
+ "none",
761
+ "type",
762
+ "rainbow",
763
+ "zoom",
764
+ "shake"
765
+ ]
766
+ },
767
+ "font": { "type": "string" },
768
+ "color": {
769
+ "type": "string",
770
+ "pattern": "^(?:|#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})$"
771
+ },
772
+ "width": {
773
+ "type": "string",
774
+ "$comment": "Empty/trimmed empty resets; otherwise normalizeTextStyleValue requires Number(value) finite and positive. Preserve the existing STRING grammar, including hexadecimal and leading plus."
775
+ },
776
+ "align": {
777
+ "type": "string",
778
+ "enum": [
779
+ "",
780
+ "left",
781
+ "center",
782
+ "right"
783
+ ]
784
+ },
785
+ "loadingCostumeAt": {
786
+ "type": "object",
787
+ "properties": { "INDEX": { "type": "number" } },
788
+ "additionalProperties": false
789
+ }
790
+ }
791
+ };
792
+ //#endregion
793
+ //#region src/fixed-input-schema.ts
794
+ /** Canonical owner schema fragments; no network resources or runtime providers. */
795
+ function fixedInputBundle(fragment, schema) {
796
+ const source = {
797
+ repository: "turbowarp-asset-cache",
798
+ path: "schemas/block-editor-fixed-inputs.schema.json",
799
+ symbol: {
800
+ "setTextStyle-PROPERTY": "#/$defs/setTextStyle/properties/PROPERTY",
801
+ "setTextStyle-VALUE": "#/$defs/setTextStyle/properties/VALUE",
802
+ "loadingCostumeAt-INDEX": "#/$defs/loadingCostumeAt/properties/INDEX"
803
+ }[fragment] ?? `#/$defs/${fragment}`
804
+ };
805
+ return {
806
+ identity: {
807
+ ownerId: "turbowarp-asset-cache",
808
+ contractId: "fixed-inputs",
809
+ revision: "1",
810
+ variantId: fragment
811
+ },
812
+ dialect: "2020-12",
813
+ root: {
814
+ retrievalUri: block_editor_fixed_inputs_schema_default.$id,
815
+ document: schema,
816
+ artifact: {
817
+ role: "canonical-schema",
818
+ source
819
+ }
820
+ },
821
+ resources: [],
822
+ provenance: {
823
+ canonicalSources: [
824
+ source,
825
+ {
826
+ repository: "turbowarp-asset-cache",
827
+ path: "src/text-style.ts",
828
+ symbol: "normalizeTextStyleProperty / normalizeTextStyleValue"
829
+ },
830
+ {
831
+ repository: "turbowarp-asset-cache",
832
+ path: "src/extension.ts",
833
+ symbol: "getInfo / loadingCostumeAt"
834
+ }
835
+ ],
836
+ generatedFiles: [],
837
+ referenceSchemas: [],
838
+ generationSources: []
839
+ }
840
+ };
841
+ }
842
+ var fixedSchemas = block_editor_fixed_inputs_schema_default.$defs;
843
+ //#endregion
844
+ //#region src/text-style.ts
845
+ var TEXT_RUNTIME_NAMESPACE = "text";
846
+ var TEXT_STYLE_RUNTIME_NAMESPACE = "textStyle";
847
+ var TEXT_STYLE_PROPERTIES = [
848
+ "animation",
849
+ "font",
850
+ "color",
851
+ "width",
852
+ "align"
853
+ ];
854
+ var DEFAULT_STAGE_WIDTH = 480;
855
+ var DEFAULT_FONT = "Handwriting";
856
+ var DEFAULT_COLOR = "#ffffff";
857
+ var DEFAULT_ALIGNMENT = "center";
858
+ var DEFAULT_OUTLINE_COLOR = "#000000";
859
+ function textRuntimeVariableName(name) {
860
+ return `${TEXT_RUNTIME_NAMESPACE}:${name}`;
861
+ }
862
+ function textStyleRuntimeVariableName(name, property) {
863
+ return `${TEXT_STYLE_RUNTIME_NAMESPACE}:${name}:${property}`;
864
+ }
865
+ function normalizeTextStyleProperty(value) {
866
+ const property = String(value ?? "").trim().toLowerCase();
867
+ if (TEXT_STYLE_PROPERTIES.includes(property)) return property;
868
+ throw new Error(`Unknown text style property: ${property || "(empty)"}`);
869
+ }
870
+ /**
871
+ * Normalize a DSL-facing style value for runtime-variable storage.
872
+ * An empty value intentionally resets the property to its default.
873
+ */
874
+ function normalizeTextStyleValue(property, value) {
875
+ const raw = String(value ?? "").trim();
876
+ if (!raw) return "";
877
+ switch (property) {
878
+ case "animation": {
879
+ const animation = raw.toLowerCase() === "typing" ? "type" : raw.toLowerCase();
880
+ if (animation === "none" || animation === "type" || animation === "rainbow" || animation === "zoom" || animation === "shake") return animation;
881
+ throw new Error(`Invalid text animation: ${raw}`);
882
+ }
883
+ case "font": return raw;
884
+ case "color": {
885
+ if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
886
+ const shortColor = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(raw);
887
+ if (shortColor) return `#${shortColor[1]}${shortColor[1]}${shortColor[2]}${shortColor[2]}${shortColor[3]}${shortColor[3]}`.toLowerCase();
888
+ throw new Error(`Invalid text color: ${raw}`);
889
+ }
890
+ case "width": {
891
+ const width = Number(raw);
892
+ if (!Number.isFinite(width) || width <= 0) throw new Error(`Text width must be a positive number: ${raw}`);
893
+ return String(width);
894
+ }
895
+ case "align": {
896
+ const align = raw.toLowerCase();
897
+ if (align === "left" || align === "center" || align === "right") return align;
898
+ throw new Error(`Invalid text alignment: ${raw}`);
899
+ }
900
+ }
901
+ }
902
+ function resolveTextStyle(name, stageWidth, getRuntimeVariable) {
903
+ const width = Number(stageWidth);
904
+ const defaults = {
905
+ animation: "none",
906
+ font: DEFAULT_FONT,
907
+ color: DEFAULT_COLOR,
908
+ width: Number.isFinite(width) && width > 0 ? width : DEFAULT_STAGE_WIDTH,
909
+ align: DEFAULT_ALIGNMENT
910
+ };
911
+ const read = (property) => normalizeTextStyleValue(property, getRuntimeVariable(textStyleRuntimeVariableName(name, property)));
912
+ const animation = read("animation");
913
+ const font = read("font");
914
+ const color = read("color");
915
+ const configuredWidth = read("width");
916
+ const align = read("align");
917
+ return {
918
+ animation: animation ? animation : defaults.animation,
919
+ font: font || defaults.font,
920
+ color: color || defaults.color,
921
+ width: configuredWidth ? Number(configuredWidth) : defaults.width,
922
+ align: align ? align : defaults.align
923
+ };
924
+ }
925
+ //#endregion
926
+ //#region src/fixed-input-editors.ts
927
+ var extensionId = "kubohiroyaassetcache";
928
+ var choices = (values) => values.map((value) => ({ value }));
929
+ var context = () => ({
930
+ consumerId: "turbowarp-asset-cache",
931
+ nowMs: 0,
932
+ settings: null
933
+ });
934
+ var string = (value) => {
935
+ if (typeof value !== "string") throw new TypeError("Expected a string literal.");
936
+ return value;
937
+ };
938
+ var issue = () => [{ message: "Invalid asset style value." }];
939
+ /** New edits preserve the candidate token; existing aliases are display projections only. */
940
+ function styleBinding(property) {
941
+ const values = property === "animation" ? [
942
+ "",
943
+ "none",
944
+ "type",
945
+ "rainbow",
946
+ "zoom",
947
+ "shake"
948
+ ] : property === "align" ? [
949
+ "",
950
+ "left",
951
+ "center",
952
+ "right"
953
+ ] : void 0;
954
+ return {
955
+ argument: "VALUE",
956
+ schemaBundle: fixedInputBundle(property, fixedSchemas[property]),
957
+ control: {
958
+ kind: values ? "enum" : property === "color" ? "color" : property === "width" ? "number" : "text",
959
+ reset: "",
960
+ ...values ? { choices: choices(values) } : {}
961
+ },
962
+ read(value) {
963
+ return normalizeTextStyleValue(property, string(value));
964
+ },
965
+ encode(value) {
966
+ const raw = string(value);
967
+ if (property !== "width" || raw === "") return raw;
968
+ normalizeTextStyleValue(property, raw);
969
+ return raw;
970
+ },
971
+ validateCandidate(value) {
972
+ try {
973
+ normalizeTextStyleValue(property, string(value));
974
+ return [];
975
+ } catch {
976
+ return issue();
977
+ }
978
+ }
979
+ };
980
+ }
981
+ var fixedInputEditors = [{
982
+ apiVersion: 1,
983
+ extensionId,
984
+ editorId: "text-style-fixed",
985
+ opcodes: [`${extensionId}_setTextStyle`],
986
+ contractId: "CP-asset-cache",
987
+ contractRevision: "1",
988
+ validationContext: context,
989
+ bindings: [{
990
+ argument: "PROPERTY",
991
+ schemaBundle: fixedInputBundle("setTextStyle-PROPERTY", fixedSchemas.setTextStyle.properties.PROPERTY),
992
+ control: {
993
+ kind: "enum",
994
+ choices: choices(TEXT_STYLE_PROPERTIES)
995
+ },
996
+ read(value) {
997
+ const raw = string(value);
998
+ try {
999
+ return normalizeTextStyleProperty(raw);
1000
+ } catch {
1001
+ return raw;
1002
+ }
1003
+ },
1004
+ encode: (value) => string(value),
1005
+ validateCandidate(value) {
1006
+ try {
1007
+ normalizeTextStyleProperty(value);
1008
+ return [];
1009
+ } catch {
1010
+ return issue();
1011
+ }
1012
+ }
1013
+ }, {
1014
+ argument: "VALUE",
1015
+ schemaBundle: fixedInputBundle("setTextStyle-VALUE", fixedSchemas.setTextStyle.properties.VALUE),
1016
+ read: (value) => string(value),
1017
+ encode: (value) => string(value),
1018
+ validateCandidate: () => []
1019
+ }],
1020
+ dependencies: [{
1021
+ selector: "PROPERTY",
1022
+ dependents: ["VALUE"]
1023
+ }],
1024
+ branchPolicies: [{
1025
+ selector: "PROPERTY",
1026
+ normalize: {
1027
+ trim: true,
1028
+ lowercase: true
1029
+ },
1030
+ cases: TEXT_STYLE_PROPERTIES.map((property) => ({
1031
+ values: [property],
1032
+ activeDependents: ["VALUE"],
1033
+ bindings: [styleBinding(property)]
1034
+ }))
1035
+ }]
1036
+ }, {
1037
+ apiVersion: 1,
1038
+ extensionId,
1039
+ editorId: "loading-costume-index-fixed",
1040
+ opcodes: [`${extensionId}_loadingCostumeAt`],
1041
+ contractId: "turbowarp-asset-cache/loadingCostumeAt.INDEX",
1042
+ contractRevision: "1",
1043
+ validationContext: context,
1044
+ bindings: [{
1045
+ argument: "INDEX",
1046
+ schemaBundle: fixedInputBundle("loadingCostumeAt-INDEX", fixedSchemas.loadingCostumeAt.properties.INDEX),
1047
+ control: { kind: "number" },
1048
+ read(value) {
1049
+ if (typeof value !== "number" && typeof value !== "string" || typeof value === "string" && value.trim() === "") throw new TypeError("Non-numeric index.");
1050
+ const number = Number(value);
1051
+ if (!Number.isFinite(number)) throw new TypeError("Nonfinite index.");
1052
+ return number;
1053
+ },
1054
+ encode(value) {
1055
+ const parsed = parseFixedNumberInput(value);
1056
+ if (parsed.kind !== "valid") throw new TypeError("Invalid index.");
1057
+ return parsed.token;
1058
+ },
1059
+ validateCandidate(value) {
1060
+ return Number.isFinite(Number(value)) ? [] : [{ message: "Invalid index." }];
1061
+ }
1062
+ }]
1063
+ }];
1064
+ /** Per-instance, idempotent registration. Legacy hosts never acquire the fixed capability. */
1065
+ function fixedInputRegistration(scratch) {
1066
+ const host = scratch;
1067
+ let active;
1068
+ let removers = [];
1069
+ let disposed = false;
1070
+ const dispose = () => {
1071
+ host.vm?.runtime?.off?.("RUNTIME_DISPOSED", shutdown);
1072
+ for (const remove of removers.splice(0).reverse()) remove();
1073
+ active = void 0;
1074
+ };
1075
+ const shutdown = () => {
1076
+ disposed = true;
1077
+ dispose();
1078
+ };
1079
+ return {
1080
+ ensure(opcodes) {
1081
+ const api = host.gui?.blockEditors;
1082
+ if (disposed || api === active) return;
1083
+ dispose();
1084
+ if (api?.apiVersion !== 1 || typeof api.registerFixedInputEditor !== "function") return;
1085
+ try {
1086
+ for (const definition of fixedInputEditors) if (definition.opcodes.every((opcode) => opcodes.includes(opcode))) {
1087
+ const remove = api.registerFixedInputEditor.call(api, definition);
1088
+ if (typeof remove !== "function") throw new TypeError("Invalid fixed registration cleanup.");
1089
+ removers.push(remove);
1090
+ }
1091
+ active = api;
1092
+ host.vm?.runtime?.on?.("RUNTIME_DISPOSED", shutdown);
1093
+ } catch (error) {
1094
+ dispose();
1095
+ throw error;
1096
+ }
1097
+ },
1098
+ dispose: shutdown
1099
+ };
1100
+ }
1101
+ //#endregion
1102
+ //#region node_modules/.pnpm/@kubohiroya+turbowarp-named-data@0.3.1/node_modules/@kubohiroya/turbowarp-named-data/dist/composition.js
291
1103
  var NAMED_DATA_REGISTRY_SYMBOL_KEY = "@kubohiroya/turbowarp-named-data/registry";
292
1104
  var NAMED_DATA_REGISTRY_SYMBOL = Symbol.for(NAMED_DATA_REGISTRY_SYMBOL_KEY);
293
1105
  var NAMED_DATA_NAMESPACE_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u;
@@ -308,6 +1120,8 @@
308
1120
  "markdown",
309
1121
  "raw"
310
1122
  ];
1123
+ var NAMED_DATA_SCHEMA_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u;
1124
+ 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;
311
1125
  var NAMED_DATA_ERROR_CODES = [
312
1126
  "NAMED_DATA_INVALID_REF",
313
1127
  "NAMED_DATA_INVALID_REGISTRY",
@@ -316,6 +1130,9 @@
316
1130
  "NAMED_DATA_NOT_FOUND",
317
1131
  "NAMED_DATA_KIND_MISMATCH",
318
1132
  "NAMED_DATA_SCOPE_MISMATCH",
1133
+ "NAMED_DATA_SCHEMA_NOT_FOUND",
1134
+ "NAMED_DATA_SCHEMA_VERSION_MISMATCH",
1135
+ "NAMED_DATA_SCHEMA_UNRESOLVED",
319
1136
  "NAMED_DATA_REPRESENTATION_UNSUPPORTED",
320
1137
  "NAMED_DATA_INVALID_METADATA",
321
1138
  "NAMED_DATA_BODY_TOO_LARGE",
@@ -536,9 +1353,21 @@
536
1353
  if (!reference || typeof reference !== "object") throw invalidReference$1();
537
1354
  requireNamespace(reference.namespace);
538
1355
  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();
1356
+ if (reference.schemaRef !== void 0) validateSchemaRef(reference.kind, reference.schemaRef);
539
1357
  }
540
1358
  function validateMetadata(metadata, reference, representation) {
541
1359
  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.");
1360
+ const expectedSchema = reference.schemaRef;
1361
+ const actualSchema = metadata.schemaRef;
1362
+ if (actualSchema !== void 0) validateSchemaRef(metadata.reference.kind, actualSchema, "NAMED_DATA_INVALID_METADATA");
1363
+ if (metadata.reference.schemaRef !== void 0) validateSchemaRef(metadata.reference.kind, metadata.reference.schemaRef, "NAMED_DATA_INVALID_METADATA");
1364
+ 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.");
1365
+ }
1366
+ function validateSchemaRef(kind, schemaRef, errorCode = "NAMED_DATA_INVALID_REF") {
1367
+ 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.");
1368
+ }
1369
+ function sameSchemaRef(left, right) {
1370
+ return left === void 0 ? right === void 0 : right !== void 0 && left.id === right.id && left.version === right.version;
542
1371
  }
543
1372
  function isNativeRepresentation(kind, representation) {
544
1373
  if (kind === "structured") return representation === "json" || representation === "yaml";
@@ -575,7 +1404,10 @@
575
1404
  const value = globalThis.__TW_NAMED_DATA_FEATURE_FLAGS__?.[name];
576
1405
  return value === true || value === "true";
577
1406
  }
578
- Object.freeze({ NAMED_DATA_REGISTRY_MVP: configuredFlag$1("NAMED_DATA_REGISTRY_MVP") });
1407
+ Object.freeze({
1408
+ NAMED_DATA_REGISTRY_MVP: configuredFlag$1("NAMED_DATA_REGISTRY_MVP"),
1409
+ NAMED_DATA_SCHEMA_REF: configuredFlag$1("NAMED_DATA_SCHEMA_REF")
1410
+ });
579
1411
  //#endregion
580
1412
  //#region \0@oxc-project+runtime@0.148.0/helpers/esm/typeof.js
581
1413
  function _typeof(o) {
@@ -994,88 +1826,6 @@
994
1826
  if (signal?.aborted) throw namedBodyAbortError();
995
1827
  }
996
1828
  //#endregion
997
- //#region src/text-style.ts
998
- var TEXT_RUNTIME_NAMESPACE = "text";
999
- var TEXT_STYLE_RUNTIME_NAMESPACE = "textStyle";
1000
- var TEXT_STYLE_PROPERTIES = [
1001
- "animation",
1002
- "font",
1003
- "color",
1004
- "width",
1005
- "align"
1006
- ];
1007
- var DEFAULT_STAGE_WIDTH = 480;
1008
- var DEFAULT_FONT = "Handwriting";
1009
- var DEFAULT_COLOR = "#ffffff";
1010
- var DEFAULT_ALIGNMENT = "center";
1011
- var DEFAULT_OUTLINE_COLOR = "#000000";
1012
- function textRuntimeVariableName(name) {
1013
- return `${TEXT_RUNTIME_NAMESPACE}:${name}`;
1014
- }
1015
- function textStyleRuntimeVariableName(name, property) {
1016
- return `${TEXT_STYLE_RUNTIME_NAMESPACE}:${name}:${property}`;
1017
- }
1018
- function normalizeTextStyleProperty(value) {
1019
- const property = String(value ?? "").trim().toLowerCase();
1020
- if (TEXT_STYLE_PROPERTIES.includes(property)) return property;
1021
- throw new Error(`Unknown text style property: ${property || "(empty)"}`);
1022
- }
1023
- /**
1024
- * Normalize a DSL-facing style value for runtime-variable storage.
1025
- * An empty value intentionally resets the property to its default.
1026
- */
1027
- function normalizeTextStyleValue(property, value) {
1028
- const raw = String(value ?? "").trim();
1029
- if (!raw) return "";
1030
- switch (property) {
1031
- case "animation": {
1032
- const animation = raw.toLowerCase() === "typing" ? "type" : raw.toLowerCase();
1033
- if (animation === "none" || animation === "type" || animation === "rainbow" || animation === "zoom" || animation === "shake") return animation;
1034
- throw new Error(`Invalid text animation: ${raw}`);
1035
- }
1036
- case "font": return raw;
1037
- case "color": {
1038
- if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
1039
- const shortColor = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(raw);
1040
- if (shortColor) return `#${shortColor[1]}${shortColor[1]}${shortColor[2]}${shortColor[2]}${shortColor[3]}${shortColor[3]}`.toLowerCase();
1041
- throw new Error(`Invalid text color: ${raw}`);
1042
- }
1043
- case "width": {
1044
- const width = Number(raw);
1045
- if (!Number.isFinite(width) || width <= 0) throw new Error(`Text width must be a positive number: ${raw}`);
1046
- return String(width);
1047
- }
1048
- case "align": {
1049
- const align = raw.toLowerCase();
1050
- if (align === "left" || align === "center" || align === "right") return align;
1051
- throw new Error(`Invalid text alignment: ${raw}`);
1052
- }
1053
- }
1054
- }
1055
- function resolveTextStyle(name, stageWidth, getRuntimeVariable) {
1056
- const width = Number(stageWidth);
1057
- const defaults = {
1058
- animation: "none",
1059
- font: DEFAULT_FONT,
1060
- color: DEFAULT_COLOR,
1061
- width: Number.isFinite(width) && width > 0 ? width : DEFAULT_STAGE_WIDTH,
1062
- align: DEFAULT_ALIGNMENT
1063
- };
1064
- const read = (property) => normalizeTextStyleValue(property, getRuntimeVariable(textStyleRuntimeVariableName(name, property)));
1065
- const animation = read("animation");
1066
- const font = read("font");
1067
- const color = read("color");
1068
- const configuredWidth = read("width");
1069
- const align = read("align");
1070
- return {
1071
- animation: animation ? animation : defaults.animation,
1072
- font: font || defaults.font,
1073
- color: color || defaults.color,
1074
- width: configuredWidth ? Number(configuredWidth) : defaults.width,
1075
- align: align ? align : defaults.align
1076
- };
1077
- }
1078
- //#endregion
1079
1829
  //#region src/extension.ts
1080
1830
  var EXTENSION_ID = "kubohiroyaassetcache";
1081
1831
  var EXTENSION_VERSION = "0.14.0";
@@ -1540,6 +2290,7 @@
1540
2290
  _defineProperty(this, "textAssets", /* @__PURE__ */ new Map());
1541
2291
  _defineProperty(this, "assetRegistry", /* @__PURE__ */ new Map());
1542
2292
  _defineProperty(this, "displayedAssets", /* @__PURE__ */ new Map());
2293
+ _defineProperty(this, "explicitAudio", /* @__PURE__ */ new WeakSet());
1543
2294
  _defineProperty(this, "playingAudio", /* @__PURE__ */ new Map());
1544
2295
  _defineProperty(this, "audioVoiceStops", /* @__PURE__ */ new WeakMap());
1545
2296
  _defineProperty(this, "registrationVersions", /* @__PURE__ */ new Map());
@@ -1581,6 +2332,7 @@
1581
2332
  this.unbindNamedDataRegistryLifecycle?.();
1582
2333
  this.unbindNamedDataRegistryLifecycle = void 0;
1583
2334
  });
2335
+ _defineProperty(this, "fixedEditors", fixedInputRegistration(Scratch));
1584
2336
  this.featureFlags = Object.freeze({ ...featureFlags });
1585
2337
  if (this.featureFlags.NAMED_ASSET_BODY_PROVIDER) {
1586
2338
  this.namedBodyProviderValue = this.createNamedBodyProvider();
@@ -1646,6 +2398,8 @@
1646
2398
  return loadingCostumes[((Number.isFinite(numericIndex) ? Math.max(1, Math.trunc(numericIndex)) : 1) - 1) % loadingCostumes.length];
1647
2399
  }
1648
2400
  getInfo() {
2401
+ const blocks = blockDefinitions.map((block) => this.toScratchBlock(block));
2402
+ this.fixedEditors.ensure(blocks.map((block) => `${EXTENSION_ID}_${block.opcode}`));
1649
2403
  return {
1650
2404
  id: EXTENSION_ID,
1651
2405
  name: Scratch.translate(block_definitions_default.extensionName),
@@ -1654,9 +2408,13 @@
1654
2408
  color1: "#5b7cfa",
1655
2409
  color2: "#425ed8",
1656
2410
  color3: "#2f46aa",
1657
- blocks: blockDefinitions.map((block) => this.toScratchBlock(block))
2411
+ blocks
1658
2412
  };
1659
2413
  }
2414
+ /** Release only this optional editor registration; runtime data remains unchanged. */
2415
+ dispose() {
2416
+ this.fixedEditors.dispose();
2417
+ }
1660
2418
  /**
1661
2419
  * Exposes the stock extension registry through a host-neutral DOM image
1662
2420
  * resource contract for other unsandboxed extensions.
@@ -1904,6 +2662,50 @@
1904
2662
  await playback.started;
1905
2663
  return playback.voice;
1906
2664
  }
2665
+ async createAudioPlayback(source, options = {}, onDispose = () => {}) {
2666
+ validateAudioPlaybackOptions(options);
2667
+ 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.");
2668
+ if (Object.hasOwn(source, "url")) {
2669
+ if (typeof source.url !== "string" || !/^https?:\/\/[^\s]+$/u.test(source.url)) throw new TypeError("Audio URL must use HTTP(S) without credentials.");
2670
+ const url = new URL(source.url);
2671
+ if (!url.hostname || url.username || url.password) throw new TypeError("Invalid audio URL.");
2672
+ return this.prepareBrowserAudio("", source.url, options, onDispose, "createAudioPlayback");
2673
+ }
2674
+ const name = normalizeName(source.asset);
2675
+ const version = this.registrationVersions.get(name);
2676
+ const resource = await this.resolveAudioBytes(name);
2677
+ if (!this.assetRegistry.has(name) || version !== this.registrationVersions.get(name)) throw new Error("Audio source was released or replaced while preparing.");
2678
+ const bytes = resource.bytes instanceof Uint8Array ? new Uint8Array(resource.bytes).buffer : resource.bytes;
2679
+ const url = URL.createObjectURL(new Blob([bytes], { type: resource.mimeType }));
2680
+ return this.prepareBrowserAudio(name, url, options, () => {
2681
+ URL.revokeObjectURL(url);
2682
+ onDispose();
2683
+ }, "createAudioPlayback");
2684
+ }
2685
+ prepareBrowserAudio(name, url, options, release, operation) {
2686
+ let audio;
2687
+ let controller;
2688
+ try {
2689
+ const directAudio = operation === "createAudioPlayback" && /^https?:/u.test(url);
2690
+ audio = directAudio ? new Audio() : new Audio(url);
2691
+ if (directAudio) {
2692
+ audio.crossOrigin = "anonymous";
2693
+ audio.src = url;
2694
+ }
2695
+ controller = createBrowserAudioPlayback(audio, options, () => {
2696
+ this.playingAudio.delete(audio);
2697
+ this.audioVoiceStops.delete(audio);
2698
+ release();
2699
+ }, (cause) => this.playbackError(name, cause, operation));
2700
+ } catch (cause) {
2701
+ release();
2702
+ throw this.playbackError(name, cause, operation);
2703
+ }
2704
+ if (operation === "createAudioPlayback") this.explicitAudio.add(audio);
2705
+ this.playingAudio.set(audio, name);
2706
+ this.audioVoiceStops.set(audio, () => controller.dispose());
2707
+ return controller;
2708
+ }
1907
2709
  stopSound(args) {
1908
2710
  const name = normalizeName(args.NAME);
1909
2711
  const kind = this.assetRegistry.get(name);
@@ -1913,12 +2715,12 @@
1913
2715
  if (!asset) throw this.assetNotRegistered("stopSound", name);
1914
2716
  asset.mimeType = normalizeMimeType(asset.mimeType, asset.url || name);
1915
2717
  if (!asset.mimeType.startsWith("audio/")) throw this.assetTypeMismatch("stopSound", name, "audio", `external/${this.externalMediaKind(asset)}`);
1916
- for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
2718
+ for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name && !this.explicitAudio.has(audio)) this.stopExternalAudio(audio);
1917
2719
  return;
1918
2720
  }
1919
2721
  if (kind === "sound") {
1920
2722
  const { target, sound, soundBank } = this.resolveSoundReference(name);
1921
- for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
2723
+ for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name && !this.explicitAudio.has(audio)) this.stopExternalAudio(audio);
1922
2724
  soundBank.stop(target, sound.soundId);
1923
2725
  return;
1924
2726
  }
@@ -2166,6 +2968,7 @@
2166
2968
  }
2167
2969
  unregisterAsset(name) {
2168
2970
  this.cancelRegistrations(name);
2971
+ for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
2169
2972
  this.invalidateDOMImageResources(name);
2170
2973
  const kind = this.assetRegistry.get(name);
2171
2974
  if (!kind) return;
@@ -2981,73 +3784,33 @@
2981
3784
  }
2982
3785
  startBrowserAudioVoice(name, bytes, mimeType, initialGain, operation) {
2983
3786
  const blobBytes = bytes instanceof Uint8Array ? new Uint8Array(bytes).buffer : bytes;
2984
- const objectUrl = URL.createObjectURL(new Blob([blobBytes], { type: mimeType }));
2985
- let audio;
2986
- try {
2987
- audio = new Audio(objectUrl);
2988
- audio.volume = initialGain;
2989
- } catch (error) {
2990
- URL.revokeObjectURL(objectUrl);
2991
- throw this.playbackError(name, error, operation);
2992
- }
3787
+ const url = URL.createObjectURL(new Blob([blobBytes], { type: mimeType }));
3788
+ const controller = this.prepareBrowserAudio(name, url, { gain: initialGain }, () => URL.revokeObjectURL(url), operation);
2993
3789
  let active = true;
2994
- let resolveEnded;
2995
- let rejectEnded;
2996
- const ended = new Promise((resolve, reject) => {
2997
- resolveEnded = resolve;
2998
- rejectEnded = reject;
2999
- });
3000
- ended.catch(() => {});
3001
- const cleanup = (error) => {
3002
- if (!active) return;
3003
- active = false;
3004
- audio.removeEventListener("ended", handleEnded);
3005
- audio.removeEventListener("error", handleError);
3006
- this.playingAudio.delete(audio);
3007
- this.audioVoiceStops.delete(audio);
3008
- URL.revokeObjectURL(objectUrl);
3009
- if (error === void 0) resolveEnded();
3010
- else rejectEnded(error);
3011
- };
3012
- const handleEnded = () => cleanup();
3013
- const handleError = () => cleanup(operation === "createAudioVoice" ? this.playbackError(name, /* @__PURE__ */ new Error("The browser audio element reported a playback error."), operation) : void 0);
3014
3790
  const stop = () => {
3015
3791
  if (!active) return;
3016
- try {
3017
- audio.pause();
3018
- audio.currentTime = 0;
3019
- } catch {}
3020
- cleanup();
3792
+ active = false;
3793
+ controller.dispose();
3021
3794
  };
3022
- const voice = Object.freeze({
3023
- ended,
3024
- setGain(value) {
3025
- const gain = normalizeAudioVoiceGain(value);
3026
- if (active) audio.volume = gain;
3027
- },
3028
- stop
3795
+ const started = controller.play();
3796
+ const ended = controller.ended.then(() => stop(), (error) => {
3797
+ stop();
3798
+ if (operation === "createAudioVoice") throw error;
3029
3799
  });
3030
- audio.addEventListener("ended", handleEnded, { once: true });
3031
- audio.addEventListener("error", handleError, { once: true });
3032
- this.playingAudio.set(audio, name);
3033
- this.audioVoiceStops.set(audio, stop);
3034
- let playResult;
3035
- try {
3036
- playResult = audio.play();
3037
- } catch (error) {
3038
- const playbackError = this.playbackError(name, error, operation);
3039
- cleanup(playbackError);
3040
- return {
3041
- voice,
3042
- started: Promise.reject(playbackError)
3043
- };
3044
- }
3800
+ ended.catch(() => {});
3045
3801
  return {
3046
- voice,
3047
- started: Promise.resolve(playResult).catch((error) => {
3048
- const playbackError = this.playbackError(name, error, operation);
3049
- cleanup(playbackError);
3050
- throw playbackError;
3802
+ voice: Object.freeze({
3803
+ ended,
3804
+ setGain(value) {
3805
+ const gain = normalizeAudioVoiceGain(value);
3806
+ if (active) controller.setGain(gain);
3807
+ },
3808
+ stop
3809
+ }),
3810
+ started: started.catch((error) => {
3811
+ const legacyStop = operation === "playSound" && !active && error instanceof Error && error.name === "AbortError";
3812
+ stop();
3813
+ if (!legacyStop) throw error;
3051
3814
  })
3052
3815
  };
3053
3816
  }