audio-channel-queue 1.5.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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @fileoverview Volume management functions for the audio-channel-queue package
3
+ */
4
+ import { VolumeConfig } from './types';
5
+ /**
6
+ * Smoothly transitions volume for a specific channel over time
7
+ * @param channelNumber - The channel number to transition
8
+ * @param targetVolume - Target volume level (0-1)
9
+ * @param duration - Transition duration in milliseconds
10
+ * @param easing - Easing function type
11
+ * @returns Promise that resolves when transition completes
12
+ * @example
13
+ * ```typescript
14
+ * await transitionVolume(0, 0.2, 500, 'ease-out'); // Duck to 20% over 500ms
15
+ * ```
16
+ */
17
+ export declare const transitionVolume: (channelNumber: number, targetVolume: number, duration?: number, easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out") => Promise<void>;
18
+ /**
19
+ * Sets the volume for a specific channel with optional smooth transition
20
+ * @param channelNumber - The channel number to set volume for
21
+ * @param volume - Volume level (0-1)
22
+ * @param transitionDuration - Optional transition duration in milliseconds
23
+ * @param easing - Optional easing function
24
+ * @example
25
+ * ```typescript
26
+ * setChannelVolume(0, 0.5); // Set channel 0 to 50%
27
+ * setChannelVolume(0, 0.5, 300, 'ease-out'); // Smooth transition over 300ms
28
+ * ```
29
+ */
30
+ export declare const setChannelVolume: (channelNumber: number, volume: number, transitionDuration?: number, easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out") => Promise<void>;
31
+ /**
32
+ * Gets the current volume for a specific channel
33
+ * @param channelNumber - The channel number to get volume for (defaults to 0)
34
+ * @returns Current volume level (0-1) or 1.0 if channel doesn't exist
35
+ * @example
36
+ * ```typescript
37
+ * const volume = getChannelVolume(0);
38
+ * const defaultChannelVolume = getChannelVolume(); // Gets channel 0
39
+ * console.log(`Channel 0 volume: ${volume * 100}%`);
40
+ * ```
41
+ */
42
+ export declare const getChannelVolume: (channelNumber?: number) => number;
43
+ /**
44
+ * Gets the volume levels for all channels
45
+ * @returns Array of volume levels (0-1) for each channel
46
+ * @example
47
+ * ```typescript
48
+ * const volumes = getAllChannelsVolume();
49
+ * volumes.forEach((volume, index) => {
50
+ * console.log(`Channel ${index}: ${volume * 100}%`);
51
+ * });
52
+ * ```
53
+ */
54
+ export declare const getAllChannelsVolume: () => number[];
55
+ /**
56
+ * Sets volume for all channels to the same level
57
+ * @param volume - Volume level (0-1) to apply to all channels
58
+ * @example
59
+ * ```typescript
60
+ * await setAllChannelsVolume(0.6); // Set all channels to 60% volume
61
+ * ```
62
+ */
63
+ export declare const setAllChannelsVolume: (volume: number) => Promise<void>;
64
+ /**
65
+ * Configures volume ducking for channels. When the priority channel plays audio,
66
+ * all other channels will be automatically reduced to the ducking volume level
67
+ * @param config - Volume ducking configuration
68
+ * @example
69
+ * ```typescript
70
+ * // When channel 1 plays, reduce all other channels to 20% volume
71
+ * setVolumeDucking({
72
+ * priorityChannel: 1,
73
+ * priorityVolume: 1.0,
74
+ * duckingVolume: 0.2
75
+ * });
76
+ * ```
77
+ */
78
+ export declare const setVolumeDucking: (config: VolumeConfig) => void;
79
+ /**
80
+ * Removes volume ducking configuration from all channels
81
+ * @example
82
+ * ```typescript
83
+ * clearVolumeDucking(); // Remove all volume ducking effects
84
+ * ```
85
+ */
86
+ export declare const clearVolumeDucking: () => void;
87
+ /**
88
+ * Applies volume ducking effects based on current playback state with smooth transitions
89
+ * @param activeChannelNumber - The channel that just started playing
90
+ * @internal
91
+ */
92
+ export declare const applyVolumeDucking: (activeChannelNumber: number) => Promise<void>;
93
+ /**
94
+ * Restores normal volume levels when priority channel stops with smooth transitions
95
+ * @param stoppedChannelNumber - The channel that just stopped playing
96
+ * @internal
97
+ */
98
+ export declare const restoreVolumeLevels: (stoppedChannelNumber: number) => Promise<void>;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "audio-channel-queue",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Allows you to queue audio files to different playback channels.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,6 +31,9 @@
31
31
  "url": "https://github.com/tonycarpenter21/audio-queue-package/issues"
32
32
  },
33
33
  "homepage": "https://github.com/tonycarpenter21/audio-queue-package#readme",
34
+ "engines": {
35
+ "node": ">=14.0.0"
36
+ },
34
37
  "devDependencies": {
35
38
  "@types/jest": "^29.5.13",
36
39
  "jest": "^29.7.0",
package/src/core.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @fileoverview Core queue management functions for the audio-channel-queue package
3
3
  */
4
4
 
5
- import { ExtendedAudioQueueChannel } from './types';
5
+ import { ExtendedAudioQueueChannel, AudioQueueOptions } from './types';
6
6
  import { audioChannels } from './info';
7
7
  import { extractFileName } from './utils';
8
8
  import {
@@ -12,39 +12,103 @@ import {
12
12
  setupProgressTracking,
13
13
  cleanupProgressTracking
14
14
  } from './events';
15
+ import { applyVolumeDucking, restoreVolumeLevels } from './volume';
15
16
 
16
17
  /**
17
18
  * Queues an audio file to a specific channel and starts playing if it's the first in queue
18
19
  * @param audioUrl - The URL of the audio file to queue
19
20
  * @param channelNumber - The channel number to queue the audio to (defaults to 0)
21
+ * @param options - Optional configuration for the audio file
20
22
  * @returns Promise that resolves when the audio is queued and starts playing (if first in queue)
21
23
  * @example
22
24
  * ```typescript
23
25
  * await queueAudio('https://example.com/song.mp3', 0);
24
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
25
29
  * ```
26
30
  */
27
- export const queueAudio = async (audioUrl: string, channelNumber: number = 0): Promise<void> => {
31
+ export const queueAudio = async (
32
+ audioUrl: string,
33
+ channelNumber: number = 0,
34
+ options?: AudioQueueOptions
35
+ ): Promise<void> => {
28
36
  if (!audioChannels[channelNumber]) {
29
37
  audioChannels[channelNumber] = {
30
38
  audioCompleteCallbacks: new Set(),
39
+ audioPauseCallbacks: new Set(),
40
+ audioResumeCallbacks: new Set(),
31
41
  audioStartCallbacks: new Set(),
42
+ isPaused: false,
32
43
  progressCallbacks: new Map(),
33
44
  queue: [],
34
- queueChangeCallbacks: new Set()
45
+ queueChangeCallbacks: new Set(),
46
+ volume: 1.0
35
47
  };
36
48
  }
37
49
 
38
50
  const audio: HTMLAudioElement = new Audio(audioUrl);
39
- audioChannels[channelNumber].queue.push(audio);
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
+ }
40
81
 
41
82
  emitQueueChange(channelNumber, audioChannels);
42
83
 
43
84
  if (audioChannels[channelNumber].queue.length === 1) {
44
- playAudioQueue(channelNumber);
85
+ // Don't await - let playback happen asynchronously
86
+ playAudioQueue(channelNumber).catch(console.error);
45
87
  }
46
88
  };
47
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
+
48
112
  /**
49
113
  * Plays the audio queue for a specific channel
50
114
  * @param channelNumber - The channel number to play
@@ -57,12 +121,20 @@ export const queueAudio = async (audioUrl: string, channelNumber: number = 0): P
57
121
  export const playAudioQueue = async (channelNumber: number): Promise<void> => {
58
122
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
59
123
 
60
- if (channel.queue.length === 0) return;
124
+ if (!channel || channel.queue.length === 0) return;
61
125
 
62
126
  const currentAudio: HTMLAudioElement = channel.queue[0];
63
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
+
64
133
  setupProgressTracking(currentAudio, channelNumber, audioChannels);
65
134
 
135
+ // Apply volume ducking when audio starts
136
+ await applyVolumeDucking(channelNumber);
137
+
66
138
  return new Promise<void>((resolve) => {
67
139
  let hasStarted: boolean = false;
68
140
  let metadataLoaded: boolean = false;
@@ -102,19 +174,33 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
102
174
  src: currentAudio.src
103
175
  }, audioChannels);
104
176
 
177
+ // Restore volume levels when priority channel stops
178
+ await restoreVolumeLevels(channelNumber);
179
+
105
180
  // Clean up event listeners
106
181
  currentAudio.removeEventListener('loadedmetadata', handleLoadedMetadata);
107
182
  currentAudio.removeEventListener('play', handlePlay);
108
183
  currentAudio.removeEventListener('ended', handleEnded);
109
184
 
110
185
  cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
111
- channel.queue.shift();
112
-
113
- // Emit queue change after completion
114
- setTimeout(() => emitQueueChange(channelNumber, audioChannels), 10);
115
186
 
116
- await playAudioQueue(channelNumber);
117
- resolve();
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
+ }
118
204
  };
119
205
 
120
206
  // Add event listeners
@@ -136,11 +222,11 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
136
222
  * @param channelNumber - The channel number (defaults to 0)
137
223
  * @example
138
224
  * ```typescript
139
- * stopCurrentAudioInChannel(0); // Stop current audio in channel 0
140
- * stopCurrentAudioInChannel(); // Stop current audio in default channel
225
+ * await stopCurrentAudioInChannel(); // Stop current audio in default channel (0)
226
+ * await stopCurrentAudioInChannel(1); // Stop current audio in channel 1
141
227
  * ```
142
228
  */
143
- export const stopCurrentAudioInChannel = (channelNumber: number = 0): void => {
229
+ export const stopCurrentAudioInChannel = async (channelNumber: number = 0): Promise<void> => {
144
230
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
145
231
  if (channel && channel.queue.length > 0) {
146
232
  const currentAudio: HTMLAudioElement = channel.queue[0];
@@ -152,13 +238,18 @@ export const stopCurrentAudioInChannel = (channelNumber: number = 0): void => {
152
238
  src: currentAudio.src
153
239
  }, audioChannels);
154
240
 
241
+ // Restore volume levels when stopping
242
+ await restoreVolumeLevels(channelNumber);
243
+
155
244
  currentAudio.pause();
156
245
  cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
157
246
  channel.queue.shift();
247
+ channel.isPaused = false; // Reset pause state
158
248
 
159
249
  emitQueueChange(channelNumber, audioChannels);
160
250
 
161
- playAudioQueue(channelNumber);
251
+ // Start next audio without waiting for it to complete
252
+ playAudioQueue(channelNumber).catch(console.error);
162
253
  }
163
254
  };
164
255
 
@@ -167,11 +258,11 @@ export const stopCurrentAudioInChannel = (channelNumber: number = 0): void => {
167
258
  * @param channelNumber - The channel number (defaults to 0)
168
259
  * @example
169
260
  * ```typescript
170
- * stopAllAudioInChannel(0); // Clear all audio in channel 0
171
- * stopAllAudioInChannel(); // Clear all audio in default channel
261
+ * await stopAllAudioInChannel(); // Clear all audio in default channel (0)
262
+ * await stopAllAudioInChannel(1); // Clear all audio in channel 1
172
263
  * ```
173
264
  */
174
- export const stopAllAudioInChannel = (channelNumber: number = 0): void => {
265
+ export const stopAllAudioInChannel = async (channelNumber: number = 0): Promise<void> => {
175
266
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
176
267
  if (channel) {
177
268
  if (channel.queue.length > 0) {
@@ -184,12 +275,16 @@ export const stopAllAudioInChannel = (channelNumber: number = 0): void => {
184
275
  src: currentAudio.src
185
276
  }, audioChannels);
186
277
 
278
+ // Restore volume levels when stopping
279
+ await restoreVolumeLevels(channelNumber);
280
+
187
281
  currentAudio.pause();
188
282
  cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
189
283
  }
190
284
  // Clean up all progress tracking for this channel
191
285
  channel.queue.forEach(audio => cleanupProgressTracking(audio, channelNumber, audioChannels));
192
286
  channel.queue = [];
287
+ channel.isPaused = false; // Reset pause state
193
288
 
194
289
  emitQueueChange(channelNumber, audioChannels);
195
290
  }
@@ -199,11 +294,13 @@ export const stopAllAudioInChannel = (channelNumber: number = 0): void => {
199
294
  * Stops all audio across all channels and clears all queues
200
295
  * @example
201
296
  * ```typescript
202
- * stopAllAudio(); // Emergency stop - clears everything
297
+ * await stopAllAudio(); // Emergency stop - clears everything
203
298
  * ```
204
299
  */
205
- export const stopAllAudio = (): void => {
300
+ export const stopAllAudio = async (): Promise<void> => {
301
+ const stopPromises: Promise<void>[] = [];
206
302
  audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
207
- stopAllAudioInChannel(index);
303
+ stopPromises.push(stopAllAudioInChannel(index));
208
304
  });
305
+ await Promise.all(stopPromises);
209
306
  };