@forgeax/engine-audio-webaudio 0.0.0-dev.8d955ade1c79

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.
Files changed (65) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +135 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/audio-bus-name-owner.test-d.d.ts +2 -0
  5. package/dist/__tests__/audio-bus-name-owner.test-d.d.ts.map +1 -0
  6. package/dist/__tests__/audio-loader-contract.test.d.ts +2 -0
  7. package/dist/__tests__/audio-loader-contract.test.d.ts.map +1 -0
  8. package/dist/__tests__/audio-local-artifacts.test.d.ts +2 -0
  9. package/dist/__tests__/audio-local-artifacts.test.d.ts.map +1 -0
  10. package/dist/__tests__/audio-webaudio.browser.test.d.ts +2 -0
  11. package/dist/__tests__/audio-webaudio.browser.test.d.ts.map +1 -0
  12. package/dist/__tests__/audio-webaudio.unit.test.d.ts +2 -0
  13. package/dist/__tests__/audio-webaudio.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/gain-automation.unit.test.d.ts +2 -0
  15. package/dist/__tests__/gain-automation.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/host-audio-consumer.unit.test.d.ts +2 -0
  17. package/dist/__tests__/host-audio-consumer.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/root-surface.unit.test.d.ts +2 -0
  19. package/dist/__tests__/root-surface.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/spatial-cleanup.test.d.ts +2 -0
  21. package/dist/__tests__/spatial-cleanup.test.d.ts.map +1 -0
  22. package/dist/__tests__/web-audio-engine-decode-recovery.browser.test.d.ts +2 -0
  23. package/dist/__tests__/web-audio-engine-decode-recovery.browser.test.d.ts.map +1 -0
  24. package/dist/__tests__/web-audio-engine-decode-recovery.unit.test.d.ts +2 -0
  25. package/dist/__tests__/web-audio-engine-decode-recovery.unit.test.d.ts.map +1 -0
  26. package/dist/audio-importer.d.ts +17 -0
  27. package/dist/audio-importer.d.ts.map +1 -0
  28. package/dist/audio-importer.mjs +90 -0
  29. package/dist/audio-importer.mjs.map +1 -0
  30. package/dist/audio-listener-sync-system.d.ts +42 -0
  31. package/dist/audio-listener-sync-system.d.ts.map +1 -0
  32. package/dist/audio-loader.d.ts +4 -0
  33. package/dist/audio-loader.d.ts.map +1 -0
  34. package/dist/clip-loader.d.ts +4 -0
  35. package/dist/clip-loader.d.ts.map +1 -0
  36. package/dist/host-audio-consumer.d.ts +11 -0
  37. package/dist/host-audio-consumer.d.ts.map +1 -0
  38. package/dist/index.d.ts +7 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.mjs +544 -0
  41. package/dist/index.mjs.map +1 -0
  42. package/dist/plugin.d.ts +3 -0
  43. package/dist/plugin.d.ts.map +1 -0
  44. package/dist/web-audio-engine.d.ts +43 -0
  45. package/dist/web-audio-engine.d.ts.map +1 -0
  46. package/package.json +67 -0
  47. package/src/__tests__/audio-bus-name-owner.test-d.ts +21 -0
  48. package/src/__tests__/audio-loader-contract.test.ts +103 -0
  49. package/src/__tests__/audio-local-artifacts.test.ts +34 -0
  50. package/src/__tests__/audio-webaudio.browser.test.ts +311 -0
  51. package/src/__tests__/audio-webaudio.unit.test.ts +3031 -0
  52. package/src/__tests__/gain-automation.unit.test.ts +173 -0
  53. package/src/__tests__/host-audio-consumer.unit.test.ts +195 -0
  54. package/src/__tests__/root-surface.unit.test.ts +8 -0
  55. package/src/__tests__/spatial-cleanup.test.ts +85 -0
  56. package/src/__tests__/web-audio-engine-decode-recovery.browser.test.ts +125 -0
  57. package/src/__tests__/web-audio-engine-decode-recovery.unit.test.ts +93 -0
  58. package/src/audio-importer.ts +149 -0
  59. package/src/audio-listener-sync-system.ts +98 -0
  60. package/src/audio-loader.ts +55 -0
  61. package/src/clip-loader.ts +53 -0
  62. package/src/host-audio-consumer.ts +168 -0
  63. package/src/index.ts +31 -0
  64. package/src/plugin.ts +15 -0
  65. package/src/web-audio-engine.ts +411 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,544 @@
