audio-channel-queue 1.9.0 → 1.10.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/src/info.ts CHANGED
@@ -12,7 +12,8 @@ import {
12
12
  AudioPauseCallback,
13
13
  AudioResumeCallback,
14
14
  ExtendedAudioQueueChannel,
15
- GLOBAL_PROGRESS_KEY
15
+ GLOBAL_PROGRESS_KEY,
16
+ MAX_CHANNELS
16
17
  } from './types';
17
18
  import { getAudioInfoFromElement, createQueueSnapshot } from './utils';
18
19
  import { setupProgressTracking, cleanupProgressTracking } from './events';
@@ -20,8 +21,85 @@ import { setupProgressTracking, cleanupProgressTracking } from './events';
20
21
  /**
21
22
  * Global array to store audio channels with their queues and callback management
22
23
  * Each channel maintains its own audio queue and event callback sets
24
+ *
25
+ * Note: While you can inspect this array for debugging, direct modification is discouraged.
26
+ * Use the provided API functions for safe channel management.
23
27
  */
24
- export const audioChannels: ExtendedAudioQueueChannel[] = [];
28
+ export const audioChannels: ExtendedAudioQueueChannel[] = new Proxy(
29
+ [] as ExtendedAudioQueueChannel[],
30
+ {
31
+ deleteProperty(target: ExtendedAudioQueueChannel[], prop: string | symbol): boolean {
32
+ if (typeof prop === 'string' && !isNaN(Number(prop))) {
33
+ // eslint-disable-next-line no-console
34
+ console.warn(
35
+ 'Warning: Direct deletion from audioChannels detected. ' +
36
+ 'Consider using stopAllAudioInChannel() for proper cleanup.'
37
+ );
38
+ }
39
+ delete (target as unknown as Record<string, unknown>)[prop as string];
40
+ return true;
41
+ },
42
+ get(target: ExtendedAudioQueueChannel[], prop: string | symbol): unknown {
43
+ const value = (target as unknown as Record<string, unknown>)[prop as string];
44
+
45
+ // Return channel objects with warnings on modification attempts
46
+ if (
47
+ typeof value === 'object' &&
48
+ value !== null &&
49
+ typeof prop === 'string' &&
50
+ !isNaN(Number(prop))
51
+ ) {
52
+ return new Proxy(value as ExtendedAudioQueueChannel, {
53
+ set(
54
+ channelTarget: ExtendedAudioQueueChannel,
55
+ channelProp: string | symbol,
56
+ channelValue: unknown
57
+ ): boolean {
58
+ // Allow internal modifications but warn about direct property changes
59
+ if (
60
+ typeof channelProp === 'string' &&
61
+ !['queue', 'volume', 'isPaused', 'isLocked', 'volumeConfig'].includes(channelProp)
62
+ ) {
63
+ // eslint-disable-next-line no-console
64
+ console.warn(
65
+ `Warning: Direct modification of channel.${channelProp} detected. ` +
66
+ 'Use API functions for safer channel management.'
67
+ );
68
+ }
69
+ const key = typeof channelProp === 'symbol' ? channelProp.toString() : channelProp;
70
+ (channelTarget as unknown as Record<string, unknown>)[key] = channelValue;
71
+ return true;
72
+ }
73
+ });
74
+ }
75
+
76
+ return value;
77
+ },
78
+ set(target: ExtendedAudioQueueChannel[], prop: string | symbol, value: unknown): boolean {
79
+ // Allow normal array operations
80
+ const key = typeof prop === 'symbol' ? prop.toString() : prop;
81
+ (target as unknown as Record<string, unknown>)[key] = value;
82
+ return true;
83
+ }
84
+ }
85
+ );
86
+
87
+ /**
88
+ * Validates a channel number against MAX_CHANNELS limit
89
+ * @param channelNumber - The channel number to validate
90
+ * @throws Error if the channel number is invalid
91
+ * @internal
92
+ */
93
+ const validateChannelNumber = (channelNumber: number): void => {
94
+ if (channelNumber < 0) {
95
+ throw new Error('Channel number must be non-negative');
96
+ }
97
+ if (channelNumber >= MAX_CHANNELS) {
98
+ throw new Error(
99
+ `Channel number ${channelNumber} exceeds maximum allowed channels (${MAX_CHANNELS})`
100
+ );
101
+ }
102
+ };
25
103
 
