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.
- package/README.md +323 -96
- package/dist/core.d.ts +27 -9
- package/dist/core.js +108 -24
- package/dist/events.d.ts +23 -1
- package/dist/events.js +50 -2
- package/dist/index.d.ts +11 -5
- package/dist/index.js +34 -4
- package/dist/info.d.ts +45 -1
- package/dist/info.js +118 -6
- package/dist/pause.d.ts +87 -0
- package/dist/pause.js +186 -0
- package/dist/types.d.ts +69 -1
- package/dist/utils.d.ts +7 -1
- package/dist/utils.js +24 -4
- package/dist/volume.d.ts +98 -0
- package/dist/volume.js +302 -0
- package/package.json +4 -1
- package/src/core.ts +119 -22
- package/src/events.ts +242 -188
- package/src/index.ts +103 -65
- package/src/info.ts +379 -261
- package/src/pause.ts +190 -0
- package/src/types.ts +198 -126
- package/src/utils.ts +133 -108
- package/src/volume.ts +328 -0
package/dist/volume.d.ts
ADDED
|
@@ -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.
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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(
|
|
140
|
-
* stopCurrentAudioInChannel(); // Stop current audio in
|
|
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
|
-
|
|
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(
|
|
171
|
-
* stopAllAudioInChannel(); // Clear all audio in
|
|
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
|
};
|