audio-channel-queue 1.4.0 → 1.6.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/volume.js ADDED
@@ -0,0 +1,302 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Volume management functions for the audio-channel-queue 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.restoreVolumeLevels = exports.applyVolumeDucking = exports.clearVolumeDucking = exports.setVolumeDucking = exports.setAllChannelsVolume = exports.getAllChannelsVolume = exports.getChannelVolume = exports.setChannelVolume = exports.transitionVolume = void 0;
16
+ const info_1 = require("./info");
17
+ // Store active volume transitions to handle interruptions
18
+ const activeTransitions = new Map();
19
+ /**
20
+ * Easing functions for smooth volume transitions
21
+ */
22
+ const easingFunctions = {
23
+ linear: (t) => t,
24
+ 'ease-in': (t) => t * t,
25
+ 'ease-out': (t) => t * (2 - t),
26
+ 'ease-in-out': (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
27
+ };
28
+ /**
29
+ * Smoothly transitions volume for a specific channel over time
30
+ * @param channelNumber - The channel number to transition
31
+ * @param targetVolume - Target volume level (0-1)
32
+ * @param duration - Transition duration in milliseconds
33
+ * @param easing - Easing function type
34
+ * @returns Promise that resolves when transition completes
35
+ * @example
36
+ * ```typescript
37
+ * await transitionVolume(0, 0.2, 500, 'ease-out'); // Duck to 20% over 500ms
38
+ * ```
39
+ */
40
+ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __awaiter(void 0, [channelNumber_1, targetVolume_1, ...args_1], void 0, function* (channelNumber, targetVolume, duration = 250, easing = 'ease-out') {
41
+ const channel = info_1.audioChannels[channelNumber];
42
+ if (!channel || channel.queue.length === 0)
43
+ return;
44
+ const currentAudio = channel.queue[0];
45
+ const startVolume = currentAudio.volume;
46
+ const volumeDelta = targetVolume - startVolume;
47
+ // Cancel any existing transition for this channel
48
+ if (activeTransitions.has(channelNumber)) {
49
+ clearTimeout(activeTransitions.get(channelNumber));
50
+ activeTransitions.delete(channelNumber);
51
+ }
52
+ // If no change needed, resolve immediately
53
+ if (Math.abs(volumeDelta) < 0.001) {
54
+ channel.volume = targetVolume;
55
+ return Promise.resolve();
56
+ }
57
+ // Handle zero duration - instant change
58
+ if (duration <= 0) {
59
+ channel.volume = targetVolume;
60
+ if (channel.queue.length > 0) {
61
+ channel.queue[0].volume = targetVolume;
62
+ }
63
+ return Promise.resolve();
64
+ }
65
+ const startTime = performance.now();
66
+ const easingFn = easingFunctions[easing];
67
+ return new Promise((resolve) => {
68
+ const updateVolume = () => {
69
+ const elapsed = performance.now() - startTime;
70
+ const progress = Math.min(elapsed / duration, 1);
71
+ const easedProgress = easingFn(progress);
72
+ const currentVolume = startVolume + (volumeDelta * easedProgress);
73
+ const clampedVolume = Math.max(0, Math.min(1, currentVolume));
74
+ // Apply volume to both channel config and current audio
75
+ channel.volume = clampedVolume;
76
+ if (channel.queue.length > 0) {
77
+ channel.queue[0].volume = clampedVolume;
78
+ }
79
+ if (progress >= 1) {
80
+ // Transition complete
81
+ activeTransitions.delete(channelNumber);
82
+ resolve();
83
+ }
84
+ else {
85
+ // Use requestAnimationFrame in browser, setTimeout in tests
86
+ if (typeof requestAnimationFrame !== 'undefined') {
87
+ const rafId = requestAnimationFrame(updateVolume);
88
+ activeTransitions.set(channelNumber, rafId);
89
+ }
90
+ else {
91
+ // In test environment, use shorter intervals
92
+ const timeoutId = setTimeout(updateVolume, 1);
93
+ activeTransitions.set(channelNumber, timeoutId);
94
+ }
95
+ }
96
+ };
97
+ updateVolume();
98
+ });
99
+ });
100
+ exports.transitionVolume = transitionVolume;
101
+ /**
102
+ * Sets the volume for a specific channel with optional smooth transition
103
+ * @param channelNumber - The channel number to set volume for
104
+ * @param volume - Volume level (0-1)
105
+ * @param transitionDuration - Optional transition duration in milliseconds
106
+ * @param easing - Optional easing function
107
+ * @example
108
+ * ```typescript
109
+ * setChannelVolume(0, 0.5); // Set channel 0 to 50%
110
+ * setChannelVolume(0, 0.5, 300, 'ease-out'); // Smooth transition over 300ms
111
+ * ```
112
+ */
113
+ const setChannelVolume = (channelNumber, volume, transitionDuration, easing) => __awaiter(void 0, void 0, void 0, function* () {
114
+ const clampedVolume = Math.max(0, Math.min(1, volume));
115
+ if (!info_1.audioChannels[channelNumber]) {
116
+ info_1.audioChannels[channelNumber] = {
117
+ audioCompleteCallbacks: new Set(),
118
+ audioPauseCallbacks: new Set(),
119
+ audioResumeCallbacks: new Set(),
120
+ audioStartCallbacks: new Set(),
121
+ isPaused: false,
122
+ progressCallbacks: new Map(),
123
+ queue: [],
124
+ queueChangeCallbacks: new Set(),
125
+ volume: clampedVolume
126
+ };
127
+ return;
128
+ }
129
+ if (transitionDuration && transitionDuration > 0) {
130
+ // Smooth transition
131
+ yield (0, exports.transitionVolume)(channelNumber, clampedVolume, transitionDuration, easing);
132
+ }
133
+ else {
134
+ // Instant change (backward compatibility)
135
+ info_1.audioChannels[channelNumber].volume = clampedVolume;
136
+ const channel = info_1.audioChannels[channelNumber];
137
+ if (channel.queue.length > 0) {
138
+ const currentAudio = channel.queue[0];
139
+ currentAudio.volume = clampedVolume;
140
+ }
141
+ }
142
+ });
143
+ exports.setChannelVolume = setChannelVolume;
144
+ /**
145
+ * Gets the current volume for a specific channel
146
+ * @param channelNumber - The channel number to get volume for (defaults to 0)
147
+ * @returns Current volume level (0-1) or 1.0 if channel doesn't exist
148
+ * @example
149
+ * ```typescript
150
+ * const volume = getChannelVolume(0);
151
+ * const defaultChannelVolume = getChannelVolume(); // Gets channel 0
152
+ * console.log(`Channel 0 volume: ${volume * 100}%`);
153
+ * ```
154
+ */
155
+ const getChannelVolume = (channelNumber = 0) => {
156
+ const channel = info_1.audioChannels[channelNumber];
157
+ return (channel === null || channel === void 0 ? void 0 : channel.volume) || 1.0;
158
+ };
159
+ exports.getChannelVolume = getChannelVolume;
160
+ /**
161
+ * Gets the volume levels for all channels
162
+ * @returns Array of volume levels (0-1) for each channel
163
+ * @example
164
+ * ```typescript
165
+ * const volumes = getAllChannelsVolume();
166
+ * volumes.forEach((volume, index) => {
167
+ * console.log(`Channel ${index}: ${volume * 100}%`);
168
+ * });
169
+ * ```
170
+ */
171
+ const getAllChannelsVolume = () => {
172
+ return info_1.audioChannels.map((channel) => (channel === null || channel === void 0 ? void 0 : channel.volume) || 1.0);
173
+ };
174
+ exports.getAllChannelsVolume = getAllChannelsVolume;
175
+ /**
176
+ * Sets volume for all channels to the same level
177
+ * @param volume - Volume level (0-1) to apply to all channels
178
+ * @example
179
+ * ```typescript
180
+ * await setAllChannelsVolume(0.6); // Set all channels to 60% volume
181
+ * ```
182
+ */
183
+ const setAllChannelsVolume = (volume) => __awaiter(void 0, void 0, void 0, function* () {
184
+ const promises = [];
185
+ info_1.audioChannels.forEach((_channel, index) => {
186
+ promises.push((0, exports.setChannelVolume)(index, volume));
187
+ });
188
+ yield Promise.all(promises);
189
+ });
190
+ exports.setAllChannelsVolume = setAllChannelsVolume;
191
+ /**
192
+ * Configures volume ducking for channels. When the priority channel plays audio,
193
+ * all other channels will be automatically reduced to the ducking volume level
194
+ * @param config - Volume ducking configuration
195
+ * @example
196
+ * ```typescript
197
+ * // When channel 1 plays, reduce all other channels to 20% volume
198
+ * setVolumeDucking({
199
+ * priorityChannel: 1,
200
+ * priorityVolume: 1.0,
201
+ * duckingVolume: 0.2
202
+ * });
203
+ * ```
204
+ */
205
+ const setVolumeDucking = (config) => {
206
+ // First, ensure we have enough channels for the priority channel
207
+ while (info_1.audioChannels.length <= config.priorityChannel) {
208
+ info_1.audioChannels.push({
209
+ audioCompleteCallbacks: new Set(),
210
+ audioPauseCallbacks: new Set(),
211
+ audioResumeCallbacks: new Set(),
212
+ audioStartCallbacks: new Set(),
213
+ isPaused: false,
214
+ progressCallbacks: new Map(),
215
+ queue: [],
216
+ queueChangeCallbacks: new Set(),
217
+ volume: 1.0
218
+ });
219
+ }
220
+ // Apply the config to all existing channels
221
+ info_1.audioChannels.forEach((channel, index) => {
222
+ if (!info_1.audioChannels[index]) {
223
+ info_1.audioChannels[index] = {
224
+ audioCompleteCallbacks: new Set(),
225
+ audioPauseCallbacks: new Set(),
226
+ audioResumeCallbacks: new Set(),
227
+ audioStartCallbacks: new Set(),
228
+ isPaused: false,
229
+ progressCallbacks: new Map(),
230
+ queue: [],
231
+ queueChangeCallbacks: new Set(),
232
+ volume: 1.0
233
+ };
234
+ }
235
+ info_1.audioChannels[index].volumeConfig = config;
236
+ });
237
+ };
238
+ exports.setVolumeDucking = setVolumeDucking;
239
+ /**
240
+ * Removes volume ducking configuration from all channels
241
+ * @example
242
+ * ```typescript
243
+ * clearVolumeDucking(); // Remove all volume ducking effects
244
+ * ```
245
+ */
246
+ const clearVolumeDucking = () => {
247
+ info_1.audioChannels.forEach((channel) => {
248
+ if (channel) {
249
+ delete channel.volumeConfig;
250
+ }
251
+ });
252
+ };
253
+ exports.clearVolumeDucking = clearVolumeDucking;
254
+ /**
255
+ * Applies volume ducking effects based on current playback state with smooth transitions
256
+ * @param activeChannelNumber - The channel that just started playing
257
+ * @internal
258
+ */
259
+ const applyVolumeDucking = (activeChannelNumber) => __awaiter(void 0, void 0, void 0, function* () {
260
+ const transitionPromises = [];
261
+ info_1.audioChannels.forEach((channel, channelNumber) => {
262
+ if (channel === null || channel === void 0 ? void 0 : channel.volumeConfig) {
263
+ const config = channel.volumeConfig;
264
+ if (activeChannelNumber === config.priorityChannel) {
265
+ const duration = config.duckTransitionDuration || 250;
266
+ const easing = config.transitionEasing || 'ease-out';
267
+ // Priority channel is active, duck other channels
268
+ if (channelNumber === config.priorityChannel) {
269
+ transitionPromises.push((0, exports.transitionVolume)(channelNumber, config.priorityVolume, duration, easing));
270
+ }
271
+ else {
272
+ transitionPromises.push((0, exports.transitionVolume)(channelNumber, config.duckingVolume, duration, easing));
273
+ }
274
+ }
275
+ }
276
+ });
277
+ // Wait for all transitions to complete
278
+ yield Promise.all(transitionPromises);
279
+ });
280
+ exports.applyVolumeDucking = applyVolumeDucking;
281
+ /**
282
+ * Restores normal volume levels when priority channel stops with smooth transitions
283
+ * @param stoppedChannelNumber - The channel that just stopped playing
284
+ * @internal
285
+ */
286
+ const restoreVolumeLevels = (stoppedChannelNumber) => __awaiter(void 0, void 0, void 0, function* () {
287
+ const transitionPromises = [];
288
+ info_1.audioChannels.forEach((channel, channelNumber) => {
289
+ if (channel === null || channel === void 0 ? void 0 : channel.volumeConfig) {
290
+ const config = channel.volumeConfig;
291
+ if (stoppedChannelNumber === config.priorityChannel) {
292
+ const duration = config.restoreTransitionDuration || 500;
293
+ const easing = config.transitionEasing || 'ease-out';
294
+ // Priority channel stopped, restore normal volumes
295
+ transitionPromises.push((0, exports.transitionVolume)(channelNumber, channel.volume || 1.0, duration, easing));
296
+ }
297
+ }
298
+ });
299
+ // Wait for all transitions to complete
300
+ yield Promise.all(transitionPromises);
301
+ });
302
+ exports.restoreVolumeLevels = restoreVolumeLevels;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "audio-channel-queue",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Allows you to queue audio files to different playback channels.",
5
- "main": "dist/audio.js",
6
- "types": "dist/audio.d.ts",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "dist",
9
9
  "src"
@@ -11,7 +11,9 @@
11
11
  "scripts": {
12
12
  "build": "tsc",
13
13
  "prepare": "npm run build",
14
- "test": "jest"
14
+ "test": "jest",
15
+ "test:watch": "jest --watch",
16
+ "test:coverage": "jest --coverage"
15
17
  },
16
18
  "repository": {
17
19
  "type": "git",
@@ -29,9 +31,13 @@
29
31
  "url": "https://github.com/tonycarpenter21/audio-queue-package/issues"
30
32
  },
31
33
  "homepage": "https://github.com/tonycarpenter21/audio-queue-package#readme",
34
+ "engines": {
35
+ "node": ">=14.0.0"
36
+ },
32
37
  "devDependencies": {
33
38
  "@types/jest": "^29.5.13",
34
39
  "jest": "^29.7.0",
40
+ "jest-environment-jsdom": "^29.7.0",
35
41
  "ts-jest": "^29.2.5",
36
42
  "typescript": "^5.6.2"
37
43
  }
package/src/core.ts ADDED
@@ -0,0 +1,306 @@
1
+ /**
2
+ * @fileoverview Core queue management functions for the audio-channel-queue package
3
+ */
4
+
5
+ import { ExtendedAudioQueueChannel, AudioQueueOptions } from './types';
6
+ import { audioChannels } from './info';
7
+ import { extractFileName } from './utils';
8
+ import {
9
+ emitQueueChange,
10
+ emitAudioStart,
11
+ emitAudioComplete,
12
+ setupProgressTracking,
13
+ cleanupProgressTracking
14
+ } from './events';
15
+ import { applyVolumeDucking, restoreVolumeLevels } from './volume';
16
+
17
+ /**
18
+ * Queues an audio file to a specific channel and starts playing if it's the first in queue
19
+ * @param audioUrl - The URL of the audio file to queue
20
+ * @param channelNumber - The channel number to queue the audio to (defaults to 0)
21
+ * @param options - Optional configuration for the audio file
22
+ * @returns Promise that resolves when the audio is queued and starts playing (if first in queue)
23
+ * @example
24
+ * ```typescript
25
+ * await queueAudio('https://example.com/song.mp3', 0);
26
+ * await queueAudio('./sounds/notification.wav'); // Uses default channel 0
27
+ * await queueAudio('./music/loop.mp3', 1, { loop: true }); // Loop the audio
28
+ * await queueAudio('./urgent.wav', 0, { addToFront: true }); // Add to front of queue
29
+ * ```
30
+ */
31
+ export const queueAudio = async (
32
+ audioUrl: string,
33
+ channelNumber: number = 0,
34
+ options?: AudioQueueOptions
35
+ ): Promise<void> => {
36
+ if (!audioChannels[channelNumber]) {
37
+ audioChannels[channelNumber] = {
38
+ audioCompleteCallbacks: new Set(),
39
+ audioPauseCallbacks: new Set(),
40
+ audioResumeCallbacks: new Set(),
41
+ audioStartCallbacks: new Set(),
42
+ isPaused: false,
43
+ progressCallbacks: new Map(),
44
+ queue: [],
45
+ queueChangeCallbacks: new Set(),
46
+ volume: 1.0
47
+ };
48
+ }
49
+
50
+ const audio: HTMLAudioElement = new Audio(audioUrl);
51
+
52
+ // Apply audio configuration from options
53
+ if (options?.loop) {
54
+ audio.loop = true;
55
+ }
56
+
57
+ if (options?.volume !== undefined) {
58
+ const clampedVolume: number = Math.max(0, Math.min(1, options.volume));
59
+ // Handle NaN case - default to channel volume or 1.0
60
+ const safeVolume: number = isNaN(clampedVolume) ? (audioChannels[channelNumber].volume || 1.0) : clampedVolume;
61
+ audio.volume = safeVolume;
62
+ // Also update the channel volume
63
+ audioChannels[channelNumber].volume = safeVolume;
64
+ } else {
65
+ // Use channel volume if no specific volume is set
66
+ const channelVolume: number = audioChannels[channelNumber].volume || 1.0;
67
+ audio.volume = channelVolume;
68
+ }
69
+
70
+ // Add to front or back of queue based on options
71
+ if ((options?.addToFront || options?.priority) && audioChannels[channelNumber].queue.length > 0) {
72
+ // Insert after the currently playing audio (index 1)
73
+ audioChannels[channelNumber].queue.splice(1, 0, audio);
74
+ } else if ((options?.addToFront || options?.priority) && audioChannels[channelNumber].queue.length === 0) {
75
+ // If queue is empty, just add normally
76
+ audioChannels[channelNumber].queue.push(audio);
77
+ } else {
78
+ // Default behavior - add to back of queue
79
+ audioChannels[channelNumber].queue.push(audio);
80
+ }
81
+
82
+ emitQueueChange(channelNumber, audioChannels);
83
+
84
+ if (audioChannels[channelNumber].queue.length === 1) {
85
+ // Don't await - let playback happen asynchronously
86
+ playAudioQueue(channelNumber).catch(console.error);
87
+ }
88
+ };
89
+
90
+ /**
91
+ * Adds an audio file to the front of the queue in a specific channel
92
+ * This is a convenience function that places the audio right after the currently playing track
93
+ * @param audioUrl - The URL of the audio file to queue
94
+ * @param channelNumber - The channel number to queue the audio to (defaults to 0)
95
+ * @param options - Optional configuration for the audio file
96
+ * @returns Promise that resolves when the audio is queued
97
+ * @example
98
+ * ```typescript
99
+ * await queueAudioPriority('./urgent-announcement.wav', 0);
100
+ * await queueAudioPriority('./priority-sound.mp3', 1, { loop: true });
101
+ * ```
102
+ */
103
+ export const queueAudioPriority = async (
104
+ audioUrl: string,
105
+ channelNumber: number = 0,
106
+ options?: AudioQueueOptions
107
+ ): Promise<void> => {
108
+ const priorityOptions: AudioQueueOptions = { ...options, addToFront: true };
109
+ return queueAudio(audioUrl, channelNumber, priorityOptions);
110
+ };
111
+
112
+ /**
113
+ * Plays the audio queue for a specific channel
114
+ * @param channelNumber - The channel number to play
115
+ * @returns Promise that resolves when the current audio finishes playing
116
+ * @example
117
+ * ```typescript
118
+ * await playAudioQueue(0); // Play queue for channel 0
119
+ * ```
120
+ */
121
+ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
122
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
123
+
124
+ if (!channel || channel.queue.length === 0) return;
125
+
126
+ const currentAudio: HTMLAudioElement = channel.queue[0];
127
+
128
+ // Apply channel volume if not already set
129
+ if (currentAudio.volume === 1.0 && channel.volume !== undefined) {
130
+ currentAudio.volume = channel.volume;
131
+ }
132
+
133
+ setupProgressTracking(currentAudio, channelNumber, audioChannels);
134
+
135
+ // Apply volume ducking when audio starts
136
+ await applyVolumeDucking(channelNumber);
137
+
138
+ return new Promise<void>((resolve) => {
139
+ let hasStarted: boolean = false;
140
+ let metadataLoaded: boolean = false;
141
+ let playStarted: boolean = false;
142
+
143
+ // Check if we should fire onAudioStart (both conditions met)
144
+ const tryFireAudioStart = (): void => {
145
+ if (!hasStarted && metadataLoaded && playStarted) {
146
+ hasStarted = true;
147
+ emitAudioStart(channelNumber, {
148
+ channelNumber,
149
+ duration: currentAudio.duration * 1000, // Now guaranteed to have valid duration
150
+ fileName: extractFileName(currentAudio.src),
151
+ src: currentAudio.src
152
+ }, audioChannels);
153
+ }
154
+ };
155
+
156
+ // Event handler for when metadata loads (duration becomes available)
157
+ const handleLoadedMetadata = (): void => {
158
+ metadataLoaded = true;
159
+ tryFireAudioStart();
160
+ };
161
+
162
+ // Event handler for when audio actually starts playing
163
+ const handlePlay = (): void => {
164
+ playStarted = true;
165
+ tryFireAudioStart();
166
+ };
167
+
168
+ // Event handler for when audio ends
169
+ const handleEnded = async (): Promise<void> => {
170
+ emitAudioComplete(channelNumber, {
171
+ channelNumber,
172
+ fileName: extractFileName(currentAudio.src),
173
+ remainingInQueue: channel.queue.length - 1,
174
+ src: currentAudio.src
175
+ }, audioChannels);
176
+
177
+ // Restore volume levels when priority channel stops
178
+ await restoreVolumeLevels(channelNumber);
179
+
180
+ // Clean up event listeners
181
+ currentAudio.removeEventListener('loadedmetadata', handleLoadedMetadata);
182
+ currentAudio.removeEventListener('play', handlePlay);
183
+ currentAudio.removeEventListener('ended', handleEnded);
184
+
185
+ cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
186
+
187
+ // Handle looping vs non-looping audio
188
+ if (currentAudio.loop) {
189
+ // For looping audio, reset current time and continue playing
190
+ currentAudio.currentTime = 0;
191
+ await currentAudio.play();
192
+ // Don't remove from queue, but resolve the promise so tests don't hang
193
+ resolve();
194
+ } else {
195
+ // For non-looping audio, remove from queue and play next
196
+ channel.queue.shift();
197
+
198
+ // Emit queue change after completion
199
+ setTimeout(() => emitQueueChange(channelNumber, audioChannels), 10);
200
+
201
+ await playAudioQueue(channelNumber);
202
+ resolve();
203
+ }
204
+ };
205
+
206
+ // Add event listeners
207
+ currentAudio.addEventListener('loadedmetadata', handleLoadedMetadata);
208
+ currentAudio.addEventListener('play', handlePlay);
209
+ currentAudio.addEventListener('ended', handleEnded);
210
+
211
+ // Check if metadata is already loaded (in case it loads before we add the listener)
212
+ if (currentAudio.readyState >= 1) { // HAVE_METADATA or higher
213
+ metadataLoaded = true;
214
+ }
215
+
216
+ currentAudio.play();
217
+ });
218
+ };
219
+
220
+ /**
221
+ * Stops the currently playing audio in a specific channel and plays the next audio in queue
222
+ * @param channelNumber - The channel number (defaults to 0)
223
+ * @example
224
+ * ```typescript
225
+ * await stopCurrentAudioInChannel(); // Stop current audio in default channel (0)
226
+ * await stopCurrentAudioInChannel(1); // Stop current audio in channel 1
227
+ * ```
228
+ */
229
+ export const stopCurrentAudioInChannel = async (channelNumber: number = 0): Promise<void> => {
230
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
231
+ if (channel && channel.queue.length > 0) {
232
+ const currentAudio: HTMLAudioElement = channel.queue[0];
233
+
234
+ emitAudioComplete(channelNumber, {
235
+ channelNumber,
236
+ fileName: extractFileName(currentAudio.src),
237
+ remainingInQueue: channel.queue.length - 1,
238
+ src: currentAudio.src
239
+ }, audioChannels);
240
+
241
+ // Restore volume levels when stopping
242
+ await restoreVolumeLevels(channelNumber);
243
+
244
+ currentAudio.pause();
245
+ cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
246
+ channel.queue.shift();
247
+ channel.isPaused = false; // Reset pause state
248
+
249
+ emitQueueChange(channelNumber, audioChannels);
250
+
251
+ // Start next audio without waiting for it to complete
252
+ playAudioQueue(channelNumber).catch(console.error);
253
+ }
254
+ };
255
+
256
+ /**
257
+ * Stops all audio in a specific channel and clears the entire queue
258
+ * @param channelNumber - The channel number (defaults to 0)
259
+ * @example
260
+ * ```typescript
261
+ * await stopAllAudioInChannel(); // Clear all audio in default channel (0)
262
+ * await stopAllAudioInChannel(1); // Clear all audio in channel 1
263
+ * ```
264
+ */
265
+ export const stopAllAudioInChannel = async (channelNumber: number = 0): Promise<void> => {
266
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
267
+ if (channel) {
268
+ if (channel.queue.length > 0) {
269
+ const currentAudio: HTMLAudioElement = channel.queue[0];
270
+
271
+ emitAudioComplete(channelNumber, {
272
+ channelNumber,
273
+ fileName: extractFileName(currentAudio.src),
274
+ remainingInQueue: 0, // Will be 0 since we're clearing the queue
275
+ src: currentAudio.src
276
+ }, audioChannels);
277
+
278
+ // Restore volume levels when stopping
279
+ await restoreVolumeLevels(channelNumber);
280
+
281
+ currentAudio.pause();
282
+ cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
283
+ }
284
+ // Clean up all progress tracking for this channel
285
+ channel.queue.forEach(audio => cleanupProgressTracking(audio, channelNumber, audioChannels));
286
+ channel.queue = [];
287
+ channel.isPaused = false; // Reset pause state
288
+
289
+ emitQueueChange(channelNumber, audioChannels);
290
+ }
291
+ };
292
+
293
+ /**
294
+ * Stops all audio across all channels and clears all queues
295
+ * @example
296
+ * ```typescript
297
+ * await stopAllAudio(); // Emergency stop - clears everything
298
+ * ```
299
+ */
300
+ export const stopAllAudio = async (): Promise<void> => {
301
+ const stopPromises: Promise<void>[] = [];
302
+ audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
303
+ stopPromises.push(stopAllAudioInChannel(index));
304
+ });
305
+ await Promise.all(stopPromises);
306
+ };