26
104
  /**
27
105
  * Gets current audio information for a specific channel
@@ -71,18 +149,19 @@ export const getAllChannelsInfo = (): (AudioInfo | null)[] => {
71
149
 
72
150
  /**
73
151
  * Gets a complete snapshot of the queue state for a specific channel
74
- * @param channelNumber - The channel number
152
+ * @param channelNumber - The channel number (defaults to 0)
75
153
  * @returns QueueSnapshot object or null if channel doesn't exist
76
154
  * @example
77
155
  * ```typescript
78
- * const snapshot = getQueueSnapshot(0);
156
+ * const snapshot = getQueueSnapshot();
79
157
  * if (snapshot) {
80
158
  * console.log(`Queue has ${snapshot.totalItems} items`);
81
159
  * console.log(`Currently playing: ${snapshot.items[0]?.fileName}`);
82
160
  * }
161
+ * const channelSnapshot = getQueueSnapshot(2);
83
162
  * ```
84
163
  */
85
- export const getQueueSnapshot = (channelNumber: number): QueueSnapshot | null => {
164
+ export const getQueueSnapshot = (channelNumber: number = 0): QueueSnapshot | null => {
86
165
  return createQueueSnapshot(channelNumber, audioChannels);
87
166
  };
88
167
 
@@ -90,6 +169,7 @@ export const getQueueSnapshot = (channelNumber: number): QueueSnapshot | null =>
90
169
  * Subscribes to real-time progress updates for a specific channel
91
170
  * @param channelNumber - The channel number
92
171
  * @param callback - Function to call with audio info updates
172
+ * @throws Error if the channel number exceeds the maximum allowed channels
93
173
  * @example
94
174
  * ```typescript
95
175
  * onAudioProgress(0, (info) => {
@@ -99,6 +179,8 @@ export const getQueueSnapshot = (channelNumber: number): QueueSnapshot | null =>
99
179
  * ```
100
180
  */
101
181
  export const onAudioProgress = (channelNumber: number, callback: ProgressCallback): void => {
182
+ validateChannelNumber(channelNumber);
183
+
102
184
  if (!audioChannels[channelNumber]) {
103
185
  audioChannels[channelNumber] = {
104
186
  audioCompleteCallbacks: new Set(),
@@ -140,13 +222,14 @@ export const onAudioProgress = (channelNumber: number, callback: ProgressCallbac
140
222
 
141
223
  /**
142
224
  * Removes progress listeners for a specific channel
143
- * @param channelNumber - The channel number
225
+ * @param channelNumber - The channel number (defaults to 0)
144
226
  * @example
145
227
  * ```typescript
146
- * offAudioProgress(0); // Stop receiving progress updates for channel 0
228
+ * offAudioProgress();
229
+ * offAudioProgress(1); // Stop receiving progress updates for channel 1
147
230
  * ```
148
231
  */
149
- export const offAudioProgress = (channelNumber: number): void => {
232
+ export function offAudioProgress(channelNumber: number = 0): void {
150
233
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
151
234
  if (!channel?.progressCallbacks) return;
152
235
 
@@ -158,12 +241,13 @@ export const offAudioProgress = (channelNumber: number): void => {
158
241
 
159
242
  // Clear all callbacks for this channel
160
243
  channel.progressCallbacks.clear();
161
- };
244
+ }
162
245
 
163
246
  /**
164
247
  * Subscribes to queue change events for a specific channel
165
248
  * @param channelNumber - The channel number to monitor
166
249
  * @param callback - Function to call when queue changes
250
+ * @throws Error if the channel number exceeds the maximum allowed channels
167
251
  * @example
168
252
  * ```typescript
169
253
  * onQueueChange(0, (snapshot) => {
@@ -173,6 +257,8 @@ export const offAudioProgress = (channelNumber: number): void => {
173
257
  * ```
174
258
  */
175
259
  export const onQueueChange = (channelNumber: number, callback: QueueChangeCallback): void => {
260
+ validateChannelNumber(channelNumber);
261
+
176
262
  if (!audioChannels[channelNumber]) {
177
263
  audioChannels[channelNumber] = {
178
264
  audioCompleteCallbacks: new Set(),
@@ -215,6 +301,7 @@ export const offQueueChange = (channelNumber: number): void => {
215
301
  * Subscribes to audio start events for a specific channel
216
302
  * @param channelNumber - The channel number to monitor
217
303
  * @param callback - Function to call when audio starts playing
304
+ * @throws Error if the channel number exceeds the maximum allowed channels
218
305
  * @example
219
306
  * ```typescript
220
307
  * onAudioStart(0, (info) => {
@@ -224,6 +311,8 @@ export const offQueueChange = (channelNumber: number): void => {
224
311
  * ```
225
312
  */
226
313
  export const onAudioStart = (channelNumber: number, callback: AudioStartCallback): void => {
314
+ validateChannelNumber(channelNumber);
315
+
227
316
  if (!audioChannels[channelNumber]) {
228
317
  audioChannels[channelNumber] = {
229
318
  audioCompleteCallbacks: new Set(),
@@ -251,6 +340,7 @@ export const onAudioStart = (channelNumber: number, callback: AudioStartCallback
251
340
  * Subscribes to audio complete events for a specific channel
252
341
  * @param channelNumber - The channel number to monitor
253
342
  * @param callback - Function to call when audio completes
343
+ * @throws Error if the channel number exceeds the maximum allowed channels
254
344
  * @example
255
345
  * ```typescript
256
346
  * onAudioComplete(0, (info) => {
@@ -262,6 +352,8 @@ export const onAudioStart = (channelNumber: number, callback: AudioStartCallback
262
352
  * ```
263
353
  */
264
354
  export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCallback): void => {
355
+ validateChannelNumber(channelNumber);
356
+
265
357
  if (!audioChannels[channelNumber]) {
266
358
  audioChannels[channelNumber] = {
267
359
  audioCompleteCallbacks: new Set(),
@@ -289,6 +381,7 @@ export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCa
289
381
  * Subscribes to audio pause events for a specific channel
290
382
  * @param channelNumber - The channel number to monitor
291
383
  * @param callback - Function to call when audio is paused
384
+ * @throws Error if the channel number exceeds the maximum allowed channels
292
385
  * @example
293
386
  * ```typescript
294
387
  * onAudioPause(0, (channelNumber, info) => {
@@ -298,6 +391,8 @@ export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCa
298
391
  * ```
299
392
  */
300
393
  export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback): void => {
394
+ validateChannelNumber(channelNumber);
395
+
301
396
  if (!audioChannels[channelNumber]) {
302
397
  audioChannels[channelNumber] = {
303
398
  audioCompleteCallbacks: new Set(),
@@ -325,6 +420,7 @@ export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback
325
420
  * Subscribes to audio resume events for a specific channel
326
421
  * @param channelNumber - The channel number to monitor
327
422
  * @param callback - Function to call when audio is resumed
423
+ * @throws Error if the channel number exceeds the maximum allowed channels
328
424
  * @example
329
425
  * ```typescript
330
426
  * onAudioResume(0, (channelNumber, info) => {
@@ -334,6 +430,8 @@ export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback
334
430
  * ```
335
431
  */
336
432
  export const onAudioResume = (channelNumber: number, callback: AudioResumeCallback): void => {
433
+ validateChannelNumber(channelNumber);
434
+
337
435
  if (!audioChannels[channelNumber]) {
338
436
  audioChannels[channelNumber] = {
339
437
  audioCompleteCallbacks: new Set(),
package/src/pause.ts CHANGED
@@ -14,8 +14,6 @@ import { getAudioInfoFromElement } from './utils';
14
14
  import { emitAudioPause, emitAudioResume } from './events';
15
15
  import { transitionVolume, getFadeConfig } from './volume';
16
16
 
17
-
18
-
19
17
  /**
20
18
  * Gets the current volume for a channel, accounting for synchronous state
21
19
  * @param channelNumber - The channel number
@@ -81,7 +79,7 @@ export const pauseWithFade = async (
81
79
  // First fade or no transition in progress, capture current volume
82
80
  // But ensure we don't capture a volume of 0 during a transition
83
81
  const currentVolume = getChannelVolumeSync(channelNumber);
84
- originalVolume = currentVolume > 0 ? currentVolume : channel.fadeState?.originalVolume ?? 1.0;
82
+ originalVolume = currentVolume > 0 ? currentVolume : (channel.fadeState?.originalVolume ?? 1.0);
85
83
  }
86
84
 
87
85
  // Store fade state for resumeWithFade to use (including custom duration)
@@ -111,7 +109,7 @@ export const pauseWithFade = async (
111
109
 
112
110
  // Reset volume to original for resume (synchronously to avoid state issues)
113
111
  setChannelVolumeSync(channelNumber, originalVolume);
114
-
112
+
115
113
  // Mark transition as complete
116
114
  if (channel.fadeState) {
117
115
  channel.fadeState.isTransitioning = false;
@@ -258,11 +256,11 @@ export const pauseAllWithFade = async (
258
256
  */
259
257
  export const resumeAllWithFade = async (fadeType?: FadeType, duration?: number): Promise<void> => {
260
258
  const resumePromises: Promise<void>[] = [];
261
-
259
+
262
260
  audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
263
261
  resumePromises.push(resumeWithFade(fadeType, index, duration));
264
262
  });
265
-
263
+
266
264
  await Promise.all(resumePromises);
267
265
  };
268
266
 
@@ -0,0 +1,378 @@
1
+ /**
2
+ * @fileoverview Queue manipulation functions for the audio-channel-queue package
3
+ * Provides advanced queue management including item removal, reordering, and clearing
4
+ */
5
+
6
+ import {
7
+ ExtendedAudioQueueChannel,
8
+ QueueManipulationResult,
9
+ QueueSnapshot,
10
+ QueueItem
11
+ } from './types';
12
+ import { audioChannels } from './info';
13
+ import { emitQueueChange } from './events';
14
+ import { cleanupProgressTracking } from './events';
15
+ import { createQueueSnapshot, getAudioInfoFromElement } from './utils';
16
+
17
+ /**
18
+ * Removes a specific item from the queue by its slot number (0-based index)
19
+ * Cannot remove the currently playing item (index 0) - use stopCurrentAudioInChannel instead
20
+ * @param queuedSlotNumber - Zero-based index of the item to remove (must be > 0)
21
+ * @param channelNumber - The channel number (defaults to 0)
22
+ * @returns Promise resolving to operation result with success status and updated queue
23
+ * @throws Error if trying to remove currently playing item or invalid slot number
24
+ * @example
25
+ * ```typescript
26
+ * // Remove the second item in queue (index 1)
27
+ * const result = await removeQueuedItem(1, 0);
28
+ * if (result.success) {
29
+ * console.log(`Removed item, queue now has ${result.updatedQueue.totalItems} items`);
30
+ * }
31
+ *
32
+ * // Remove the third item from channel 1
33
+ * await removeQueuedItem(2, 1);
34
+ * ```
35
+ */
36
+ export const removeQueuedItem = async (
37
+ queuedSlotNumber: number,
38
+ channelNumber: number = 0
39
+ ): Promise<QueueManipulationResult> => {
40
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
41
+
42
+ if (!channel) {
43
+ return {
44
+ error: `Channel ${channelNumber} does not exist`,
45
+ success: false
46
+ };
47
+ }
48
+
49
+ if (queuedSlotNumber < 0 || queuedSlotNumber >= channel.queue.length) {
50
+ return {
51
+ error:
52
+ `Invalid slot number ${queuedSlotNumber}. Queue has ${channel.queue.length} items ` +
53
+ `(indices 0-${channel.queue.length - 1})`,
54
+ success: false
55
+ };
56
+ }
57
+
58
+ if (queuedSlotNumber === 0) {
59
+ return {
60
+ error:
61
+ 'Cannot remove currently playing item (index 0). ' +
62
+ 'Use stopCurrentAudioInChannel() instead',
63
+ success: false
64
+ };
65
+ }
66
+
67
+ // Remove the audio element from the queue
68
+ const removedAudio: HTMLAudioElement = channel.queue.splice(queuedSlotNumber, 1)[0];
69
+
70
+ // Clean up any progress tracking for the removed audio
71
+ cleanupProgressTracking(removedAudio, channelNumber, audioChannels);
72
+
73
+ // Emit queue change event
74
+ emitQueueChange(channelNumber, audioChannels);
75
+
76
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
77
+
78
+ return {
79
+ success: true,
80
+ updatedQueue: updatedQueue ?? undefined
81
+ };
82
+ };
83
+
84
+ /**
85
+ * Reorders a queue item by moving it from one position to another
86
+ * Cannot reorder the currently playing item (index 0)
87
+ * @param currentQueuedSlotNumber - Current zero-based index of the item to move (must be > 0)
88
+ * @param newQueuedSlotNumber - New zero-based index where the item should be placed (must be > 0)
89
+ * @param channelNumber - The channel number (defaults to 0)
90
+ * @returns Promise resolving to operation result with success status and updated queue
91
+ * @throws Error if trying to reorder currently playing item or invalid slot numbers
92
+ * @example
93
+ * ```typescript
94
+ * // Move item from position 2 to position 1 (make it play next)
95
+ * const result = await reorderQueue(2, 1, 0);
96
+ * if (result.success) {
97
+ * console.log('Item moved successfully');
98
+ * }
99
+ *
100
+ * // Move item from position 1 to end of queue
101
+ * await reorderQueue(1, 4, 0); // Assuming queue has 5+ items
102
+ * ```
103
+ */
104
+ export const reorderQueue = async (
105
+ currentQueuedSlotNumber: number,
106
+ newQueuedSlotNumber: number,
107
+ channelNumber: number = 0
108
+ ): Promise<QueueManipulationResult> => {
109
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
110
+
111
+ if (!channel) {
112
+ return {
113
+ error: `Channel ${channelNumber} does not exist`,
114
+ success: false
115
+ };
116
+ }
117
+
118
+ if (currentQueuedSlotNumber < 0 || currentQueuedSlotNumber >= channel.queue.length) {
119
+ return {
120
+ error:
121
+ `Invalid current slot number ${currentQueuedSlotNumber}. Queue has ` +
122
+ `${channel.queue.length} items (indices 0-${channel.queue.length - 1})`,
123
+ success: false
124
+ };
125
+ }
126
+
127
+ if (newQueuedSlotNumber < 0 || newQueuedSlotNumber >= channel.queue.length) {
128
+ return {
129
+ error:
130
+ `Invalid new slot number ${newQueuedSlotNumber}. Queue has ` +
131
+ `${channel.queue.length} items (indices 0-${channel.queue.length - 1})`,
132
+ success: false
133
+ };
134
+ }
135
+
136
+ if (currentQueuedSlotNumber === 0) {
137
+ return {
138
+ error:
139
+ 'Cannot reorder currently playing item (index 0). ' + 'Stop current audio first if needed',
140
+ success: false
141
+ };
142
+ }
143
+
144
+ if (newQueuedSlotNumber === 0) {
145
+ return {
146
+ error:
147
+ 'Cannot move item to currently playing position (index 0). ' +
148
+ 'Use queueAudioPriority() to add items to front of queue',
149
+ success: false
150
+ };
151
+ }
152
+
153
+ if (currentQueuedSlotNumber === newQueuedSlotNumber) {
154
+ // No change needed, but return success
155
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
156
+ return {
157
+ success: true,
158
+ updatedQueue: updatedQueue ?? undefined
159
+ };
160
+ }
161
+
162
+ // Remove the item from its current position
163
+ const audioToMove: HTMLAudioElement = channel.queue.splice(currentQueuedSlotNumber, 1)[0];
164
+
165
+ // Insert it at the new position
166
+ channel.queue.splice(newQueuedSlotNumber, 0, audioToMove);
167
+
168
+ // Emit queue change event
169
+ emitQueueChange(channelNumber, audioChannels);
170
+
171
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
172
+
173
+ return {
174
+ success: true,
175
+ updatedQueue: updatedQueue ?? undefined
176
+ };
177
+ };
178
+
179
+ /**
180
+ * Clears all queued audio items after the currently playing item
181
+ * The current audio will continue playing but nothing will follow it
182
+ * @param channelNumber - The channel number (defaults to 0)
183
+ * @returns Promise resolving to operation result with success status and updated queue
184
+ * @example
185
+ * ```typescript
186
+ * // Let current song finish but clear everything after it
187
+ * const result = await clearQueueAfterCurrent(0);
188
+ * if (result.success) {
189
+ * console.log(`Cleared queue, current audio will be the last to play`);
190
+ * }
191
+ * ```
192
+ */
193
+ export const clearQueueAfterCurrent = async (
194
+ channelNumber: number = 0
195
+ ): Promise<QueueManipulationResult> => {
196
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
197
+
198
+ if (!channel) {
199
+ // For empty/non-existent channels, we can consider this a successful no-op
200
+ // since there's nothing to clear anyway
201
+ return {
202
+ success: true,
203
+ updatedQueue: {
204
+ channelNumber,
205
+ currentIndex: -1,
206
+ isPaused: false,
207
+ items: [],
208
+ totalItems: 0,
209
+ volume: 1.0
210
+ }
211
+ };
212
+ }
213
+
214
+ if (channel.queue.length <= 1) {
215
+ // Nothing to clear - either empty queue or only current audio
216
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
217
+ return {
218
+ success: true,
219
+ updatedQueue: updatedQueue ?? undefined
220
+ };
221
+ }
222
+
223
+ // Clean up progress tracking for all items except the current one
224
+ for (let i: number = 1; i < channel.queue.length; i++) {
225
+ cleanupProgressTracking(channel.queue[i], channelNumber, audioChannels);
226
+ }
227
+
228
+ // Keep only the currently playing audio (index 0)
229
+ channel.queue = channel.queue.slice(0, 1);
230
+
231
+ // Emit queue change event
232
+ emitQueueChange(channelNumber, audioChannels);
233
+
234
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
235
+
236
+ return {
237
+ success: true,
238
+ updatedQueue: updatedQueue ?? undefined
239
+ };
240
+ };
241
+
242
+ /**
243
+ * Gets information about a specific queue item by its slot number
244
+ * @param queueSlotNumber - Zero-based index of the queue item
245
+ * @param channelNumber - The channel number (defaults to 0)
246
+ * @returns QueueItem information or null if slot doesn't exist
247
+ * @example
248
+ * ```typescript
249
+ * const itemInfo = getQueueItemInfo(1, 0);
250
+ * if (itemInfo) {
251
+ * console.log(`Next to play: ${itemInfo.fileName}`);
252
+ * console.log(`Duration: ${itemInfo.duration}ms`);
253
+ * }
254
+ * ```
255
+ */
256
+ export const getQueueItemInfo = (
257
+ queueSlotNumber: number,
258
+ channelNumber: number = 0
259
+ ): QueueItem | null => {
260
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
261
+
262
+ if (!channel || queueSlotNumber < 0 || queueSlotNumber >= channel.queue.length) {
263
+ return null;
264
+ }
265
+
266
+ const audio: HTMLAudioElement = channel.queue[queueSlotNumber];
267
+ const audioInfo = getAudioInfoFromElement(audio, channelNumber, audioChannels);
268
+
269
+ if (!audioInfo) {
270
+ return null;
271
+ }
272
+
273
+ const { duration, fileName, isLooping, isPlaying, src, volume } = audioInfo;
274
+
275
+ return {
276
+ duration,
277
+ fileName,
278
+ isCurrentlyPlaying: queueSlotNumber === 0 && isPlaying,
279
+ isLooping,
280
+ src,
281
+ volume
282
+ };
283
+ };
284
+
285
+ /**
286
+ * Gets the current queue length for a specific channel
287
+ * @param channelNumber - The channel number (defaults to 0)
288
+ * @returns Number of items in the queue, or 0 if channel doesn't exist
289
+ * @example
290
+ * ```typescript
291
+ * const queueSize = getQueueLength(0);
292
+ * console.log(`Channel 0 has ${queueSize} items in queue`);
293
+ * ```
294
+ */
295
+ export const getQueueLength = (channelNumber: number = 0): number => {
296
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
297
+ return channel ? channel.queue.length : 0;
298
+ };
299
+
300
+ /**
301
+ * Swaps the positions of two queue items
302
+ * Cannot swap with the currently playing item (index 0)
303
+ * @param slotA - Zero-based index of first item to swap (must be > 0)
304
+ * @param slotB - Zero-based index of second item to swap (must be > 0)
305
+ * @param channelNumber - The channel number (defaults to 0)
306
+ * @returns Promise resolving to operation result with success status and updated queue
307
+ * @example
308
+ * ```typescript
309
+ * // Swap the second and third items in queue
310
+ * const result = await swapQueueItems(1, 2, 0);
311
+ * if (result.success) {
312
+ * console.log('Items swapped successfully');
313
+ * }
314
+ * ```
315
+ */
316
+ export const swapQueueItems = async (
317
+ slotA: number,
318
+ slotB: number,
319
+ channelNumber: number = 0
320
+ ): Promise<QueueManipulationResult> => {
321
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
322
+
323
+ if (!channel) {
324
+ return {
325
+ error: `Channel ${channelNumber} does not exist`,
326
+ success: false
327
+ };
328
+ }
329
+
330
+ if (slotA < 0 || slotA >= channel.queue.length) {
331
+ return {
332
+ error:
333
+ `Invalid slot A ${slotA}. Queue has ${channel.queue.length} items ` +
334
+ `(indices 0-${channel.queue.length - 1})`,
335
+ success: false
336
+ };
337
+ }
338
+
339
+ if (slotB < 0 || slotB >= channel.queue.length) {
340
+ return {
341
+ error:
342
+ `Invalid slot B ${slotB}. Queue has ${channel.queue.length} items ` +
343
+ `(indices 0-${channel.queue.length - 1})`,
344
+ success: false
345
+ };
346
+ }
347
+
348
+ if (slotA === 0 || slotB === 0) {
349
+ return {
350
+ error: 'Cannot swap with currently playing item (index 0)',
351
+ success: false
352
+ };
353
+ }
354
+
355
+ if (slotA === slotB) {
356
+ // No change needed, but return success
357
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
358
+ return {
359
+ success: true,
360
+ updatedQueue: updatedQueue ?? undefined
361
+ };
362
+ }
363
+
364
+ // Swap the audio elements
365
+ const temp: HTMLAudioElement = channel.queue[slotA];
366
+ channel.queue[slotA] = channel.queue[slotB];
367
+ channel.queue[slotB] = temp;
368
+
369
+ // Emit queue change event
370
+ emitQueueChange(channelNumber, audioChannels);
371
+
372
+ const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
373
+
374
+ return {
375
+ success: true,
376
+ updatedQueue: updatedQueue ?? undefined
377
+ };
378
+ };