1
+ import { AudioError, AUDIO_ERROR_HINTS, createAudioIntentBackend } from '@forgeax/engine-audio';
2
+ export { AUDIO_ENGINE_RESOURCE_KEY } from '@forgeax/engine-audio';
3
+ import { mat4, vec3 } from '@forgeax/engine-math';
4
+ import { AssetError, err, ok, AudioError as AudioError$1 } from '@forgeax/engine-types';
5
+
6
+ // src/index.ts
7
+ function syncListenerFromWorldMatrix(listener, worldMatrix) {
8
+ const m = worldMatrix;
9
+ const position = mat4.getTranslation(vec3.create(), m);
10
+ listener.positionX.value = position[0];
11
+ listener.positionY.value = position[1];
12
+ listener.positionZ.value = position[2];
13
+ const forward = mat4.getForward(vec3.create(), m);
14
+ listener.forwardX.value = forward[0];
15
+ listener.forwardY.value = forward[1];
16
+ listener.forwardZ.value = forward[2];
17
+ const up = mat4.getUp(vec3.create(), m);
18
+ listener.upX.value = up[0];
19
+ listener.upY.value = up[1];
20
+ listener.upZ.value = up[2];
21
+ }
22
+ function audioListenerSyncSystem(ctx, worldMatrix) {
23
+ syncListenerFromWorldMatrix(ctx.listener, worldMatrix);
24
+ }
25
+ async function decodeAudioClipBytes(guid, bytes, mediaType) {
26
+ if (mediaType.length <= "audio/".length || bytes.byteLength === 0) {
27
+ return err(
28
+ new AudioError({
29
+ code: "decode-failed",
30
+ expected: `non-empty audio mediaType and source bytes for GUID ${guid}`,
31
+ hint: "verify the audio artifact mediaType and recook the source bytes",
32
+ detail: {
33
+ code: "decode-failed",
34
+ reason: "audio mediaType or source bytes are empty"
35
+ }
36
+ })
37
+ );
38
+ }
39
+ try {
40
+ return ok({ kind: "audio", sourceKey: guid, mediaType, bytes: bytes.slice() });
41
+ } catch (e) {
42
+ return err(
43
+ new AudioError({
44
+ code: "decode-failed",
45
+ expected: `decodable audio artifact bytes for GUID ${guid}`,
46
+ hint: "verify the audio artifact mediaType and browser-supported codec",
47
+ detail: {
48
+ code: "decode-failed",
49
+ reason: e instanceof Error ? e.message : "audio artifact decode failed"
50
+ }
51
+ })
52
+ );
53
+ }
54
+ }
55
+
56
+ // src/audio-loader.ts
57
+ var audioLoader = {
58
+ kind: "audio",
59
+ loadPack(input) {
60
+ if (input.kind !== "audio") {
61
+ return Promise.resolve({
62
+ ok: false,
63
+ error: new AssetError({
64
+ code: "asset-parse-failed",
65
+ expected: "Pack v2 loader input with kind 'audio'",
66
+ hint: "pass the asset-local audio envelope to the audio loader",
67
+ detail: { sourcePath: input.guid }
68
+ })
69
+ });
70
+ }
71
+ const source = input.artifacts.source;
72
+ const payloadMediaType = typeof input.payload.mediaType === "string" ? input.payload.mediaType : void 0;
73
+ if (source === void 0 || !source.descriptor.mediaType.startsWith("audio/") || payloadMediaType !== source.descriptor.mediaType) {
74
+ return Promise.resolve({
75
+ ok: false,
76
+ error: {
77
+ code: "asset-artifact-media-unsupported",
78
+ expected: "an asset-local source artifact with an audio/* mediaType",
79
+ hint: "declare the audio source artifact with a supported mediaType and re-cook",
80
+ detail: {
81
+ guid: input.guid,
82
+ artifactKey: "source",
83
+ observed: payloadMediaType ?? source?.descriptor.mediaType ?? "missing",
84
+ expected: "payload.mediaType matching the audio/* source artifact"
85
+ }
86
+ }
87
+ });
88
+ }
89
+ return decodeAudioClipBytes(input.guid, source.bytes, payloadMediaType);
90
+ },
91
+ async load() {
92
+ return {
93
+ ok: false,
94
+ error: new AssetError({
95
+ code: "asset-parse-failed",
96
+ expected: "Pack v2 audio input with an asset-local source artifact",
97
+ hint: "re-cook the audio source into the Pack v2 asset envelope",
98
+ detail: { sourcePath: "audio-loader-input" }
99
+ })
100
+ };
101
+ }
102
+ };
103
+ var GESTURE_EVENTS = ["click", "keydown", "touchstart"];
104
+ var GAIN_TRANSITION_SECONDS = 0.01;
105
+ var WebAudioEngine = class {
106
+ ctx;
107
+ closed = false;
108
+ masterGain;
109
+ sfxGain;
110
+ musicGain;
111
+ sources = /* @__PURE__ */ new Map();
112
+ gestureListening = false;
113
+ resumeInFlight;
114
+ gestureResumeHandler;
115
+ lastError = null;
116
+ // Per-bus previous-volume cache for mute/unmute restore (D-5).
117
+ busVolumes = /* @__PURE__ */ new Map([
118
+ ["sfx", 1],
119
+ ["music", 1]
120
+ ]);
121
+ busMuted = /* @__PURE__ */ new Map([
122
+ ["sfx", false],
123
+ ["music", false]
124
+ ]);
125
+ constructor() {
126
+ this.gestureResumeHandler = () => {
127
+ void this.tryResume();
128
+ };
129
+ }
130
+ /**
131
+ * Returns the Web Audio AudioListener for spatialization (D-2).
132
+ * Triggers lazy ensureContext() on first access.
133
+ * Returns undefined if the context could not be created or is closed.
134
+ */
135
+ get listener() {
136
+ return this.ensureContext().listener;
137
+ }
138
+ setListenerPose(pose) {
139
+ const listener = this.ensureContext().listener;
140
+ listener.positionX.value = pose.positionX;
141
+ listener.positionY.value = pose.positionY;
142
+ listener.positionZ.value = pose.positionZ;
143
+ listener.forwardX.value = pose.forwardX;
144
+ listener.forwardY.value = pose.forwardY;
145
+ listener.forwardZ.value = pose.forwardZ;
146
+ listener.upX.value = pose.upX;
147
+ listener.upY.value = pose.upY;
148
+ listener.upZ.value = pose.upZ;
149
+ }
150
+ // -----------------------------------------------------------------------
151
+ // ensureContext -- lazy AudioContext + bus topology creation
152
+ // -----------------------------------------------------------------------
153
+ ensureContext() {
154
+ if (this.ctx) {
155
+ this.registerGestureListener(this.ctx);
156
+ return this.ctx;
157
+ }
158
+ const ctx = new AudioContext();
159
+ const master = ctx.createGain();
160
+ master.gain.value = 1;
161
+ master.connect(ctx.destination);
162
+ const sfx = ctx.createGain();
163
+ sfx.gain.value = 1;
164
+ sfx.connect(master);
165
+ const music = ctx.createGain();
166
+ music.gain.value = 1;
167
+ music.connect(master);
168
+ this.ctx = ctx;
169
+ this.masterGain = master;
170
+ this.sfxGain = sfx;
171
+ this.musicGain = music;
172
+ this.registerGestureListener(ctx);
173
+ return ctx;
174
+ }
175
+ // -----------------------------------------------------------------------
176
+ // Gesture listener -- D-3 bounded resume retry on user gesture
177
+ // -----------------------------------------------------------------------
178
+ registerGestureListener(ctx) {
179
+ if (ctx.state !== "suspended") {
180
+ return;
181
+ }
182
+ if (this.gestureListening) {
183
+ return;
184
+ }
185
+ this.gestureListening = true;
186
+ for (const event of GESTURE_EVENTS) {
187
+ document.addEventListener(event, this.gestureResumeHandler, { once: true });
188
+ }
189
+ }
190
+ removeGestureListener() {
191
+ if (!this.gestureListening) {
192
+ return;
193
+ }
194
+ this.gestureListening = false;
195
+ for (const event of GESTURE_EVENTS) {
196
+ document.removeEventListener(event, this.gestureResumeHandler);
197
+ }
198
+ }
199
+ async tryResume() {
200
+ const ctx = this.ctx;
201
+ if (!ctx || this.closed || ctx.state !== "suspended") return;
202
+ if (this.resumeInFlight !== void 0) return this.resumeInFlight;
203
+ const attempt = (async () => {
204
+ try {
205
+ await ctx.resume();
206
+ } catch {
207
+ } finally {
208
+ if (!this.closed && this.ctx === ctx) {
209
+ if (ctx.state === "running") {
210
+ this.lastError = null;
211
+ this.removeGestureListener();
212
+ } else if (ctx.state === "suspended") {
213
+ this.recordResumeFailure();
214
+ this.rearmGestureListener(ctx);
215
+ } else {
216
+ this.removeGestureListener();
217
+ }
218
+ }
219
+ this.resumeInFlight = void 0;
220
+ }
221
+ })();
222
+ this.resumeInFlight = attempt;
223
+ return attempt;
224
+ }
225
+ recordResumeFailure() {
226
+ this.lastError = new AudioError({
227
+ code: "context-suspended",
228
+ expected: "AudioContext.resume() to make the existing context running",
229
+ hint: AUDIO_ERROR_HINTS["context-suspended"],
230
+ detail: { code: "context-suspended" }
231
+ });
232
+ }
233
+ recordDecodeFailure(sourceKey, cause) {
234
+ this.lastError = new AudioError({
235
+ code: "decode-failed",
236
+ expected: `browser-decodable audio bytes for sourceKey ${sourceKey}`,
237
+ hint: AUDIO_ERROR_HINTS["decode-failed"],
238
+ detail: {
239
+ code: "decode-failed",
240
+ reason: cause instanceof Error ? cause.message : String(cause)
241
+ }
242
+ });
243
+ }
244
+ rearmGestureListener(ctx) {
245
+ this.removeGestureListener();
246
+ this.registerGestureListener(ctx);
247
+ }
248
+ // -----------------------------------------------------------------------
249
+ // AudioBackend implementation
250
+ // -----------------------------------------------------------------------
251
+ decode(bytes) {
252
+ return this.ensureContext().decodeAudioData(bytes.slice().buffer);
253
+ }
254
+ play(entityId, clip, opts) {
255
+ if ("kind" in clip) {
256
+ void this.decode(clip.bytes).then(
257
+ (buffer) => {
258
+ if (this.lastError?.code === "decode-failed") {
259
+ this.lastError = null;
260
+ }
261
+ this.play(entityId, buffer, opts);
262
+ },
263
+ (cause) => this.recordDecodeFailure(clip.sourceKey, cause)
264
+ );
265
+ return;
266
+ }
267
+ const clipBuffer = clip;
268
+ if (this.sources.has(entityId)) {
269
+ this.stop(entityId);
270
+ }
271
+ const ctx = this.ensureContext();
272
+ const sourceGain = ctx.createGain();
273
+ sourceGain.gain.value = opts.volume;
274
+ let panner;
275
+ if (opts.spatialBlend > 0) {
276
+ panner = ctx.createPanner();
277
+ panner.panningModel = "equalpower";
278
+ }
279
+ const busGain = this.busGainFor(opts.bus);
280
+ if (!busGain) return;
281
+ if (panner) {
282
+ sourceGain.connect(panner);
283
+ panner.connect(busGain);
284
+ } else {
285
+ sourceGain.connect(busGain);
286
+ }
287
+ const node = ctx.createBufferSource();
288
+ node.buffer = clipBuffer;
289
+ node.loop = opts.loop;
290
+ node.connect(sourceGain);
291
+ node.start();
292
+ this.sources.set(entityId, { node, sourceGain, panner, bus: opts.bus });
293
+ if (!opts.loop) {
294
+ node.onended = () => {
295
+ const current = this.sources.get(entityId);
296
+ if (current?.node === node) {
297
+ this.stop(entityId);
298
+ }
299
+ };
300
+ }
301
+ }
302
+ stop(entityId) {
303
+ const source = this.sources.get(entityId);
304
+ if (!source) return;
305
+ try {
306
+ source.node.stop();
307
+ } catch {
308
+ }
309
+ source.node.disconnect();
310
+ source.sourceGain.disconnect();
311
+ source.panner?.disconnect();
312
+ this.sources.delete(entityId);
313
+ }
314
+ setVolume(entityId, volume) {
315
+ const source = this.sources.get(entityId);
316
+ if (!source) return;
317
+ this.scheduleGainTransition(source.sourceGain, volume);
318
+ }
319
+ setBusVolume(busName, volume) {
320
+ const gain = this.busGainFor(busName);
321
+ if (!gain) return;
322
+ if (!this.scheduleGainTransition(gain, volume)) return;
323
+ this.busVolumes.set(busName, volume);
324
+ if (this.busMuted.get(busName)) {
325
+ this.busMuted.set(busName, false);
326
+ }
327
+ }
328
+ setBusMute(busName, muted) {
329
+ const gain = this.busGainFor(busName);
330
+ if (!gain) return;
331
+ const target = muted ? 0 : this.busVolumes.get(busName) ?? 1;
332
+ if (!this.scheduleGainTransition(gain, target)) return;
333
+ this.busMuted.set(busName, muted);
334
+ }
335
+ getState() {
336
+ if (this.closed) {
337
+ return { contextState: "closed", activeSourceCount: 0, lastError: null };
338
+ }
339
+ const contextState = this.ctx?.state === "closed" ? "closed" : this.ctx?.state === "running" ? "running" : "suspended";
340
+ return {
341
+ contextState,
342
+ activeSourceCount: this.sources.size,
343
+ lastError: this.lastError
344
+ };
345
+ }
346
+ getActiveSourceCount() {
347
+ return this.sources.size;
348
+ }
349
+ destroy() {
350
+ if (this.closed) return;
351
+ this.closed = true;
352
+ for (const entityId of this.sources.keys()) {
353
+ this.stop(entityId);
354
+ }
355
+ if (this.sfxGain) {
356
+ this.sfxGain.disconnect();
357
+ this.sfxGain = void 0;
358
+ }
359
+ if (this.musicGain) {
360
+ this.musicGain.disconnect();
361
+ this.musicGain = void 0;
362
+ }
363
+ if (this.masterGain) {
364
+ this.masterGain.disconnect();
365
+ this.masterGain = void 0;
366
+ }
367
+ this.removeGestureListener();
368
+ if (this.ctx) {
369
+ void this.ctx.close();
370
+ this.ctx = void 0;
371
+ }
372
+ }
373
+ // -----------------------------------------------------------------------
374
+ // Private helpers
375
+ // -----------------------------------------------------------------------
376
+ scheduleGainTransition(gain, target) {
377
+ if (!this.ctx || !Number.isFinite(target) || target < 0) return false;
378
+ const now = this.ctx.currentTime;
379
+ const param = gain.gain;
380
+ param.cancelScheduledValues(now);
381
+ param.setValueAtTime(param.value, now);
382
+ param.linearRampToValueAtTime(target, now + GAIN_TRANSITION_SECONDS);
383
+ return true;
384
+ }
385
+ busGainFor(busName) {
386
+ switch (busName) {
387
+ case "sfx":
388
+ return this.sfxGain;
389
+ case "music":
390
+ return this.musicGain;
391
+ }
392
+ }
393
+ };
394
+
395
+ // src/host-audio-consumer.ts
396
+ function decodeError(sourceKey, cause) {
397
+ return new AudioError$1({
398
+ code: "decode-failed",
399
+ expected: `browser-decodable audio bytes for sourceKey ${sourceKey}`,
400
+ hint: "verify the audio media type and source bytes",
401
+ detail: {
402
+ code: "decode-failed",
403
+ reason: cause instanceof Error ? cause.message : String(cause)
404
+ }
405
+ });
406
+ }
407
+ function sameBytes(left, right) {
408
+ if (left.byteLength !== right.byteLength) return false;
409
+ for (let index = 0; index < left.byteLength; index += 1) {
410
+ if (left[index] !== right[index]) return false;
411
+ }
412
+ return true;
413
+ }
414
+ function createHostAudioConsumer(engine = new WebAudioEngine()) {
415
+ const sources = /* @__PURE__ */ new Map();
416
+ const sourceBytes = /* @__PURE__ */ new Map();
417
+ const activeSources = /* @__PURE__ */ new Map();
418
+ const entityEpoch = /* @__PURE__ */ new Map();
419
+ const bus = {
420
+ sfx: { volume: 1, muted: false },
421
+ music: { volume: 1, muted: false }
422
+ };
423
+ const cleanup = [];
424
+ let lastError = null;
425
+ let disposed = false;
426
+ const nextEpoch = (entityId) => {
427
+ const epoch = (entityEpoch.get(entityId) ?? 0) + 1;
428
+ entityEpoch.set(entityId, epoch);
429
+ return epoch;
430
+ };
431
+ const consumer = {
432
+ engine,
433
+ consume(intent) {
434
+ if (disposed && intent.kind !== "destroy") return;
435
+ if (intent.kind === "play") {
436
+ const epoch = nextEpoch(intent.entityId);
437
+ if (intent.bytes !== void 0) {
438
+ const incomingBytes = intent.bytes.slice();
439
+ const publishedBytes = sourceBytes.get(intent.sourceKey);
440
+ if (publishedBytes === void 0 || !sameBytes(publishedBytes, incomingBytes)) {
441
+ sourceBytes.set(intent.sourceKey, incomingBytes);
442
+ sources.delete(intent.sourceKey);
443
+ }
444
+ }
445
+ const bytes = sourceBytes.get(intent.sourceKey);
446
+ activeSources.set(intent.entityId, {
447
+ entityId: intent.entityId,
448
+ sourceKey: intent.sourceKey,
449
+ ...bytes === void 0 ? {} : { bytes: bytes.slice() },
450
+ options: intent.options
451
+ });
452
+ let decoded = sources.get(intent.sourceKey);
453
+ if (decoded === void 0 && bytes !== void 0) {
454
+ const entry = {
455
+ promise: engine.decode(bytes)
456
+ };
457
+ decoded = entry;
458
+ sources.set(intent.sourceKey, entry);
459
+ void entry.promise.then(
460
+ () => {
461
+ if (sources.get(intent.sourceKey) === entry && lastError?.code === "decode-failed") {
462
+ lastError = null;
463
+ }
464
+ },
465
+ (cause) => {
466
+ if (sources.get(intent.sourceKey) !== entry) return;
467
+ sources.delete(intent.sourceKey);
468
+ lastError = decodeError(intent.sourceKey, cause);
469
+ }
470
+ );
471
+ }
472
+ if (decoded === void 0) {
473
+ lastError = decodeError(intent.sourceKey, new Error("sourceKey was not published"));
474
+ return;
475
+ }
476
+ const currentDecode = decoded;
477
+ void currentDecode.promise.then((buffer) => {
478
+ if (!disposed && sources.get(intent.sourceKey) === currentDecode && entityEpoch.get(intent.entityId) === epoch) {
479
+ engine.play(intent.entityId, buffer, intent.options);
480
+ }
481
+ }).catch(() => {
482
+ });
483
+ } else if (intent.kind === "stop") {
484
+ nextEpoch(intent.entityId);
485
+ if (activeSources.delete(intent.entityId)) {
486
+ cleanup.push(intent.entityId);
487
+ engine.stop(intent.entityId);
488
+ }
489
+ } else if (intent.kind === "set-volume") {
490
+ engine.setVolume(intent.entityId, intent.volume);
491
+ } else if (intent.kind === "set-bus-volume") {
492
+ bus[intent.bus].volume = intent.volume;
493
+ bus[intent.bus].muted = false;
494
+ engine.setBusVolume(intent.bus, intent.volume);
495
+ } else if (intent.kind === "set-bus-mute") {
496
+ bus[intent.bus].muted = intent.muted;
497
+ engine.setBusMute(intent.bus, intent.muted);
498
+ } else if (intent.kind === "set-listener-pose") {
499
+ engine.setListenerPose(intent.pose);
500
+ } else {
501
+ consumer.dispose();
502
+ }
503
+ },
504
+ state() {
505
+ return { ...engine.getState(), lastError };
506
+ },
507
+ dispose() {
508
+ if (disposed) return;
509
+ disposed = true;
510
+ activeSources.clear();
511
+ entityEpoch.clear();
512
+ sources.clear();
513
+ sourceBytes.clear();
514
+ cleanup.length = 0;
515
+ engine.destroy();
516
+ }
517
+ };
518
+ return consumer;
519
+ }
520
+ function createWebAudioBackend() {
521
+ const consumer = createHostAudioConsumer();
522
+ const backend = createAudioIntentBackend({
523
+ emit: (intent) => consumer.consume(intent),
524
+ state: () => consumer.state()
525
+ });
526
+ return backend;
527
+ }
528
+
529
+ // src/plugin.ts
530
+ function webAudioPlugin() {
531
+ return {
532
+ name: "web-audio",
533
+ provide: "audio",
534
+ apply(ctx) {
535
+ const backend = createWebAudioBackend();
536
+ ctx.effect(() => () => backend.destroy(), "audio/destroy-webaudio");
537
+ ctx.provide("audio", backend);
538
+ }
539
+ };
540
+ }
541
+
542
+ export { WebAudioEngine, audioListenerSyncSystem, audioLoader, createHostAudioConsumer, createWebAudioBackend, syncListenerFromWorldMatrix, webAudioPlugin };
543
+ //# sourceMappingURL=index.mjs.map
544
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audio-listener-sync-system.ts","../src/clip-loader.ts","../src/audio-loader.ts","../src/web-audio-engine.ts","../src/host-audio-consumer.ts","../src/plugin.ts"],"names":["AudioError"],"mappings":"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","file":"index.mjs","sourcesContent":["// audio-listener-sync-system.ts -- AudioListener world-matrix sync.\n//\n// Syncs the first AudioListener entity's resolved world transform to the Web\n// Audio listener (position + forward/up orientation).\n//\n// feat-20260601 D-6: the sync consumes the single `Transform.world` mat4 (16\n// column-major floats, written by propagateTransforms) instead of a decomposed\n// GlobalTransform TRS. forward / up / position are extracted via the\n// `@forgeax/engine-math` mat4 helpers (getForward = -col2 normalized; getUp =\n// col1 normalized; getTranslation = col3, NOT normalized) -- direction\n// normalization eliminates non-uniform-scale pollution that a bare-column read\n// would leak. The old quaternion-rotate-vector formula is gone.\n//\n// Decision anchors:\n// - plan-strategy D-6 (rename + world-mat4 reshape + mat4 helper reuse)\n// - requirements AC-08 (AudioListener world-matrix sync; quadrant 3 falsify)\n// - requirements E-3 (multiple AudioListeners -> first only)\n//\n// Architecture note: the audio-webaudio package has no dependency on\n// engine-runtime (where Transform lives). The sync function is a pure helper\n// exported for host assembly. The host (engine-runtime or app layer) queries\n// the first AudioListener entity, reads its `Transform.world` mat4 (a 16-float\n// Float32Array), obtains the AudioContext listener from the AudioBackend, and\n// calls `syncListenerFromWorldMatrix(listener, worldMatrix)`.\n//\n// charter awareness:\n// - P3 explicit failure: no-op contracts, no throw for missing listener\n// - P4 consistent abstraction: reuse the math mat4 extract helpers (no\n// hand-rolled quaternion rotation); pure function exports for unit testing\n// - P5 producer/consumer: sync function is the producer; host is the consumer\n\nimport { mat4, vec3 } from '@forgeax/engine-math';\n\n/**\n * Resolved world transform shape (feat-20260601 D-6): a single column-major\n * mat4 carried as 16 contiguous floats -- the `Transform.world` column array\n * view written by propagateTransforms.\n */\nexport interface WorldMatrixData {\n readonly worldMatrix: Float32Array;\n}\n\n/**\n * Pure function: write a world mat4's position/orientation to the Web Audio\n * listener's AudioParams.\n *\n * Exported for unit testing (listener-sync.test.ts) and for host assembly (the\n * host reads `Transform.world` + AudioListener from the World and calls this\n * each frame).\n *\n * - position = `mat4.getTranslation(world)` (col3, copied directly, not normalized)\n * - forward = `mat4.getForward(world)` (-col2, normalized -- removes scale)\n * - up = `mat4.getUp(world)` (col1, normalized -- removes scale)\n *\n * @param listener The Web Audio API AudioListener (from AudioContext.listener)\n * @param worldMatrix The resolved world mat4 (16 column-major floats) of the\n * entity carrying AudioListener.\n */\nexport function syncListenerFromWorldMatrix(\n listener: AudioListener,\n worldMatrix: Float32Array,\n): void {\n const m = worldMatrix as unknown as mat4.Mat4Like;\n\n const position = mat4.getTranslation(vec3.create(), m);\n listener.positionX.value = position[0] as number;\n listener.positionY.value = position[1] as number;\n listener.positionZ.value = position[2] as number;\n\n const forward = mat4.getForward(vec3.create(), m);\n listener.forwardX.value = forward[0] as number;\n listener.forwardY.value = forward[1] as number;\n listener.forwardZ.value = forward[2] as number;\n\n const up = mat4.getUp(vec3.create(), m);\n listener.upX.value = up[0] as number;\n listener.upY.value = up[1] as number;\n listener.upZ.value = up[2] as number;\n}\n\n/**\n * Run the audio listener sync system for a host-owned audio context.\n *\n * The host is responsible for:\n * 1. Querying the World for entities with AudioListener\n * 2. Taking the first AudioListener entity (E-3)\n * 3. Reading its `Transform.world` mat4 (16-float Float32Array)\n * 4. Calling this function with the AudioContext's listener\n *\n * This function is a convenience wrapper that calls\n * `syncListenerFromWorldMatrix(ctx.listener, worldMatrix)`.\n *\n * @param ctx The Web Audio AudioContext (whose .listener receives position/orientation)\n * @param worldMatrix The `Transform.world` mat4 from the AudioListener entity\n */\nexport function audioListenerSyncSystem(ctx: AudioContext, worldMatrix: Float32Array): void {\n syncListenerFromWorldMatrix(ctx.listener, worldMatrix);\n}\n","// clip-loader.ts -- M3 (w27) AudioClipAsset register/load path\n//\n// Load AudioClipAsset from pack-index catalog via fetch + decodeAudioData.\n//\n// Decision anchors:\n// - plan-strategy D-6 (load path: fetch ArrayBuffer -> decodeAudioData -> register)\n// - requirements AC-03 (AudioClipAsset via asset system loadByGuid)\n// - requirements constraint 5 (format decided by browser decodeAudioData)\n// - requirements E-2 (decodeAudioData failure returns Err with code: 'decode-failed')\n// - requirements E-9 (nonexistent GUID returns Err with code 'asset-not-found')\n// - research Finding 'decodeAudioData error semantics'\n//\n// charter awareness:\n// - P3 explicit failure: returns Result<AudioClipAsset, AudioError> with structured errors\n// - P4 consistent abstraction: parallel to image/gltf loader patterns\n\nimport { AudioError } from '@forgeax/engine-audio';\nimport { type AudioClipAsset, err, ok, type Result } from '@forgeax/engine-types';\n\nexport async function decodeAudioClipBytes(\n guid: string,\n bytes: Uint8Array,\n mediaType: `audio/${string}`,\n): Promise<Result<AudioClipAsset, AudioError>> {\n if (mediaType.length <= 'audio/'.length || bytes.byteLength === 0) {\n return err(\n new AudioError({\n code: 'decode-failed',\n expected: `non-empty audio mediaType and source bytes for GUID ${guid}`,\n hint: 'verify the audio artifact mediaType and recook the source bytes',\n detail: {\n code: 'decode-failed' as const,\n reason: 'audio mediaType or source bytes are empty',\n },\n }),\n );\n }\n try {\n return ok({ kind: 'audio', sourceKey: guid, mediaType, bytes: bytes.slice() });\n } catch (e) {\n return err(\n new AudioError({\n code: 'decode-failed',\n expected: `decodable audio artifact bytes for GUID ${guid}`,\n hint: 'verify the audio artifact mediaType and browser-supported codec',\n detail: {\n code: 'decode-failed' as const,\n reason: e instanceof Error ? e.message : 'audio artifact decode failed',\n },\n }),\n );\n }\n}\n","import { AssetError, type Loader, type LoaderAsyncResult } from '@forgeax/engine-types';\nimport { decodeAudioClipBytes } from './clip-loader';\n\n/** Loads an audio catalog row through the browser's native decoder. */\nexport const audioLoader: Loader = {\n kind: 'audio',\n loadPack(input): Promise<LoaderAsyncResult> {\n if (input.kind !== 'audio') {\n return Promise.resolve({\n ok: false,\n error: new AssetError({\n code: 'asset-parse-failed',\n expected: \"Pack v2 loader input with kind 'audio'\",\n hint: 'pass the asset-local audio envelope to the audio loader',\n detail: { sourcePath: input.guid },\n }),\n });\n }\n const source = input.artifacts.source;\n const payloadMediaType =\n typeof input.payload.mediaType === 'string' ? input.payload.mediaType : undefined;\n if (\n source === undefined ||\n !source.descriptor.mediaType.startsWith('audio/') ||\n payloadMediaType !== source.descriptor.mediaType\n ) {\n return Promise.resolve({\n ok: false,\n error: {\n code: 'asset-artifact-media-unsupported',\n expected: 'an asset-local source artifact with an audio/* mediaType',\n hint: 'declare the audio source artifact with a supported mediaType and re-cook',\n detail: {\n guid: input.guid,\n artifactKey: 'source',\n observed: payloadMediaType ?? source?.descriptor.mediaType ?? 'missing',\n expected: 'payload.mediaType matching the audio/* source artifact',\n },\n },\n });\n }\n return decodeAudioClipBytes(input.guid, source.bytes, payloadMediaType as `audio/${string}`);\n },\n async load(): Promise<LoaderAsyncResult> {\n return {\n ok: false,\n error: new AssetError({\n code: 'asset-parse-failed',\n expected: 'Pack v2 audio input with an asset-local source artifact',\n hint: 're-cook the audio source into the Pack v2 asset envelope',\n detail: { sourcePath: 'audio-loader-input' },\n }),\n };\n },\n};\n","// web-audio-engine.ts -- M2 (w16) WebAudioEngine AudioBackend implementation\n//\n// Implements AudioBackend for Web Audio API:\n// 1. Lazy AudioContext creation (D-3) -- ensureContext() on first play()\n// 2. Gesture listener resume (D-3) -- register bounded 'click'/'keydown'/'touchstart'\n// listeners that call ctx.resume(), re-arming them after a refusal\n// 3. Fixed two-bus topology (D-5): masterGain <= sfxGain + musicGain\n// 4. Per-source GainNode for individual volume control\n// 5. Active source Map<entityId, { node, sourceGain, bus }>\n// 6. Health check: getState() / getActiveSourceCount()\n// 7. destroy(): stop all, disconnect, close ctx\n//\n// Decision anchors:\n// - plan-strategy D-3 (lazy-create + one-shot gesture listener resume)\n// - plan-strategy D-5 (fixed two-bus topology: SFX + Music -> Master)\n// - plan-strategy section 3.1 (WebAudioEngine owner of AudioContext + bus GainNodes)\n// - requirements S-1 (AudioContext lifecycle), S-5 (dual bus), S-9 (World Resource)\n// - requirements AC-01 (lazy creation), AC-02 (auto resume), AC-10 (bus volume/mute)\n//\n// charter awareness:\n// - P3 explicit failure: getState() returns real AudioContext.state, never a stale cache\n// - P4 consistent abstraction: implements AudioBackend interface, parallel to InputBackend\n\nimport {\n AUDIO_ERROR_HINTS,\n AudioError,\n type AudioListenerPose,\n type AudioPlayOptions,\n type AudioState,\n type BusName,\n} from '@forgeax/engine-audio';\nimport type { AudioClipAsset } from '@forgeax/engine-types';\n\ninterface ActiveSource {\n node: AudioBufferSourceNode;\n sourceGain: GainNode;\n panner: PannerNode | undefined;\n bus: BusName;\n}\n\nconst GESTURE_EVENTS = ['click', 'keydown', 'touchstart'] as const;\nconst GAIN_TRANSITION_SECONDS = 0.01;\n\nexport class WebAudioEngine {\n private ctx: AudioContext | undefined;\n private closed = false;\n private masterGain: GainNode | undefined;\n private sfxGain: GainNode | undefined;\n private musicGain: GainNode | undefined;\n\n private readonly sources = new Map<number, ActiveSource>();\n\n private gestureListening = false;\n private resumeInFlight: Promise<void> | undefined;\n private readonly gestureResumeHandler: () => void;\n private lastError: AudioError | null = null;\n\n // Per-bus previous-volume cache for mute/unmute restore (D-5).\n private readonly busVolumes = new Map<BusName, number>([\n ['sfx', 1],\n ['music', 1],\n ]);\n private readonly busMuted = new Map<BusName, boolean>([\n ['sfx', false],\n ['music', false],\n ]);\n\n constructor() {\n // Lazy: AudioContext is NOT created here (D-3 / AC-01).\n // The gesture resume handler is a bound arrow so we can pass it\n // to addEventListener/removeEventListener with the same identity.\n this.gestureResumeHandler = () => {\n void this.tryResume();\n };\n }\n\n /**\n * Returns the Web Audio AudioListener for spatialization (D-2).\n * Triggers lazy ensureContext() on first access.\n * Returns undefined if the context could not be created or is closed.\n */\n get listener(): AudioListener | undefined {\n return this.ensureContext().listener;\n }\n\n setListenerPose(pose: AudioListenerPose): void {\n const listener = this.ensureContext().listener;\n listener.positionX.value = pose.positionX;\n listener.positionY.value = pose.positionY;\n listener.positionZ.value = pose.positionZ;\n listener.forwardX.value = pose.forwardX;\n listener.forwardY.value = pose.forwardY;\n listener.forwardZ.value = pose.forwardZ;\n listener.upX.value = pose.upX;\n listener.upY.value = pose.upY;\n listener.upZ.value = pose.upZ;\n }\n\n // -----------------------------------------------------------------------\n // ensureContext -- lazy AudioContext + bus topology creation\n // -----------------------------------------------------------------------\n\n private ensureContext(): AudioContext {\n if (this.ctx) {\n this.registerGestureListener(this.ctx);\n return this.ctx;\n }\n\n const ctx = new AudioContext();\n\n // Build bus topology: masterGain <= sfxGain + musicGain\n const master = ctx.createGain();\n master.gain.value = 1;\n master.connect(ctx.destination);\n\n const sfx = ctx.createGain();\n sfx.gain.value = 1;\n sfx.connect(master);\n\n const music = ctx.createGain();\n music.gain.value = 1;\n music.connect(master);\n\n this.ctx = ctx;\n this.masterGain = master;\n this.sfxGain = sfx;\n this.musicGain = music;\n\n // Register the bounded gesture listener set if ctx is suspended (autoplay gate).\n this.registerGestureListener(ctx);\n\n return ctx;\n }\n\n // -----------------------------------------------------------------------\n // Gesture listener -- D-3 bounded resume retry on user gesture\n // -----------------------------------------------------------------------\n\n private registerGestureListener(ctx: AudioContext): void {\n if (ctx.state !== 'suspended') {\n return;\n }\n if (this.gestureListening) {\n return;\n }\n\n this.gestureListening = true;\n for (const event of GESTURE_EVENTS) {\n document.addEventListener(event, this.gestureResumeHandler, { once: true });\n }\n }\n\n private removeGestureListener(): void {\n if (!this.gestureListening) {\n return;\n }\n this.gestureListening = false;\n for (const event of GESTURE_EVENTS) {\n document.removeEventListener(event, this.gestureResumeHandler);\n }\n }\n\n private async tryResume(): Promise<void> {\n const ctx = this.ctx;\n if (!ctx || this.closed || ctx.state !== 'suspended') return;\n if (this.resumeInFlight !== undefined) return this.resumeInFlight;\n\n const attempt = (async () => {\n try {\n await ctx.resume();\n } catch {\n // Inspect the real context state below so refusal remains recoverable.\n } finally {\n if (!this.closed && this.ctx === ctx) {\n if (ctx.state === 'running') {\n this.lastError = null;\n this.removeGestureListener();\n } else if (ctx.state === 'suspended') {\n this.recordResumeFailure();\n this.rearmGestureListener(ctx);\n } else {\n this.removeGestureListener();\n }\n }\n this.resumeInFlight = undefined;\n }\n })();\n this.resumeInFlight = attempt;\n return attempt;\n }\n\n private recordResumeFailure(): void {\n this.lastError = new AudioError({\n code: 'context-suspended',\n expected: 'AudioContext.resume() to make the existing context running',\n hint: AUDIO_ERROR_HINTS['context-suspended'],\n detail: { code: 'context-suspended' },\n });\n }\n\n private recordDecodeFailure(sourceKey: string, cause: unknown): void {\n this.lastError = new AudioError({\n code: 'decode-failed',\n expected: `browser-decodable audio bytes for sourceKey ${sourceKey}`,\n hint: AUDIO_ERROR_HINTS['decode-failed'],\n detail: {\n code: 'decode-failed',\n reason: cause instanceof Error ? cause.message : String(cause),\n },\n });\n }\n\n private rearmGestureListener(ctx: AudioContext): void {\n this.removeGestureListener();\n this.registerGestureListener(ctx);\n }\n\n // -----------------------------------------------------------------------\n // AudioBackend implementation\n // -----------------------------------------------------------------------\n\n decode(bytes: Uint8Array): Promise<AudioBuffer> {\n return this.ensureContext().decodeAudioData(bytes.slice().buffer as ArrayBuffer);\n }\n\n play(entityId: number, clip: AudioBuffer | AudioClipAsset, opts: AudioPlayOptions): void {\n if ('kind' in clip) {\n void this.decode(clip.bytes).then(\n (buffer) => {\n if (this.lastError?.code === 'decode-failed') {\n this.lastError = null;\n }\n this.play(entityId, buffer, opts);\n },\n (cause) => this.recordDecodeFailure(clip.sourceKey, cause),\n );\n return;\n }\n const clipBuffer = clip;\n // If this entity is already playing, stop it first (replace).\n if (this.sources.has(entityId)) {\n this.stop(entityId);\n }\n\n const ctx = this.ensureContext();\n\n // Per-source GainNode for volume control\n const sourceGain = ctx.createGain();\n sourceGain.gain.value = opts.volume;\n\n // PannerNode for 3D spatialization (D-2 equalpower default)\n let panner: PannerNode | undefined;\n if (opts.spatialBlend > 0) {\n panner = ctx.createPanner();\n panner.panningModel = 'equalpower';\n }\n\n // Route to the appropriate bus (gain nodes guaranteed by ensureContext above)\n const busGain = this.busGainFor(opts.bus);\n if (!busGain) return;\n\n if (panner) {\n sourceGain.connect(panner);\n panner.connect(busGain);\n } else {\n sourceGain.connect(busGain);\n }\n\n // Create AudioBufferSourceNode for one-shot playback\n const node = ctx.createBufferSource();\n node.buffer = clipBuffer;\n node.loop = opts.loop;\n node.connect(sourceGain);\n node.start();\n\n // Bookkeeping\n this.sources.set(entityId, { node, sourceGain, panner, bus: opts.bus });\n\n // F24: attach onended for non-loop sources with identity guard (D-5).\n // Loop sources never naturally end — no onended needed.\n if (!opts.loop) {\n node.onended = () => {\n const current = this.sources.get(entityId);\n if (current?.node === node) {\n this.stop(entityId);\n }\n };\n }\n }\n\n stop(entityId: number): void {\n const source = this.sources.get(entityId);\n if (!source) return;\n\n try {\n source.node.stop();\n } catch {\n // Already stopped -- ignore InvalidStateError from doubly-stopped nodes.\n }\n source.node.disconnect();\n source.sourceGain.disconnect();\n source.panner?.disconnect();\n this.sources.delete(entityId);\n }\n\n setVolume(entityId: number, volume: number): void {\n const source = this.sources.get(entityId);\n if (!source) return;\n this.scheduleGainTransition(source.sourceGain, volume);\n }\n\n setBusVolume(busName: BusName, volume: number): void {\n const gain = this.busGainFor(busName);\n if (!gain) return;\n\n if (!this.scheduleGainTransition(gain, volume)) return;\n this.busVolumes.set(busName, volume);\n\n // If we were muted, un-mute (setting volume is an explicit un-mute signal).\n if (this.busMuted.get(busName)) {\n this.busMuted.set(busName, false);\n }\n }\n\n setBusMute(busName: BusName, muted: boolean): void {\n const gain = this.busGainFor(busName);\n if (!gain) return;\n\n const target = muted ? 0 : (this.busVolumes.get(busName) ?? 1);\n if (!this.scheduleGainTransition(gain, target)) return;\n this.busMuted.set(busName, muted);\n }\n\n getState(): AudioState {\n if (this.closed) {\n return { contextState: 'closed', activeSourceCount: 0, lastError: null };\n }\n const contextState: 'running' | 'suspended' | 'closed' =\n this.ctx?.state === 'closed'\n ? 'closed'\n : this.ctx?.state === 'running'\n ? 'running'\n : 'suspended';\n return {\n contextState,\n activeSourceCount: this.sources.size,\n lastError: this.lastError,\n };\n }\n\n getActiveSourceCount(): number {\n return this.sources.size;\n }\n\n destroy(): void {\n if (this.closed) return;\n this.closed = true;\n\n // Stop all active sources\n for (const entityId of this.sources.keys()) {\n this.stop(entityId);\n }\n\n // Disconnect bus topology\n if (this.sfxGain) {\n this.sfxGain.disconnect();\n this.sfxGain = undefined;\n }\n if (this.musicGain) {\n this.musicGain.disconnect();\n this.musicGain = undefined;\n }\n if (this.masterGain) {\n this.masterGain.disconnect();\n this.masterGain = undefined;\n }\n\n // Remove gesture listener\n this.removeGestureListener();\n\n // Close AudioContext (irreversible per R-4)\n if (this.ctx) {\n void this.ctx.close();\n this.ctx = undefined;\n }\n }\n\n // -----------------------------------------------------------------------\n // Private helpers\n // -----------------------------------------------------------------------\n\n private scheduleGainTransition(gain: GainNode, target: number): boolean {\n if (!this.ctx || !Number.isFinite(target) || target < 0) return false;\n\n const now = this.ctx.currentTime;\n const param = gain.gain;\n param.cancelScheduledValues(now);\n param.setValueAtTime(param.value, now);\n param.linearRampToValueAtTime(target, now + GAIN_TRANSITION_SECONDS);\n return true;\n }\n\n private busGainFor(busName: BusName): GainNode | undefined {\n switch (busName) {\n case 'sfx':\n return this.sfxGain;\n case 'music':\n return this.musicGain;\n }\n }\n}\n","import {\n type AudioBackend,\n type AudioIntent,\n type AudioPlayOptions,\n type AudioState,\n createAudioIntentBackend,\n} from '@forgeax/engine-audio';\nimport { AudioError } from '@forgeax/engine-types';\nimport { WebAudioEngine } from './web-audio-engine';\n\ninterface ActiveSource {\n readonly entityId: number;\n readonly sourceKey: string;\n readonly bytes?: Uint8Array;\n readonly options: AudioPlayOptions;\n}\n\nexport interface HostAudioConsumer {\n consume(intent: AudioIntent): void;\n state(): AudioState;\n dispose(): void;\n readonly engine: WebAudioEngine;\n}\n\nfunction decodeError(sourceKey: string, cause: unknown): AudioError {\n return new AudioError({\n code: 'decode-failed',\n expected: `browser-decodable audio bytes for sourceKey ${sourceKey}`,\n hint: 'verify the audio media type and source bytes',\n detail: {\n code: 'decode-failed',\n reason: cause instanceof Error ? cause.message : String(cause),\n },\n });\n}\n\nfunction sameBytes(left: Uint8Array, right: Uint8Array): boolean {\n if (left.byteLength !== right.byteLength) return false;\n for (let index = 0; index < left.byteLength; index += 1) {\n if (left[index] !== right[index]) return false;\n }\n return true;\n}\n\ninterface DecodeEntry {\n readonly promise: Promise<AudioBuffer>;\n}\n\nexport function createHostAudioConsumer(engine = new WebAudioEngine()): HostAudioConsumer {\n const sources = new Map<string, DecodeEntry>();\n const sourceBytes = new Map<string, Uint8Array>();\n const activeSources = new Map<number, ActiveSource>();\n const entityEpoch = new Map<number, number>();\n const bus = {\n sfx: { volume: 1, muted: false },\n music: { volume: 1, muted: false },\n };\n const cleanup: number[] = [];\n let lastError: AudioError | null = null;\n let disposed = false;\n const nextEpoch = (entityId: number): number => {\n const epoch = (entityEpoch.get(entityId) ?? 0) + 1;\n entityEpoch.set(entityId, epoch);\n return epoch;\n };\n const consumer: HostAudioConsumer = {\n engine,\n consume(intent): void {\n if (disposed && intent.kind !== 'destroy') return;\n if (intent.kind === 'play') {\n const epoch = nextEpoch(intent.entityId);\n if (intent.bytes !== undefined) {\n const incomingBytes = intent.bytes.slice();\n const publishedBytes = sourceBytes.get(intent.sourceKey);\n if (publishedBytes === undefined || !sameBytes(publishedBytes, incomingBytes)) {\n sourceBytes.set(intent.sourceKey, incomingBytes);\n sources.delete(intent.sourceKey);\n }\n }\n const bytes = sourceBytes.get(intent.sourceKey);\n activeSources.set(intent.entityId, {\n entityId: intent.entityId,\n sourceKey: intent.sourceKey,\n ...(bytes === undefined ? {} : { bytes: bytes.slice() }),\n options: intent.options,\n });\n let decoded = sources.get(intent.sourceKey);\n if (decoded === undefined && bytes !== undefined) {\n const entry: DecodeEntry = {\n promise: engine.decode(bytes),\n };\n decoded = entry;\n sources.set(intent.sourceKey, entry);\n void entry.promise.then(\n () => {\n if (sources.get(intent.sourceKey) === entry && lastError?.code === 'decode-failed') {\n lastError = null;\n }\n },\n (cause) => {\n if (sources.get(intent.sourceKey) !== entry) return;\n sources.delete(intent.sourceKey);\n lastError = decodeError(intent.sourceKey, cause);\n },\n );\n }\n if (decoded === undefined) {\n lastError = decodeError(intent.sourceKey, new Error('sourceKey was not published'));\n return;\n }\n const currentDecode = decoded;\n void currentDecode.promise\n .then((buffer) => {\n if (\n !disposed &&\n sources.get(intent.sourceKey) === currentDecode &&\n entityEpoch.get(intent.entityId) === epoch\n ) {\n engine.play(intent.entityId, buffer, intent.options);\n }\n })\n .catch(() => {});\n } else if (intent.kind === 'stop') {\n nextEpoch(intent.entityId);\n if (activeSources.delete(intent.entityId)) {\n cleanup.push(intent.entityId);\n engine.stop(intent.entityId);\n }\n } else if (intent.kind === 'set-volume') {\n engine.setVolume(intent.entityId, intent.volume);\n } else if (intent.kind === 'set-bus-volume') {\n bus[intent.bus].volume = intent.volume;\n bus[intent.bus].muted = false;\n engine.setBusVolume(intent.bus, intent.volume);\n } else if (intent.kind === 'set-bus-mute') {\n bus[intent.bus].muted = intent.muted;\n engine.setBusMute(intent.bus, intent.muted);\n } else if (intent.kind === 'set-listener-pose') {\n engine.setListenerPose(intent.pose);\n } else {\n consumer.dispose();\n }\n },\n state(): AudioState {\n return { ...engine.getState(), lastError };\n },\n dispose(): void {\n if (disposed) return;\n disposed = true;\n activeSources.clear();\n entityEpoch.clear();\n sources.clear();\n sourceBytes.clear();\n cleanup.length = 0;\n engine.destroy();\n },\n };\n return consumer;\n}\n\nexport function createWebAudioBackend(): AudioBackend {\n const consumer = createHostAudioConsumer();\n const backend = createAudioIntentBackend({\n emit: (intent) => consumer.consume(intent),\n state: () => consumer.state(),\n });\n return backend;\n}\n","import type { Plugin } from '@forgeax/engine-plugin';\n\nimport { createWebAudioBackend } from './host-audio-consumer';\n\nexport function webAudioPlugin(): Plugin {\n return {\n name: 'web-audio',\n provide: 'audio',\n apply(ctx) {\n const backend = createWebAudioBackend();\n ctx.effect(() => () => backend.destroy(), 'audio/destroy-webaudio');\n ctx.provide('audio', backend);\n },\n };\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from '@forgeax/engine-plugin';
2
+ export declare function webAudioPlugin(): Plugin;
3
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAIrD,wBAAgB,cAAc,IAAI,MAAM,CAUvC"}