audioq 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js ADDED
@@ -0,0 +1,591 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Core queue management functions for the audioq package
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.destroyAllChannels = exports.destroyChannel = exports.stopAllAudio = exports.stopAllAudioInChannel = exports.stopCurrentAudioInChannel = exports.playAudioQueue = exports.queueAudioPriority = exports.queueAudio = exports.setChannelQueueLimit = exports.getQueueConfig = exports.setQueueConfig = void 0;
16
+ const types_1 = require("./types");
17
+ const info_1 = require("./info");
18
+ const utils_1 = require("./utils");
19
+ const events_1 = require("./events");
20
+ const volume_1 = require("./volume");
21
+ const errors_1 = require("./errors");
22
+ /**
23
+ * Global queue configuration
24
+ */
25
+ let globalQueueConfig = {
26
+ defaultMaxQueueSize: undefined, // unlimited by default
27
+ dropOldestWhenFull: false,
28
+ showQueueWarnings: true
29
+ };
30
+ /**
31
+ * Operation lock timeout in milliseconds
32
+ */
33
+ const OPERATION_LOCK_TIMEOUT = 100;
34
+ /**
35
+ * Acquires an operation lock for a channel to prevent race conditions
36
+ * @param channelNumber - The channel number to lock
37
+ * @param operationName - Name of the operation for debugging
38
+ * @returns Promise that resolves when lock is acquired
39
+ * @internal
40
+ */
41
+ const acquireChannelLock = (channelNumber, operationName) => __awaiter(void 0, void 0, void 0, function* () {
42
+ const channel = info_1.audioChannels[channelNumber];
43
+ if (!channel)
44
+ return;
45
+ const startTime = Date.now();
46
+ // Wait for any existing lock to be released
47
+ while (channel.isLocked) {
48
+ // Prevent infinite waiting with timeout
49
+ if (Date.now() - startTime > OPERATION_LOCK_TIMEOUT) {
50
+ // eslint-disable-next-line no-console
51
+ console.warn(`Operation lock timeout for channel ${channelNumber} during ${operationName}. ` +
52
+ `Forcibly acquiring lock.`);
53
+ break;
54
+ }
55
+ // Small delay to prevent tight polling
56
+ yield new Promise((resolve) => setTimeout(resolve, 10));
57
+ }
58
+ channel.isLocked = true;
59
+ });
60
+ /**
61
+ * Releases an operation lock for a channel
62
+ * @param channelNumber - The channel number to unlock
63
+ * @internal
64
+ */
65
+ const releaseChannelLock = (channelNumber) => {
66
+ const channel = info_1.audioChannels[channelNumber];
67
+ if (channel) {
68
+ channel.isLocked = false;
69
+ }
70
+ };
71
+ /**
72
+ * Executes an operation with channel lock protection
73
+ * @param channelNumber - The channel number to operate on
74
+ * @param operationName - Name of the operation for debugging
75
+ * @param operation - The operation to execute
76
+ * @returns Promise that resolves with the operation result
77
+ * @internal
78
+ */
79
+ const withChannelLock = (channelNumber, operationName, operation) => __awaiter(void 0, void 0, void 0, function* () {
80
+ try {
81
+ yield acquireChannelLock(channelNumber, operationName);
82
+ return yield operation();
83
+ }
84
+ finally {
85
+ releaseChannelLock(channelNumber);
86
+ }
87
+ });
88
+ /**
89
+ * Sets the global queue configuration
90
+ * @param config - Queue configuration options
91
+ * @example
92
+ * ```typescript
93
+ * setQueueConfig({
94
+ * defaultMaxQueueSize: 50,
95
+ * dropOldestWhenFull: true,
96
+ * showQueueWarnings: true
97
+ * });
98
+ * ```
99
+ */
100
+ const setQueueConfig = (config) => {
101
+ globalQueueConfig = Object.assign(Object.assign({}, globalQueueConfig), config);
102
+ };
103
+ exports.setQueueConfig = setQueueConfig;
104
+ /**
105
+ * Gets the current global queue configuration
106
+ * @returns Current queue configuration
107
+ * @example
108
+ * ```typescript
109
+ * const config = getQueueConfig();
110
+ * console.log(`Default max queue size: ${config.defaultMaxQueueSize}`);
111
+ * ```
112
+ */
113
+ const getQueueConfig = () => {
114
+ return Object.assign({}, globalQueueConfig);
115
+ };
116
+ exports.getQueueConfig = getQueueConfig;
117
+ /**
118
+ * Sets the maximum queue size for a specific channel
119
+ * @param channelNumber - The channel number to configure
120
+ * @param maxSize - Maximum queue size (undefined for unlimited)
121
+ * @throws Error if the channel number exceeds the maximum allowed channels
122
+ * @example
123
+ * ```typescript
124
+ * setChannelQueueLimit(0, 25); // Limit channel 0 to 25 items
125
+ * setChannelQueueLimit(1, undefined); // Remove limit for channel 1
126
+ * ```
127
+ */
128
+ const setChannelQueueLimit = (channelNumber, maxSize) => {
129
+ // Validate channel number limits BEFORE creating any channels
130
+ if (channelNumber < 0) {
131
+ throw new Error('Channel number must be non-negative');
132
+ }
133
+ if (channelNumber >= types_1.MAX_CHANNELS) {
134
+ throw new Error(`Channel number ${channelNumber} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
135
+ }
136
+ // Ensure channel exists (now safe because we validated the limit above)
137
+ while (info_1.audioChannels.length <= channelNumber) {
138
+ info_1.audioChannels.push({
139
+ audioCompleteCallbacks: new Set(),
140
+ audioErrorCallbacks: new Set(),
141
+ audioPauseCallbacks: new Set(),
142
+ audioResumeCallbacks: new Set(),
143
+ audioStartCallbacks: new Set(),
144
+ isPaused: false,
145
+ progressCallbacks: new Map(),
146
+ queue: [],
147
+ queueChangeCallbacks: new Set(),
148
+ volume: 1.0
149
+ });
150
+ }
151
+ const channel = info_1.audioChannels[channelNumber];
152
+ channel.maxQueueSize = maxSize;
153
+ };
154
+ exports.setChannelQueueLimit = setChannelQueueLimit;
155
+ /**
156
+ * Checks if adding an item to the queue would exceed limits and handles the situation
157
+ * @param channel - The channel to check
158
+ * @param channelNumber - The channel number for logging
159
+ * @param maxQueueSize - Override max queue size from options
160
+ * @returns true if the item can be added, false otherwise
161
+ * @internal
162
+ */
163
+ const checkQueueLimit = (channel, channelNumber, maxQueueSize) => {
164
+ var _a;
165
+ // Determine the effective queue limit
166
+ const effectiveLimit = (_a = maxQueueSize !== null && maxQueueSize !== void 0 ? maxQueueSize : channel.maxQueueSize) !== null && _a !== void 0 ? _a : globalQueueConfig.defaultMaxQueueSize;
167
+ if (effectiveLimit === undefined) {
168
+ return true; // No limit set
169
+ }
170
+ if (channel.queue.length < effectiveLimit) {
171
+ return true; // Within limits
172
+ }
173
+ // Queue is at or over the limit
174
+ if (globalQueueConfig.showQueueWarnings) {
175
+ // eslint-disable-next-line no-console
176
+ console.warn(`Queue limit reached for channel ${channelNumber}. ` +
177
+ `Current size: ${channel.queue.length}, Limit: ${effectiveLimit}`);
178
+ }
179
+ if (globalQueueConfig.dropOldestWhenFull) {
180
+ // Remove oldest item (but not currently playing)
181
+ if (channel.queue.length > 1) {
182
+ const removedAudio = channel.queue.splice(1, 1)[0];
183
+ (0, events_1.cleanupProgressTracking)(removedAudio, channelNumber, info_1.audioChannels);
184
+ if (globalQueueConfig.showQueueWarnings) {
185
+ // eslint-disable-next-line no-console
186
+ console.warn(`Dropped oldest queued item to make room for new audio`);
187
+ }
188
+ return true;
189
+ }
190
+ }
191
+ // Cannot add - queue is full and not dropping oldest
192
+ return false;
193
+ };
194
+ /**
195
+ * Queues an audio file to a specific channel and starts playing if it's the first in queue
196
+ * @param audioUrl - The URL of the audio file to queue
197
+ * @param channelNumber - The channel number to queue the audio to (defaults to 0)
198
+ * @param options - Optional configuration for the audio file
199
+ * @returns Promise that resolves when the audio is queued and starts playing (if first in queue)
200
+ * @throws Error if the audio URL is invalid or potentially malicious
201
+ * @throws Error if the channel number exceeds the maximum allowed channels
202
+ * @throws Error if the queue size limit would be exceeded
203
+ * @example
204
+ * ```typescript
205
+ * await queueAudio('https://example.com/song.mp3', 0);
206
+ * await queueAudio('./sounds/notification.wav'); // Uses default channel 0
207
+ * await queueAudio('./music/loop.mp3', 1, { loop: true }); // Loop the audio
208
+ * await queueAudio('./urgent.wav', 0, { addToFront: true }); // Add to front of queue
209
+ * await queueAudio('./limited.mp3', 0, { maxQueueSize: 10 }); // Limit this queue to 10 items
210
+ * ```
211
+ */
212
+ const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...args_1], void 0, function* (audioUrl, channelNumber = 0, options) {
213
+ // Validate the URL for security
214
+ const validatedUrl = (0, utils_1.validateAudioUrl)(audioUrl);
215
+ // Check channel number limits
216
+ if (channelNumber < 0) {
217
+ throw new Error('Channel number must be non-negative');
218
+ }
219
+ if (channelNumber >= types_1.MAX_CHANNELS) {
220
+ throw new Error(`Channel number ${channelNumber} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
221
+ }
222
+ // Ensure the channel exists
223
+ while (info_1.audioChannels.length <= channelNumber) {
224
+ info_1.audioChannels.push({
225
+ audioCompleteCallbacks: new Set(),
226
+ audioErrorCallbacks: new Set(),
227
+ audioPauseCallbacks: new Set(),
228
+ audioResumeCallbacks: new Set(),
229
+ audioStartCallbacks: new Set(),
230
+ isPaused: false,
231
+ progressCallbacks: new Map(),
232
+ queue: [],
233
+ queueChangeCallbacks: new Set(),
234
+ volume: 1.0
235
+ });
236
+ }
237
+ const channel = info_1.audioChannels[channelNumber];
238
+ // Check queue size limits before creating audio element
239
+ if (!checkQueueLimit(channel, channelNumber, options === null || options === void 0 ? void 0 : options.maxQueueSize)) {
240
+ throw new Error(`Queue size limit exceeded for channel ${channelNumber}`);
241
+ }
242
+ const audio = new Audio(validatedUrl);
243
+ // Set up comprehensive error handling
244
+ (0, errors_1.setupAudioErrorHandling)(audio, channelNumber, validatedUrl, (error) => __awaiter(void 0, void 0, void 0, function* () {
245
+ yield (0, errors_1.handleAudioError)(audio, channelNumber, validatedUrl, error);
246
+ }));
247
+ // Initialize Web Audio API support if needed
248
+ yield (0, volume_1.initializeWebAudioForAudio)(audio, channelNumber);
249
+ // Apply options if provided
250
+ if (options) {
251
+ if (typeof options.loop === 'boolean') {
252
+ audio.loop = options.loop;
253
+ }
254
+ if (typeof options.volume === 'number' && !isNaN(options.volume)) {
255
+ const clampedVolume = Math.max(0, Math.min(1, options.volume));
256
+ audio.volume = clampedVolume;
257
+ // Set channel volume to match the audio volume
258
+ channel.volume = clampedVolume;
259
+ }
260
+ // Set channel-specific queue limit if provided
261
+ if (typeof options.maxQueueSize === 'number') {
262
+ channel.maxQueueSize = options.maxQueueSize;
263
+ }
264
+ }
265
+ // Handle addToFront option
266
+ const shouldAddToFront = options === null || options === void 0 ? void 0 : options.addToFront;
267
+ // Add to queue based on addToFront option
268
+ if (shouldAddToFront && channel.queue.length > 0) {
269
+ // Insert after currently playing track (at index 1)
270
+ channel.queue.splice(1, 0, audio);
271
+ }
272
+ else if (shouldAddToFront) {
273
+ // If queue is empty, add to front
274
+ channel.queue.unshift(audio);
275
+ }
276
+ else {
277
+ // Add to back of queue
278
+ channel.queue.push(audio);
279
+ }
280
+ // Emit queue change event
281
+ (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
282
+ // Start playing if this is the first item and channel isn't paused
283
+ if (channel.queue.length === 1 && !channel.isPaused) {
284
+ // Await the audio setup to complete before resolving queueAudio
285
+ yield (0, exports.playAudioQueue)(channelNumber);
286
+ }
287
+ });
288
+ exports.queueAudio = queueAudio;
289
+ /**
290
+ * Adds an audio file to the front of the queue in a specific channel
291
+ * This is a convenience function that places the audio right after the currently playing track
292
+ * @param audioUrl - The URL of the audio file to queue
293
+ * @param channelNumber - The channel number to queue the audio to (defaults to 0)
294
+ * @param options - Optional configuration for the audio file
295
+ * @returns Promise that resolves when the audio is queued
296
+ * @example
297
+ * ```typescript
298
+ * await queueAudioPriority('./urgent-announcement.wav', 0);
299
+ * await queueAudioPriority('./priority-sound.mp3', 1, { loop: true });
300
+ * ```
301
+ */
302
+ const queueAudioPriority = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...args_1], void 0, function* (audioUrl, channelNumber = 0, options) {
303
+ const priorityOptions = Object.assign(Object.assign({}, options), { addToFront: true });
304
+ return (0, exports.queueAudio)(audioUrl, channelNumber, priorityOptions);
305
+ });
306
+ exports.queueAudioPriority = queueAudioPriority;
307
+ /**
308
+ * Plays the audio queue for a specific channel
309
+ * @param channelNumber - The channel number to play
310
+ * @returns Promise that resolves when the audio starts playing (setup complete)
311
+ * @example
312
+ * ```typescript
313
+ * await playAudioQueue(0); // Start playing queue for channel 0
314
+ * ```
315
+ */
316
+ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, function* () {
317
+ const channel = info_1.audioChannels[channelNumber];
318
+ if (!channel || channel.queue.length === 0)
319
+ return;
320
+ const currentAudio = channel.queue[0];
321
+ // Apply channel volume with global volume multiplier if not already set
322
+ if (currentAudio.volume === 1.0 && channel.volume !== undefined) {
323
+ const globalVolume = (0, volume_1.getGlobalVolume)();
324
+ currentAudio.volume = channel.volume * globalVolume;
325
+ }
326
+ (0, events_1.setupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
327
+ // Apply volume ducking when audio starts
328
+ yield (0, volume_1.applyVolumeDucking)(channelNumber);
329
+ return new Promise((resolve) => {
330
+ let hasStarted = false;
331
+ let metadataLoaded = false;
332
+ let playStarted = false;
333
+ let setupComplete = false;
334
+ // Check if we should fire onAudioStart (both conditions met)
335
+ const tryFireAudioStart = () => {
336
+ if (!hasStarted && metadataLoaded && playStarted) {
337
+ hasStarted = true;
338
+ (0, events_1.emitAudioStart)(channelNumber, {
339
+ channelNumber,
340
+ duration: currentAudio.duration * 1000,
341
+ fileName: (0, utils_1.extractFileName)(currentAudio.src),
342
+ src: currentAudio.src
343
+ }, info_1.audioChannels);
344
+ // Resolve setup promise when audio start event is fired
345
+ if (!setupComplete) {
346
+ setupComplete = true;
347
+ resolve();
348
+ }
349
+ }
350
+ };
351
+ // Event handler for when metadata loads (duration becomes available)
352
+ const handleLoadedMetadata = () => {
353
+ metadataLoaded = true;
354
+ tryFireAudioStart();
355
+ };
356
+ // Event handler for when audio actually starts playing
357
+ const handlePlay = () => {
358
+ playStarted = true;
359
+ tryFireAudioStart();
360
+ };
361
+ // Event handler for when audio ends
362
+ const handleEnded = () => __awaiter(void 0, void 0, void 0, function* () {
363
+ (0, events_1.emitAudioComplete)(channelNumber, {
364
+ channelNumber,
365
+ fileName: (0, utils_1.extractFileName)(currentAudio.src),
366
+ remainingInQueue: channel.queue.length - 1,
367
+ src: currentAudio.src
368
+ }, info_1.audioChannels);
369
+ // Handle looping vs non-looping audio
370
+ if (currentAudio.loop) {
371
+ // For looping audio, keep in queue and try to restart playback
372
+ currentAudio.currentTime = 0;
373
+ try {
374
+ yield currentAudio.play();
375
+ }
376
+ catch (error) {
377
+ yield (0, errors_1.handleAudioError)(currentAudio, channelNumber, currentAudio.src, error);
378
+ }
379
+ }
380
+ else {
381
+ // For non-looping audio, remove from queue and play next
382
+ currentAudio.pause();
383
+ (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
384
+ (0, volume_1.cleanupWebAudioForAudio)(currentAudio, channelNumber);
385
+ channel.queue.shift();
386
+ channel.isPaused = false; // Reset pause state
387
+ // Restore volume levels AFTER removing audio from queue
388
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
389
+ (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
390
+ // Play next audio immediately if there's more in queue
391
+ yield (0, exports.playAudioQueue)(channelNumber);
392
+ }
393
+ });
394
+ // Add event listeners
395
+ currentAudio.addEventListener('loadedmetadata', handleLoadedMetadata);
396
+ currentAudio.addEventListener('play', handlePlay);
397
+ currentAudio.addEventListener('ended', handleEnded);
398
+ // Check if metadata is already loaded (in case it loads before we add the listener)
399
+ if (currentAudio.readyState >= 1) {
400
+ metadataLoaded = true;
401
+ }
402
+ // Enhanced play with error handling
403
+ currentAudio.play().catch((error) => __awaiter(void 0, void 0, void 0, function* () {
404
+ yield (0, errors_1.handleAudioError)(currentAudio, channelNumber, currentAudio.src, error);
405
+ if (!setupComplete) {
406
+ setupComplete = true;
407
+ resolve(); // Resolve gracefully instead of rejecting
408
+ }
409
+ }));
410
+ });
411
+ });
412
+ exports.playAudioQueue = playAudioQueue;
413
+ /**
414
+ * Stops the currently playing audio in a specific channel and plays the next audio in queue
415
+ * @param channelNumber - The channel number (defaults to 0)
416
+ * @example
417
+ * ```typescript
418
+ * await stopCurrentAudioInChannel(); // Stop current audio in default channel (0)
419
+ * await stopCurrentAudioInChannel(1); // Stop current audio in channel 1
420
+ * ```
421
+ */
422
+ const stopCurrentAudioInChannel = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (channelNumber = 0) {
423
+ const channel = info_1.audioChannels[channelNumber];
424
+ if (channel && channel.queue.length > 0) {
425
+ const currentAudio = channel.queue[0];
426
+ (0, events_1.emitAudioComplete)(channelNumber, {
427
+ channelNumber,
428
+ fileName: (0, utils_1.extractFileName)(currentAudio.src),
429
+ remainingInQueue: channel.queue.length - 1,
430
+ src: currentAudio.src
431
+ }, info_1.audioChannels);
432
+ currentAudio.pause();
433
+ (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
434
+ channel.queue.shift();
435
+ channel.isPaused = false; // Reset pause state
436
+ // Restore volume levels AFTER removing from queue (so queue.length check works correctly)
437
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
438
+ (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
439
+ // Start next audio immediately if there's more in queue
440
+ if (channel.queue.length > 0) {
441
+ // eslint-disable-next-line no-console
442
+ (0, exports.playAudioQueue)(channelNumber).catch(console.error);
443
+ }
444
+ }
445
+ });
446
+ exports.stopCurrentAudioInChannel = stopCurrentAudioInChannel;
447
+ /**
448
+ * Stops all audio in a specific channel and clears the entire queue
449
+ * @param channelNumber - The channel number (defaults to 0)
450
+ * @example
451
+ * ```typescript
452
+ * await stopAllAudioInChannel(); // Clear all audio in default channel (0)
453
+ * await stopAllAudioInChannel(1); // Clear all audio in channel 1
454
+ * ```
455
+ */
456
+ const stopAllAudioInChannel = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (channelNumber = 0) {
457
+ return withChannelLock(channelNumber, 'stopAllAudioInChannel', () => __awaiter(void 0, void 0, void 0, function* () {
458
+ const channel = info_1.audioChannels[channelNumber];
459
+ if (channel) {
460
+ if (channel.queue.length > 0) {
461
+ const currentAudio = channel.queue[0];
462
+ (0, events_1.emitAudioComplete)(channelNumber, {
463
+ channelNumber,
464
+ fileName: (0, utils_1.extractFileName)(currentAudio.src),
465
+ remainingInQueue: 0, // Will be 0 since we're clearing the queue
466
+ src: currentAudio.src
467
+ }, info_1.audioChannels);
468
+ // Restore volume levels when stopping
469
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
470
+ currentAudio.pause();
471
+ (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
472
+ }
473
+ // Clean up all progress tracking for this channel
474
+ channel.queue.forEach((audio) => (0, events_1.cleanupProgressTracking)(audio, channelNumber, info_1.audioChannels));
475
+ channel.queue = [];
476
+ channel.isPaused = false; // Reset pause state
477
+ (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
478
+ }
479
+ }));
480
+ });
481
+ exports.stopAllAudioInChannel = stopAllAudioInChannel;
482
+ /**
483
+ * Stops all audio across all channels and clears all queues
484
+ * @example
485
+ * ```typescript
486
+ * await stopAllAudio(); // Emergency stop - clears everything
487
+ * ```
488
+ */
489
+ const stopAllAudio = () => __awaiter(void 0, void 0, void 0, function* () {
490
+ const stopPromises = [];
491
+ info_1.audioChannels.forEach((_channel, index) => {
492
+ stopPromises.push((0, exports.stopAllAudioInChannel)(index));
493
+ });
494
+ yield Promise.all(stopPromises);
495
+ });
496
+ exports.stopAllAudio = stopAllAudio;
497
+ /**
498
+ * Completely destroys a channel and cleans up all associated resources
499
+ * This stops all audio, cancels transitions, clears callbacks, and removes the channel
500
+ * @param channelNumber - The channel number to destroy (defaults to 0)
501
+ * @example
502
+ * ```typescript
503
+ * await destroyChannel(1); // Completely removes channel 1 and cleans up resources
504
+ * ```
505
+ */
506
+ const destroyChannel = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (channelNumber = 0) {
507
+ const channel = info_1.audioChannels[channelNumber];
508
+ if (!channel)
509
+ return;
510
+ // Comprehensive cleanup of all audio elements in the queue
511
+ if (channel.queue && channel.queue.length > 0) {
512
+ channel.queue.forEach((audio) => {
513
+ // Properly clean up each audio element
514
+ const cleanAudio = audio;
515
+ cleanAudio.pause();
516
+ cleanAudio.currentTime = 0;
517
+ // Remove all event listeners if possible
518
+ if (cleanAudio.parentNode) {
519
+ cleanAudio.parentNode.removeChild(cleanAudio);
520
+ }
521
+ // Clean up audio attributes
522
+ cleanAudio.removeAttribute('src');
523
+ // Reset audio element state
524
+ if (cleanAudio.src) {
525
+ // Copy essential properties
526
+ cleanAudio.src = '';
527
+ try {
528
+ cleanAudio.load();
529
+ }
530
+ catch (_a) {
531
+ // Ignore load errors in tests (jsdom limitation)
532
+ }
533
+ }
534
+ });
535
+ }
536
+ // Stop all audio in the channel (this handles additional cleanup)
537
+ yield (0, exports.stopAllAudioInChannel)(channelNumber);
538
+ // Cancel any active volume transitions
539
+ (0, volume_1.cancelVolumeTransition)(channelNumber);
540
+ // Clear all callback sets completely
541
+ const callbackProperties = [
542
+ 'audioCompleteCallbacks',
543
+ 'audioErrorCallbacks',
544
+ 'audioPauseCallbacks',
545
+ 'audioResumeCallbacks',
546
+ 'audioStartCallbacks',
547
+ 'queueChangeCallbacks',
548
+ 'progressCallbacks'
549
+ ];
550
+ callbackProperties.forEach((prop) => {
551
+ if (channel[prop]) {
552
+ channel[prop].clear();
553
+ }
554
+ });
555
+ // Remove optional channel configuration
556
+ delete channel.fadeState;
557
+ delete channel.retryConfig;
558
+ // Reset required properties to clean state
559
+ channel.isPaused = false;
560
+ channel.volume = 1.0;
561
+ channel.queue = [];
562
+ // Remove the channel completely
563
+ delete info_1.audioChannels[channelNumber];
564
+ });
565
+ exports.destroyChannel = destroyChannel;
566
+ /**
567
+ * Destroys all channels and cleans up all resources
568
+ * This is useful for complete cleanup when the audio system is no longer needed
569
+ * @example
570
+ * ```typescript
571
+ * await destroyAllChannels(); // Complete cleanup - removes all channels
572
+ * ```
573
+ */
574
+ const destroyAllChannels = () => __awaiter(void 0, void 0, void 0, function* () {
575
+ const destroyPromises = [];
576
+ // Collect indices of existing channels
577
+ const channelIndices = [];
578
+ info_1.audioChannels.forEach((_channel, index) => {
579
+ if (info_1.audioChannels[index]) {
580
+ channelIndices.push(index);
581
+ }
582
+ });
583
+ // Destroy all channels in parallel
584
+ channelIndices.forEach((index) => {
585
+ destroyPromises.push((0, exports.destroyChannel)(index));
586
+ });
587
+ yield Promise.all(destroyPromises);
588
+ // Clear the entire array
589
+ info_1.audioChannels.length = 0;
590
+ });
591
+ exports.destroyAllChannels = destroyAllChannels;