@utsp/audio 0.4.0-nightly.20251203172602.bbfc50f

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.
@@ -0,0 +1,620 @@
1
+ import { ISoundLoader, IAudioProcessor } from '@utsp/types';
2
+
3
+ /**
4
+ * SoundBank - Unified audio asset cache for UTSP applications
5
+ *
6
+ * Stores decoded AudioBuffers from any source (File, FileExternal, Procedural)
7
+ * in a unified format for consistent playback.
8
+ *
9
+ * Implements ISoundLoader interface from @utsp/types for unified sound loading
10
+ * abstraction.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const soundBank = new SoundBank(audioContext);
15
+ *
16
+ * // Load from different sources (all stored the same way)
17
+ * await soundBank.loadFromData(0, 'coin', mp3Data);
18
+ * await soundBank.loadFromUrl(1, 'music', 'https://cdn.example.com/music.mp3');
19
+ *
20
+ * // Access uniformly
21
+ * const sound = soundBank.get('coin');
22
+ * console.log(sound.duration); // Works the same regardless of source
23
+ * ```
24
+ */
25
+
26
+ /**
27
+ * Source type for a loaded sound
28
+ */
29
+ type SoundSource = 'file' | 'external' | 'procedural';
30
+ /**
31
+ * Metadata about a loaded sound
32
+ */
33
+ interface SoundMetadata {
34
+ /** How the sound was loaded */
35
+ source: SoundSource;
36
+ /** Original size in bytes before decoding (if applicable) */
37
+ originalSize?: number;
38
+ /** Original URL (for external sounds) */
39
+ originalUrl?: string;
40
+ /** Timestamp when the sound was loaded */
41
+ loadedAt: number;
42
+ }
43
+ /**
44
+ * A loaded sound ready for playback
45
+ * Unified representation regardless of source
46
+ */
47
+ interface LoadedSound {
48
+ /** Unique sound identifier (0-255) */
49
+ soundId: number;
50
+ /** Human-readable name for the sound */
51
+ name: string;
52
+ /** Decoded audio buffer ready for playback */
53
+ buffer: AudioBuffer;
54
+ /** Duration in seconds */
55
+ duration: number;
56
+ /** Loading metadata (source, size, etc.) */
57
+ metadata: SoundMetadata;
58
+ }
59
+ /**
60
+ * Statistics about the sound bank
61
+ */
62
+ interface SoundBankStats {
63
+ /** Total number of loaded sounds */
64
+ totalSounds: number;
65
+ /** Total duration of all sounds in seconds */
66
+ totalDuration: number;
67
+ /** Total memory usage estimate in bytes */
68
+ estimatedMemory: number;
69
+ /** Count by source type */
70
+ bySource: {
71
+ file: number;
72
+ external: number;
73
+ procedural: number;
74
+ };
75
+ }
76
+ /**
77
+ * Progress callback for loading sounds
78
+ */
79
+ type LoadProgressCallback = (loaded: number, total: number) => void;
80
+ /**
81
+ * SoundBank - Unified storage for decoded audio assets
82
+ *
83
+ * Implements ISoundLoader for unified sound loading abstraction.
84
+ */
85
+ declare class SoundBank implements ISoundLoader {
86
+ private sounds;
87
+ private nameToId;
88
+ private audioContext;
89
+ private debug;
90
+ /**
91
+ * Create a new SoundBank
92
+ * @param audioContext - The Web Audio context for decoding
93
+ * @param debug - Enable debug logging
94
+ */
95
+ constructor(audioContext: AudioContext, debug?: boolean);
96
+ /**
97
+ * Load a sound from binary data (File mode - sent via UTSP)
98
+ *
99
+ * @param soundId - Unique identifier (0-255)
100
+ * @param name - Human-readable name
101
+ * @param data - Raw audio data (MP3, WAV, OGG, etc.)
102
+ */
103
+ loadFromData(soundId: number, name: string, data: Uint8Array): Promise<void>;
104
+ /**
105
+ * Load a sound from an external URL (FileExternal mode - CDN, etc.)
106
+ *
107
+ * @param soundId - Unique identifier (0-255)
108
+ * @param name - Human-readable name
109
+ * @param url - URL to fetch the audio from
110
+ * @param onProgress - Optional progress callback
111
+ */
112
+ loadFromUrl(soundId: number, name: string, url: string, onProgress?: LoadProgressCallback): Promise<void>;
113
+ /**
114
+ * Load a sound from a procedural generator (future use)
115
+ *
116
+ * @param soundId - Unique identifier (0-255)
117
+ * @param name - Human-readable name
118
+ * @param buffer - Pre-generated AudioBuffer
119
+ */
120
+ loadFromBuffer(soundId: number, name: string, buffer: AudioBuffer): void;
121
+ /**
122
+ * Store a decoded sound in the bank
123
+ */
124
+ private store;
125
+ /**
126
+ * Decode raw audio data to an AudioBuffer
127
+ */
128
+ private decodeAudio;
129
+ /**
130
+ * Get a loaded sound by ID or name
131
+ *
132
+ * @param idOrName - Sound ID (number) or name (string)
133
+ * @returns The loaded sound, or undefined if not found
134
+ */
135
+ get(idOrName: number | string): LoadedSound | undefined;
136
+ /**
137
+ * Check if a sound is loaded
138
+ *
139
+ * @param idOrName - Sound ID (number) or name (string)
140
+ */
141
+ has(idOrName: number | string): boolean;
142
+ /**
143
+ * Get all loaded sounds
144
+ */
145
+ getAll(): LoadedSound[];
146
+ /**
147
+ * Get all loaded sound names
148
+ */
149
+ getNames(): string[];
150
+ /**
151
+ * Get the number of loaded sounds
152
+ */
153
+ get size(): number;
154
+ /**
155
+ * Remove a sound from the bank
156
+ *
157
+ * @param idOrName - Sound ID (number) or name (string)
158
+ * @returns true if the sound was removed, false if not found
159
+ */
160
+ remove(idOrName: number | string): boolean;
161
+ /**
162
+ * Clear all sounds from the bank
163
+ */
164
+ clear(): void;
165
+ /**
166
+ * Get statistics about the sound bank
167
+ */
168
+ getStats(): SoundBankStats;
169
+ private log;
170
+ }
171
+
172
+ /**
173
+ * AudioManager - Web Audio API management for UTSP applications
174
+ *
175
+ * Provides a centralized audio context and utilities for playing sounds.
176
+ * Handles browser autoplay restrictions by requiring user interaction to initialize.
177
+ *
178
+ * Implements IAudioProcessor interface from @utsp/types for unified audio handling
179
+ * across standalone and connected modes.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * // Create audio manager
184
+ * const audio = new AudioManager({ debug: true });
185
+ *
186
+ * // Initialize on user interaction (required by browsers)
187
+ * button.addEventListener('click', () => {
188
+ * audio.initialize();
189
+ * audio.playStartSound(); // Confirmation sound
190
+ * });
191
+ *
192
+ * // Play loaded sounds (from File or FileExternal)
193
+ * audio.play('coin');
194
+ * audio.play('explosion', { volume: 0.5, pitch: 1.2 });
195
+ *
196
+ * // Play custom tones
197
+ * audio.playTone(440, 0.5); // A4 note for 0.5 seconds
198
+ * ```
199
+ */
200
+
201
+ interface AudioManagerOptions {
202
+ /** Enable debug logging */
203
+ debug?: boolean;
204
+ /** Master volume (0.0 to 1.0, default: 1.0) */
205
+ masterVolume?: number;
206
+ }
207
+ interface ToneOptions {
208
+ /** Oscillator type (default: 'sine') */
209
+ type?: OscillatorType;
210
+ /** Volume (0.0 to 1.0, default: 0.3) */
211
+ volume?: number;
212
+ /** Attack time in seconds (default: 0.01) */
213
+ attack?: number;
214
+ /** Release time in seconds (default: 0.1) */
215
+ release?: number;
216
+ }
217
+ /**
218
+ * Options for playing a loaded sound
219
+ */
220
+ interface PlayOptions {
221
+ /** Volume multiplier (0.0 to 1.0, default: 1.0) */
222
+ volume?: number;
223
+ /** Playback rate / pitch (0.5 = octave down, 2.0 = octave up, default: 1.0) */
224
+ pitch?: number;
225
+ /** Loop the sound (default: false) */
226
+ loop?: boolean;
227
+ /** Start time offset in seconds (default: 0) */
228
+ offset?: number;
229
+ /** Fade in duration in seconds (0 = no fade, default: 0) */
230
+ fadeIn?: number;
231
+ /** Spatial position for 2D positional audio (16-bit X/Y) */
232
+ position?: {
233
+ x: number;
234
+ y: number;
235
+ };
236
+ /**
237
+ * Instance ID for this playback (optional)
238
+ * If provided (e.g., from server), this ID will be used.
239
+ * If not provided (standalone mode), an ID will be auto-generated.
240
+ */
241
+ instanceId?: number;
242
+ }
243
+ /**
244
+ * Configuration for spatial audio listener
245
+ */
246
+ interface SpatialConfig {
247
+ /** Listener X position (0-65535) */
248
+ listenerX: number;
249
+ /** Listener Y position (0-65535) */
250
+ listenerY: number;
251
+ /** Maximum audible distance (default: 16384 = 1/4 of 16-bit range) */
252
+ maxDistance: number;
253
+ /** Reference distance where volume = 1.0 (default: 1024) */
254
+ referenceDistance: number;
255
+ /** Rolloff factor (higher = faster volume falloff, default: 1.0) */
256
+ rolloffFactor: number;
257
+ /** Pan spread (0 = no stereo, 1 = full stereo pan, default: 0.8) */
258
+ panSpread: number;
259
+ }
260
+ /**
261
+ * Handle to a playing sound for stop/control
262
+ */
263
+ interface PlayingSound {
264
+ /** Unique instance ID for this playback */
265
+ readonly instanceId: number;
266
+ /** Stop the sound immediately */
267
+ stop(): void;
268
+ /** Check if the sound is still playing */
269
+ readonly playing: boolean;
270
+ /** The sound being played */
271
+ readonly sound: LoadedSound;
272
+ }
273
+ /**
274
+ * AudioManager - Main audio management class
275
+ *
276
+ * Implements IAudioProcessor for unified audio handling in standalone and connected modes.
277
+ */
278
+ declare class AudioManager implements IAudioProcessor {
279
+ private audioContext;
280
+ private masterGainNode;
281
+ private soundBank;
282
+ private options;
283
+ private initialized;
284
+ private nextInstanceId;
285
+ private instances;
286
+ private instancesBySound;
287
+ private activeSounds;
288
+ private spatialSounds;
289
+ private spatialConfig;
290
+ constructor(options?: AudioManagerOptions);
291
+ /**
292
+ * Initialize the Web AudioContext
293
+ * Must be called from a user interaction event (click, touch, keypress)
294
+ *
295
+ * @returns true if initialization was successful, false otherwise
296
+ */
297
+ initialize(): boolean;
298
+ /**
299
+ * Check if the AudioManager is initialized and ready to play sounds
300
+ */
301
+ isInitialized(): boolean;
302
+ /**
303
+ * Get the underlying AudioContext
304
+ * Returns null if not initialized
305
+ */
306
+ getContext(): AudioContext | null;
307
+ /**
308
+ * Get the master gain node for connecting custom audio nodes
309
+ * Returns null if not initialized
310
+ */
311
+ getMasterGain(): GainNode | null;
312
+ /**
313
+ * Set master volume
314
+ * @param volume Volume level (0.0 to 1.0)
315
+ */
316
+ setMasterVolume(volume: number): void;
317
+ /**
318
+ * Get current master volume
319
+ */
320
+ getMasterVolume(): number;
321
+ /**
322
+ * Play a retro arcade-style start sound
323
+ * Creates a quick ascending arpeggio with a square wave for that classic 8-bit feel
324
+ */
325
+ playStartSound(): void;
326
+ /**
327
+ * Play a tone at a specific frequency
328
+ *
329
+ * @param frequency Frequency in Hz (e.g., 440 for A4)
330
+ * @param duration Duration in seconds
331
+ * @param options Tone options (type, volume, attack, release)
332
+ */
333
+ playTone(frequency: number, duration: number, options?: ToneOptions): void;
334
+ /**
335
+ * Play a note by name (e.g., 'C4', 'A#5', 'Bb3')
336
+ *
337
+ * @param note Note name with octave (e.g., 'A4', 'C#5', 'Bb3')
338
+ * @param duration Duration in seconds
339
+ * @param options Tone options
340
+ */
341
+ playNote(note: string, duration: number, options?: ToneOptions): void;
342
+ /**
343
+ * Convert a note name to frequency
344
+ *
345
+ * @param note Note name (e.g., 'A4', 'C#5', 'Bb3')
346
+ * @returns Frequency in Hz, or null if invalid
347
+ */
348
+ noteToFrequency(note: string): number | null;
349
+ /**
350
+ * Resume the AudioContext if it was suspended
351
+ * Useful when tab becomes visible again
352
+ */
353
+ resumeContext(): Promise<void>;
354
+ /**
355
+ * Suspend the AudioContext to save resources
356
+ * Useful when tab becomes hidden
357
+ */
358
+ suspendContext(): Promise<void>;
359
+ /**
360
+ * Get the current state of the AudioContext
361
+ */
362
+ getState(): AudioContextState | null;
363
+ /**
364
+ * Get the sound bank for loading and managing sounds
365
+ * Returns null if not initialized
366
+ */
367
+ getSoundBank(): SoundBank | null;
368
+ /**
369
+ * Play a loaded sound by name or ID
370
+ *
371
+ * @param nameOrId - Sound name (string) or ID (number)
372
+ * @param options - Playback options (volume, pitch, loop, position)
373
+ * @returns Handle to control the playing sound, or null if sound not found
374
+ *
375
+ * @example
376
+ * ```typescript
377
+ * // Simple playback (global, centered)
378
+ * const instance = audio.play('coin');
379
+ * console.log(instance.instanceId); // e.g., 1
380
+ *
381
+ * // With options
382
+ * audio.play('explosion', { volume: 0.5, pitch: 0.8 });
383
+ *
384
+ * // Positional audio (2D spatial)
385
+ * audio.play('footstep', { position: { x: 10000, y: 32768 } });
386
+ *
387
+ * // Looping music - can stop later by instanceId
388
+ * const music = audio.play('background', { loop: true });
389
+ * // Later: audio.stop(music.instanceId);
390
+ * ```
391
+ */
392
+ play(nameOrId: string | number, options?: PlayOptions): PlayingSound | null;
393
+ /**
394
+ * Stop a sound by instance ID, sound name, or all sounds
395
+ *
396
+ * @param target - Instance ID (number), sound name (string), or 'all'
397
+ * @returns Number of instances stopped
398
+ *
399
+ * @example
400
+ * ```typescript
401
+ * // Stop specific instance
402
+ * const music = audio.play('background', { loop: true });
403
+ * audio.stop(music.instanceId);
404
+ *
405
+ * // Stop all instances of a sound
406
+ * audio.stop('explosion');
407
+ *
408
+ * // Stop everything
409
+ * audio.stop('all');
410
+ * ```
411
+ */
412
+ stop(target: number | string): number;
413
+ /**
414
+ * Fade out and stop a sound
415
+ *
416
+ * @param target - Instance ID (number), sound name (string), or 'all'
417
+ * @param duration - Fade duration in seconds
418
+ * @returns Number of instances being faded
419
+ *
420
+ * @example
421
+ * ```typescript
422
+ * // Fade out specific instance over 2 seconds
423
+ * const music = audio.play('background', { loop: true });
424
+ * audio.fadeOut(music.instanceId, 2);
425
+ *
426
+ * // Fade out all instances of a sound
427
+ * audio.fadeOut('ambience', 1.5);
428
+ *
429
+ * // Fade out everything
430
+ * audio.fadeOut('all', 1);
431
+ * ```
432
+ */
433
+ fadeOut(target: number | string | 'all', duration: number): number;
434
+ /**
435
+ * Fade out a specific instance
436
+ * @private
437
+ */
438
+ private fadeOutInstance;
439
+ /**
440
+ * Fade out all instances of a sound by name
441
+ * @private
442
+ */
443
+ private fadeOutByName;
444
+ /**
445
+ * Fade out all playing sounds
446
+ * @private
447
+ */
448
+ private fadeOutAll;
449
+ /**
450
+ * Pause a sound by instance ID, sound name, or all sounds
451
+ *
452
+ * @param target - Instance ID (number), sound name (string), or 'all'
453
+ * @returns Number of instances paused
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * // Pause specific instance
458
+ * const music = audio.play('background', { loop: true });
459
+ * audio.pause(music.instanceId);
460
+ *
461
+ * // Pause all instances of a sound
462
+ * audio.pause('ambience');
463
+ *
464
+ * // Pause everything
465
+ * audio.pause('all');
466
+ * ```
467
+ */
468
+ pause(target: number | string | 'all'): number;
469
+ /**
470
+ * Pause a specific instance
471
+ * @private
472
+ */
473
+ private pauseInstance;
474
+ /**
475
+ * Pause all instances of a sound by name
476
+ * @private
477
+ */
478
+ private pauseByName;
479
+ /**
480
+ * Pause all playing sounds
481
+ * @private
482
+ */
483
+ private pauseAll;
484
+ /**
485
+ * Resume a paused sound by instance ID, sound name, or all sounds
486
+ *
487
+ * @param target - Instance ID (number), sound name (string), or 'all'
488
+ * @returns Number of instances resumed
489
+ *
490
+ * @example
491
+ * ```typescript
492
+ * // Resume specific instance
493
+ * audio.resume(musicInstanceId);
494
+ *
495
+ * // Resume all instances of a sound
496
+ * audio.resume('ambience');
497
+ *
498
+ * // Resume everything
499
+ * audio.resume('all');
500
+ * ```
501
+ */
502
+ resume(target: number | string | 'all'): number;
503
+ /**
504
+ * Resume a specific paused instance
505
+ * @private
506
+ */
507
+ private resumeInstance;
508
+ /**
509
+ * Resume all instances of a sound by name
510
+ * @private
511
+ */
512
+ private resumeByName;
513
+ /**
514
+ * Resume all paused sounds
515
+ * @private
516
+ */
517
+ private resumeAll;
518
+ /**
519
+ * Stop a specific sound instance by ID
520
+ * @private
521
+ */
522
+ private stopInstance;
523
+ /**
524
+ * Stop all instances of a sound by name
525
+ * @private
526
+ */
527
+ private stopByName;
528
+ /**
529
+ * Cleanup instance data after it ends or is stopped
530
+ * @private
531
+ */
532
+ private cleanupInstance;
533
+ /**
534
+ * Set the listener position for spatial audio
535
+ * Updates all currently playing spatial sounds in real-time
536
+ *
537
+ * @param x - X position
538
+ * @param y - Y position
539
+ *
540
+ * @example
541
+ * ```typescript
542
+ * // Update listener position every frame (typically the player's position)
543
+ * audio.setListenerPosition(playerX, playerY);
544
+ * ```
545
+ */
546
+ setListenerPosition(x: number, y: number): void;
547
+ /**
548
+ * Update all active spatial sounds based on current listener position
549
+ * Called automatically when listener position changes
550
+ * @private
551
+ */
552
+ private updateAllSpatialSounds;
553
+ /**
554
+ * Get the current listener position
555
+ */
556
+ getListenerPosition(): {
557
+ x: number;
558
+ y: number;
559
+ };
560
+ /**
561
+ * Configure spatial audio parameters
562
+ *
563
+ * @param config - Partial spatial configuration
564
+ *
565
+ * @example
566
+ * ```typescript
567
+ * // Configure for a smaller game world
568
+ * audio.configureSpatial({
569
+ * maxDistance: 8192, // Sounds fade out at 8192 units
570
+ * referenceDistance: 512, // Full volume within 512 units
571
+ * rolloffFactor: 1.5, // Faster falloff
572
+ * panSpread: 1.0, // Full stereo pan
573
+ * });
574
+ * ```
575
+ */
576
+ configureSpatial(config: Partial<SpatialConfig>): void;
577
+ /**
578
+ * Get current spatial audio configuration
579
+ */
580
+ getSpatialConfig(): Readonly<SpatialConfig>;
581
+ /**
582
+ * Calculate stereo pan and volume based on sound position relative to listener
583
+ *
584
+ * Uses a linear attenuation model:
585
+ * - Volume = 1.0 when distance <= referenceDistance
586
+ * - Volume decreases linearly from 1.0 to 0.0 between referenceDistance and maxDistance
587
+ * - Volume = 0.0 when distance >= maxDistance
588
+ *
589
+ * @param soundX - Sound X position
590
+ * @param soundY - Sound Y position
591
+ * @returns Object with pan (-1 to 1) and distanceVolume (0 to 1)
592
+ * @private
593
+ */
594
+ private calculateSpatialAudio;
595
+ /**
596
+ * Stop all currently playing sounds
597
+ * @returns Number of instances stopped
598
+ */
599
+ stopAll(): number;
600
+ /**
601
+ * Get the number of currently playing sounds
602
+ */
603
+ getPlayingCount(): number;
604
+ /**
605
+ * Get all active instance IDs
606
+ */
607
+ getActiveInstances(): number[];
608
+ /**
609
+ * Check if a specific instance is still playing
610
+ */
611
+ isPlaying(instanceId: number): boolean;
612
+ /**
613
+ * Destroy the AudioManager and release resources
614
+ */
615
+ destroy(): void;
616
+ private log;
617
+ }
618
+
619
+ export { AudioManager, SoundBank };
620
+ export type { AudioManagerOptions, LoadProgressCallback, LoadedSound, PlayOptions, PlayingSound, SoundBankStats, SoundMetadata, SoundSource, ToneOptions };
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @utsp/audio
3
+ *
4
+ * Web Audio API management for UTSP applications.
5
+ * Provides audio context initialization, sound playback, and utilities.
6
+ */
7
+ // Main AudioManager
8
+ export { AudioManager } from './AudioManager';
9
+ // SoundBank for managing loaded sounds
10
+ export { SoundBank } from './SoundBank';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,oBAAoB;AACpB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,uCAAuC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ var $=Object.defineProperty;var I=(m,t,e)=>t in m?$(m,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):m[t]=e;var A=(m,t)=>$(m,"name",{value:t,configurable:!0});var c=(m,t,e)=>(I(m,typeof t!="symbol"?t+"":t,e),e);var S=class S{constructor(t,e=!1){c(this,"sounds",new Map);c(this,"nameToId",new Map);c(this,"audioContext");c(this,"debug");this.audioContext=t,this.debug=e,this.log("SoundBank created")}async loadFromData(t,e,n){this.log(`Loading sound from data: ${e} (${n.length} bytes)`);try{let o=await this.decodeAudio(n);this.store(t,e,o,{source:"file",originalSize:n.length,loadedAt:Date.now()}),this.log(`\u2713 Loaded: ${e} (${o.duration.toFixed(2)}s)`)}catch(o){throw console.error(`[SoundBank] Failed to decode sound "${e}":`,o),o}}async loadFromUrl(t,e,n,o){this.log(`Loading sound from URL: ${e} \u2192 ${n}`);try{let i=await fetch(n);if(!i.ok)throw new Error(`HTTP ${i.status}: ${i.statusText}`);let r=i.headers.get("content-length"),d=r?parseInt(r,10):0,a;if(d>0&&o&&i.body){let u=i.body.getReader(),l=[],f=0;for(;;){let{done:p,value:h}=await u.read();if(p)break;l.push(h),f+=h.length,o(f,d)}let y=new Uint8Array(f),x=0;for(let p of l)y.set(p,x),x+=p.length;a=y.buffer}else a=await i.arrayBuffer();let s=await this.decodeAudio(new Uint8Array(a));this.store(t,e,s,{source:"external",originalSize:a.byteLength,originalUrl:n,loadedAt:Date.now()}),this.log(`\u2713 Loaded from URL: ${e} (${s.duration.toFixed(2)}s)`)}catch(i){throw console.error(`[SoundBank] Failed to load sound "${e}" from ${n}:`,i),i}}loadFromBuffer(t,e,n){this.log(`Loading sound from buffer: ${e}`),this.store(t,e,n,{source:"procedural",loadedAt:Date.now()}),this.log(`\u2713 Loaded from buffer: ${e} (${n.duration.toFixed(2)}s)`)}store(t,e,n,o){if(this.sounds.has(t)){let r=this.sounds.get(t);this.log(`Replacing sound ID ${t} (was: ${r.name}, now: ${e})`),this.nameToId.delete(r.name)}if(this.nameToId.has(e)){let r=this.nameToId.get(e);r!==t&&this.log(`Warning: Sound name "${e}" already exists with ID ${r}`)}let i={soundId:t,name:e,buffer:n,duration:n.duration,metadata:o};this.sounds.set(t,i),this.nameToId.set(e,t)}async decodeAudio(t){let e=new ArrayBuffer(t.byteLength);return new Uint8Array(e).set(t),this.audioContext.decodeAudioData(e)}get(t){if(typeof t=="number")return this.sounds.get(t);let e=this.nameToId.get(t);return e!==void 0?this.sounds.get(e):void 0}has(t){return this.get(t)!==void 0}getAll(){return Array.from(this.sounds.values())}getNames(){return Array.from(this.nameToId.keys())}get size(){return this.sounds.size}remove(t){let e=this.get(t);return e?(this.sounds.delete(e.soundId),this.nameToId.delete(e.name),this.log(`Removed sound: ${e.name}`),!0):!1}clear(){let t=this.sounds.size;this.sounds.clear(),this.nameToId.clear(),this.log(`Cleared ${t} sounds`)}getStats(){let t=this.getAll(),e=0;for(let n of t){let{buffer:o}=n;e+=o.length*o.numberOfChannels*4}return{totalSounds:t.length,totalDuration:t.reduce((n,o)=>n+o.duration,0),estimatedMemory:e,bySource:{file:t.filter(n=>n.metadata.source==="file").length,external:t.filter(n=>n.metadata.source==="external").length,procedural:t.filter(n=>n.metadata.source==="procedural").length}}}log(t){this.debug&&console.warn(`[SoundBank] ${t}`)}};A(S,"SoundBank");var b=S;var v=class v{constructor(t={}){c(this,"audioContext",null);c(this,"masterGainNode",null);c(this,"soundBank",null);c(this,"options");c(this,"initialized",!1);c(this,"nextInstanceId",1);c(this,"instances",new Map);c(this,"instancesBySound",new Map);c(this,"activeSounds",new Set);c(this,"spatialSounds",new Map);c(this,"spatialConfig",{listenerX:32768,listenerY:32768,maxDistance:16384,referenceDistance:1024,rolloffFactor:1,panSpread:.8});this.options={debug:t.debug??!1,masterVolume:t.masterVolume??1},this.log("AudioManager created")}initialize(){if(this.initialized&&this.audioContext)return this.log("AudioContext already initialized"),!0;try{let t=window.AudioContext||window.webkitAudioContext;return t?(this.audioContext=new t,this.log(`AudioContext initialized (state: ${this.audioContext.state}, sampleRate: ${this.audioContext.sampleRate}Hz)`),this.masterGainNode=this.audioContext.createGain(),this.masterGainNode.gain.value=this.options.masterVolume,this.masterGainNode.connect(this.audioContext.destination),this.audioContext.state==="suspended"&&this.audioContext.resume().then(()=>{this.log("AudioContext resumed")}),this.soundBank=new b(this.audioContext,this.options.debug),this.initialized=!0,!0):(console.warn("[AudioManager] Web Audio API not supported"),!1)}catch(t){return console.error("[AudioManager] Failed to initialize AudioContext:",t),!1}}isInitialized(){return this.initialized&&this.audioContext!==null}getContext(){return this.audioContext}getMasterGain(){return this.masterGainNode}setMasterVolume(t){this.options.masterVolume=Math.max(0,Math.min(1,t)),this.masterGainNode&&(this.masterGainNode.gain.value=this.options.masterVolume),this.log(`Master volume set to ${this.options.masterVolume}`)}getMasterVolume(){return this.options.masterVolume}playStartSound(){if(!this.audioContext||!this.masterGainNode){this.log("Cannot play sound: AudioContext not initialized");return}try{let t=this.audioContext,e=t.currentTime,n=[262,330,392,523],o=.06,i=.07;n.forEach((r,d)=>{let a=e+d*i,s=t.createOscillator();s.type="square",s.frequency.setValueAtTime(r,a);let u=t.createGain();u.gain.setValueAtTime(0,a),u.gain.linearRampToValueAtTime(.15,a+.005),u.gain.setValueAtTime(.15,a+o-.01),u.gain.linearRampToValueAtTime(0,a+o),s.connect(u),u.connect(this.masterGainNode),s.start(a),s.stop(a+o)}),this.log("Start sound played (retro arpeggio)")}catch(t){console.error("[AudioManager] Failed to play start sound:",t)}}playTone(t,e,n={}){if(!this.audioContext||!this.masterGainNode){this.log("Cannot play tone: AudioContext not initialized");return}let{type:o="sine",volume:i=.3,attack:r=.01,release:d=.1}=n;try{let a=this.audioContext,s=a.currentTime,u=a.createOscillator();u.type=o,u.frequency.setValueAtTime(t,s);let l=a.createGain();l.gain.setValueAtTime(0,s),l.gain.linearRampToValueAtTime(i,s+r),l.gain.setValueAtTime(i,s+e-d),l.gain.linearRampToValueAtTime(0,s+e),u.connect(l),l.connect(this.masterGainNode),u.start(s),u.stop(s+e),this.log(`Tone played: ${t}Hz for ${e}s`)}catch(a){console.error("[AudioManager] Failed to play tone:",a)}}playNote(t,e,n={}){let o=this.noteToFrequency(t);if(o===null){console.warn(`[AudioManager] Invalid note: ${t}`);return}this.playTone(o,e,n)}noteToFrequency(t){let e=/^([A-Ga-g])([#b]?)(\d)$/,n=t.match(e);if(!n)return null;let[,o,i,r]=n,d=parseInt(r,10),s={C:0,D:2,E:4,F:5,G:7,A:9,B:11}[o.toUpperCase()];if(s===void 0)return null;i==="#"?s+=1:i==="b"&&(s-=1);let u=s+(d+1)*12;return 440*Math.pow(2,(u-49)/12)}async resumeContext(){this.audioContext&&this.audioContext.state==="suspended"&&(await this.audioContext.resume(),this.log("AudioContext resumed"))}async suspendContext(){this.audioContext&&this.audioContext.state==="running"&&(await this.audioContext.suspend(),this.log("AudioContext suspended"))}getState(){return this.audioContext?.state??null}getSoundBank(){return this.soundBank}play(t,e={}){if(!this.audioContext||!this.masterGainNode||!this.soundBank)return this.log("Cannot play: not initialized"),null;let n=this.soundBank.get(t);if(!n)return console.warn(`[AudioManager] Sound not found: ${t}`),null;try{let o=this.audioContext,i=e.instanceId??this.nextInstanceId++;e.instanceId!==void 0&&e.instanceId>=this.nextInstanceId&&(this.nextInstanceId=e.instanceId+1);let r=o.createBufferSource();r.buffer=n.buffer,r.loop=e.loop??!1,r.playbackRate.value=e.pitch??1;let d=o.createGain(),a=e.volume??1,s=e.fadeIn??0,u={source:r,gainNode:d,soundName:n.name,stopped:!1,baseVolume:a,targetVolume:a,fadingOut:!1,paused:!1,pausedAt:0,startedAt:o.currentTime,loop:e.loop??!1,playbackRate:e.pitch??1,buffer:n.buffer},l=a,f=a;if(e.position){let{pan:p,distanceVolume:h}=this.calculateSpatialAudio(e.position.x,e.position.y),g=o.createStereoPanner();g.pan.value=p,l=s>0?0:a*h,f=a*h,d.gain.setValueAtTime(l,o.currentTime),s>0&&(d.gain.linearRampToValueAtTime(f,o.currentTime+s),this.log(`Fade in: ${n.name} [#${i}] over ${s}s`)),r.connect(d),d.connect(g),g.connect(this.masterGainNode),u.pannerNode=g,u.position={x:e.position.x,y:e.position.y},u.targetVolume=f,this.spatialSounds.set(r,{gainNode:d,pannerNode:g,position:{x:e.position.x,y:e.position.y},baseVolume:a}),this.log(`Playing spatial: ${n.name} [#${i}] at (${e.position.x}, ${e.position.y}) pan=${p.toFixed(2)} vol=${h.toFixed(2)}`)}else l=s>0?0:a,f=a,d.gain.setValueAtTime(l,o.currentTime),s>0&&(d.gain.linearRampToValueAtTime(f,o.currentTime+s),this.log(`Fade in: ${n.name} [#${i}] over ${s}s`)),r.connect(d),d.connect(this.masterGainNode);this.instances.set(i,u),this.instancesBySound.has(n.name)||this.instancesBySound.set(n.name,new Set),this.instancesBySound.get(n.name).add(i),this.activeSounds.add(r),r.onended=()=>{u.paused||this.cleanupInstance(i)},r.start(0,e.offset??0),e.position||this.log(`Playing: ${n.name} [#${i}]`);let y=this.stop.bind(this),x=this.instances;return{instanceId:i,stop:()=>{y(i)},get playing(){let p=x.get(i);return p!==void 0&&!p.stopped},sound:n}}catch(o){return console.error(`[AudioManager] Failed to play sound "${t}":`,o),null}}stop(t){return t==="all"?this.stopAll():typeof t=="number"?this.stopInstance(t)?1:0:this.stopByName(t)}fadeOut(t,e){if(!this.audioContext)return 0;let n=Math.max(.01,e);return t==="all"?this.fadeOutAll(n):typeof t=="number"?this.fadeOutInstance(t,n)?1:0:this.fadeOutByName(t,n)}fadeOutInstance(t,e){let n=this.instances.get(t);if(!n||n.stopped||n.fadingOut||!this.audioContext)return!1;let o=this.audioContext;n.fadingOut=!0;let i=n.gainNode.gain.value;return n.gainNode.gain.cancelScheduledValues(o.currentTime),n.gainNode.gain.setValueAtTime(i,o.currentTime),n.gainNode.gain.linearRampToValueAtTime(0,o.currentTime+e),setTimeout(()=>{this.stopInstance(t)},e*1e3),this.log(`Fading out instance #${t} (${n.soundName}) over ${e}s`),!0}fadeOutByName(t,e){let n=this.instancesBySound.get(t);if(!n||n.size===0)return 0;let o=0;for(let i of Array.from(n))this.fadeOutInstance(i,e)&&o++;return this.log(`Fading out ${o} instances of "${t}" over ${e}s`),o}fadeOutAll(t){let e=0;for(let n of Array.from(this.instances.keys()))this.fadeOutInstance(n,t)&&e++;return this.log(`Fading out all sounds (${e} instances) over ${t}s`),e}pause(t){return t==="all"?this.pauseAll():typeof t=="number"?this.pauseInstance(t)?1:0:this.pauseByName(t)}pauseInstance(t){let e=this.instances.get(t);if(!e||e.stopped||e.paused||e.fadingOut||!this.audioContext)return!1;let o=(this.audioContext.currentTime-e.startedAt)*e.playbackRate;if(e.loop)e.pausedAt=o%e.buffer.duration;else if(e.pausedAt=o,e.pausedAt>=e.buffer.duration)return!1;try{e.source.stop()}catch{}return e.paused=!0,this.log(`Paused instance #${t} (${e.soundName}) at ${e.pausedAt.toFixed(2)}s`),!0}pauseByName(t){let e=this.instancesBySound.get(t);if(!e||e.size===0)return 0;let n=0;for(let o of Array.from(e))this.pauseInstance(o)&&n++;return this.log(`Paused ${n} instances of "${t}"`),n}pauseAll(){let t=0;for(let e of Array.from(this.instances.keys()))this.pauseInstance(e)&&t++;return this.log(`Paused all sounds (${t} instances)`),t}resume(t){return t==="all"?this.resumeAll():typeof t=="number"?this.resumeInstance(t)?1:0:this.resumeByName(t)}resumeInstance(t){let e=this.instances.get(t);if(!e||e.stopped||!e.paused||!this.audioContext||!this.masterGainNode)return!1;let n=this.audioContext,o=n.createBufferSource();return o.buffer=e.buffer,o.loop=e.loop,o.playbackRate.value=e.playbackRate,o.connect(e.gainNode),e.source=o,e.paused=!1,e.startedAt=n.currentTime-e.pausedAt/e.playbackRate,o.onended=()=>{e.paused||this.cleanupInstance(t)},o.start(0,e.pausedAt),this.log(`Resumed instance #${t} (${e.soundName}) from ${e.pausedAt.toFixed(2)}s`),!0}resumeByName(t){let e=this.instancesBySound.get(t);if(!e||e.size===0)return 0;let n=0;for(let o of Array.from(e))this.resumeInstance(o)&&n++;return this.log(`Resumed ${n} instances of "${t}"`),n}resumeAll(){let t=0;for(let e of Array.from(this.instances.keys()))this.resumeInstance(e)&&t++;return this.log(`Resumed all sounds (${t} instances)`),t}stopInstance(t){let e=this.instances.get(t);if(!e||e.stopped)return!1;try{e.source.stop(),e.stopped=!0,this.log(`Stopped instance #${t} (${e.soundName})`)}catch{}return this.cleanupInstance(t),!0}stopByName(t){let e=this.instancesBySound.get(t);if(!e||e.size===0)return 0;let n=0;for(let o of Array.from(e))this.stopInstance(o)&&n++;return this.log(`Stopped ${n} instances of "${t}"`),n}cleanupInstance(t){let e=this.instances.get(t);if(!e)return;this.instances.delete(t);let n=this.instancesBySound.get(e.soundName);n&&(n.delete(t),n.size===0&&this.instancesBySound.delete(e.soundName)),this.activeSounds.delete(e.source),this.spatialSounds.delete(e.source)}setListenerPosition(t,e){t===this.spatialConfig.listenerX&&e===this.spatialConfig.listenerY||(this.spatialConfig.listenerX=t,this.spatialConfig.listenerY=e,this.spatialSounds.size>0&&this.updateAllSpatialSounds())}updateAllSpatialSounds(){for(let[t,e]of this.spatialSounds){if(!this.activeSounds.has(t)){this.spatialSounds.delete(t);continue}let{pan:n,distanceVolume:o}=this.calculateSpatialAudio(e.position.x,e.position.y);e.pannerNode.pan.value=n,e.gainNode.gain.value=e.baseVolume*o}}getListenerPosition(){return{x:this.spatialConfig.listenerX,y:this.spatialConfig.listenerY}}configureSpatial(t){t.listenerX!==void 0&&(this.spatialConfig.listenerX=Math.max(0,Math.min(65535,t.listenerX))),t.listenerY!==void 0&&(this.spatialConfig.listenerY=Math.max(0,Math.min(65535,t.listenerY))),t.maxDistance!==void 0&&(this.spatialConfig.maxDistance=Math.max(1,t.maxDistance)),t.referenceDistance!==void 0&&(this.spatialConfig.referenceDistance=Math.max(1,t.referenceDistance)),t.rolloffFactor!==void 0&&(this.spatialConfig.rolloffFactor=Math.max(0,t.rolloffFactor)),t.panSpread!==void 0&&(this.spatialConfig.panSpread=Math.max(0,Math.min(1,t.panSpread)))}getSpatialConfig(){return{...this.spatialConfig}}calculateSpatialAudio(t,e){let{listenerX:n,listenerY:o,maxDistance:i,referenceDistance:r,panSpread:d}=this.spatialConfig,a=t-n,s=e-o,u=Math.sqrt(a*a+s*s),l;u<=r?l=1:u>=i?l=0:l=1-(u-r)/(i-r);let f=0;return i>0&&d>0&&(f=a/i*d,f=Math.max(-1,Math.min(1,f))),this.log(`Spatial: listener(${n.toFixed(1)}, ${o.toFixed(1)}) sound(${t.toFixed(1)}, ${e.toFixed(1)}) dist=${u.toFixed(1)} vol=${l.toFixed(2)} pan=${f.toFixed(2)}`),{pan:f,distanceVolume:l}}stopAll(){let t=this.instances.size;for(let[,e]of this.instances)if(!e.stopped)try{e.source.stop(),e.stopped=!0}catch{}return this.instances.clear(),this.instancesBySound.clear(),this.activeSounds.clear(),this.spatialSounds.clear(),this.log(`Stopped all sounds (${t} instances)`),t}getPlayingCount(){return this.instances.size}getActiveInstances(){return Array.from(this.instances.keys())}isPlaying(t){let e=this.instances.get(t);return e!==void 0&&!e.stopped}destroy(){this.stopAll(),this.soundBank&&(this.soundBank.clear(),this.soundBank=null),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.masterGainNode=null,this.initialized=!1,this.log("AudioManager destroyed")}log(t){this.options.debug&&console.warn(`[AudioManager] ${t}`)}};A(v,"AudioManager");var C=v;export{C as AudioManager,b as SoundBank};