@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.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/AudioManager.js +1058 -0
- package/dist/AudioManager.js.map +1 -0
- package/dist/SoundBank.js +286 -0
- package/dist/SoundBank.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +620 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +58 -0
package/dist/index.d.ts
ADDED
|
@@ -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};
|