audio-channel-queue 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.js +4 -3
- package/dist/errors.js +27 -17
- package/dist/events.js +21 -14
- package/dist/index.d.ts +6 -5
- package/dist/index.js +16 -1
- package/dist/info.js +8 -7
- package/dist/pause.d.ts +83 -0
- package/dist/pause.js +258 -3
- package/dist/types.d.ts +53 -4
- package/dist/types.js +25 -0
- package/dist/utils.js +4 -3
- package/dist/volume.d.ts +28 -3
- package/dist/volume.js +77 -15
- package/package.json +8 -3
- package/src/core.ts +59 -42
- package/src/errors.ts +504 -480
- package/src/events.ts +36 -27
- package/src/index.ts +60 -39
- package/src/info.ts +23 -22
- package/src/pause.ts +328 -23
- package/src/types.ts +57 -3
- package/src/utils.ts +7 -7
- package/src/volume.ts +92 -30
package/src/pause.ts
CHANGED
|
@@ -2,10 +2,309 @@
|
|
|
2
2
|
* @fileoverview Pause and resume management functions for the audio-channel-queue package
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ExtendedAudioQueueChannel,
|
|
7
|
+
AudioInfo,
|
|
8
|
+
FadeType,
|
|
9
|
+
FadeConfig,
|
|
10
|
+
ChannelFadeState
|
|
11
|
+
} from './types';
|
|
6
12
|
import { audioChannels } from './info';
|
|
7
13
|
import { getAudioInfoFromElement } from './utils';
|
|
8
14
|
import { emitAudioPause, emitAudioResume } from './events';
|
|
15
|
+
import { transitionVolume, getFadeConfig } from './volume';
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets the current volume for a channel, accounting for synchronous state
|
|
21
|
+
* @param channelNumber - The channel number
|
|
22
|
+
* @returns Current volume level (0-1)
|
|
23
|
+
*/
|
|
24
|
+
const getChannelVolumeSync = (channelNumber: number): number => {
|
|
25
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
26
|
+
return channel?.volume ?? 1.0;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sets the channel volume synchronously in internal state
|
|
31
|
+
* @param channelNumber - The channel number
|
|
32
|
+
* @param volume - Volume level (0-1)
|
|
33
|
+
*/
|
|
34
|
+
const setChannelVolumeSync = (channelNumber: number, volume: number): void => {
|
|
35
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
36
|
+
if (channel) {
|
|
37
|
+
channel.volume = volume;
|
|
38
|
+
if (channel.queue.length > 0) {
|
|
39
|
+
channel.queue[0].volume = volume;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pauses the currently playing audio in a specific channel with smooth volume fade
|
|
46
|
+
* @param fadeType - Type of fade transition to apply
|
|
47
|
+
* @param channelNumber - The channel number to pause (defaults to 0)
|
|
48
|
+
* @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
|
|
49
|
+
* @returns Promise that resolves when the pause and fade are complete
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* await pauseWithFade(FadeType.Gentle, 0); // Pause with gentle fade out over 800ms
|
|
53
|
+
* await pauseWithFade(FadeType.Dramatic, 1, 1500); // Pause with dramatic fade out over 1.5s
|
|
54
|
+
* await pauseWithFade(FadeType.Linear, 2, 500); // Linear pause with custom 500ms fade
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export const pauseWithFade = async (
|
|
58
|
+
fadeType: FadeType = FadeType.Gentle,
|
|
59
|
+
channelNumber: number = 0,
|
|
60
|
+
duration?: number
|
|
61
|
+
): Promise<void> => {
|
|
62
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
63
|
+
|
|
64
|
+
if (!channel || channel.queue.length === 0) return;
|
|
65
|
+
|
|
66
|
+
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
67
|
+
|
|
68
|
+
// Don't pause if already paused or ended
|
|
69
|
+
if (currentAudio.paused || currentAudio.ended) return;
|
|
70
|
+
|
|
71
|
+
const config: FadeConfig = getFadeConfig(fadeType);
|
|
72
|
+
const effectiveDuration: number = duration ?? config.duration;
|
|
73
|
+
|
|
74
|
+
// Race condition fix: Use existing fadeState originalVolume if already transitioning,
|
|
75
|
+
// otherwise capture current volume
|
|
76
|
+
let originalVolume: number;
|
|
77
|
+
if (channel.fadeState?.isTransitioning) {
|
|
78
|
+
// We're already in any kind of transition (pause or resume), preserve original volume
|
|
79
|
+
originalVolume = channel.fadeState.originalVolume;
|
|
80
|
+
} else {
|
|
81
|
+
// First fade or no transition in progress, capture current volume
|
|
82
|
+
// But ensure we don't capture a volume of 0 during a transition
|
|
83
|
+
const currentVolume = getChannelVolumeSync(channelNumber);
|
|
84
|
+
originalVolume = currentVolume > 0 ? currentVolume : channel.fadeState?.originalVolume ?? 1.0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Store fade state for resumeWithFade to use (including custom duration)
|
|
88
|
+
channel.fadeState = {
|
|
89
|
+
customDuration: duration,
|
|
90
|
+
fadeType,
|
|
91
|
+
isPaused: true,
|
|
92
|
+
isTransitioning: true,
|
|
93
|
+
originalVolume
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (effectiveDuration === 0) {
|
|
97
|
+
// Instant pause
|
|
98
|
+
await pauseChannel(channelNumber);
|
|
99
|
+
// Reset volume to original for resume (synchronously to avoid state issues)
|
|
100
|
+
setChannelVolumeSync(channelNumber, originalVolume);
|
|
101
|
+
// Mark transition as complete for instant pause
|
|
102
|
+
if (channel.fadeState) {
|
|
103
|
+
channel.fadeState.isTransitioning = false;
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fade to 0 with pause curve, then pause
|
|
109
|
+
await transitionVolume(channelNumber, 0, effectiveDuration, config.pauseCurve);
|
|
110
|
+
await pauseChannel(channelNumber);
|
|
111
|
+
|
|
112
|
+
// Reset volume to original for resume (synchronously to avoid state issues)
|
|
113
|
+
setChannelVolumeSync(channelNumber, originalVolume);
|
|
114
|
+
|
|
115
|
+
// Mark transition as complete
|
|
116
|
+
if (channel.fadeState) {
|
|
117
|
+
channel.fadeState.isTransitioning = false;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resumes the currently paused audio in a specific channel with smooth volume fade
|
|
123
|
+
* Uses the complementary fade curve automatically based on the pause fade type, or allows override
|
|
124
|
+
* @param fadeType - Optional fade type to override the stored fade type from pause
|
|
125
|
+
* @param channelNumber - The channel number to resume (defaults to 0)
|
|
126
|
+
* @param duration - Optional custom fade duration in milliseconds (uses stored or fadeType default if not provided)
|
|
127
|
+
* @returns Promise that resolves when the resume and fade are complete
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* await resumeWithFade(); // Resume with automatically paired fade curve from pause
|
|
131
|
+
* await resumeWithFade(FadeType.Dramatic, 0); // Override with dramatic fade
|
|
132
|
+
* await resumeWithFade(FadeType.Linear, 0, 1000); // Override with linear fade over 1 second
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export const resumeWithFade = async (
|
|
136
|
+
fadeType?: FadeType,
|
|
137
|
+
channelNumber: number = 0,
|
|
138
|
+
duration?: number
|
|
139
|
+
): Promise<void> => {
|
|
140
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
141
|
+
|
|
142
|
+
if (!channel || channel.queue.length === 0) return;
|
|
143
|
+
|
|
144
|
+
const fadeState: ChannelFadeState | undefined = channel.fadeState;
|
|
145
|
+
if (!fadeState?.isPaused) {
|
|
146
|
+
// Fall back to regular resume if no fade state
|
|
147
|
+
await resumeChannel(channelNumber);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Use provided fadeType or fall back to stored fadeType from pause
|
|
152
|
+
const effectiveFadeType: FadeType = fadeType ?? fadeState.fadeType;
|
|
153
|
+
const config: FadeConfig = getFadeConfig(effectiveFadeType);
|
|
154
|
+
|
|
155
|
+
// Determine effective duration: custom parameter > stored custom > fadeType default
|
|
156
|
+
let effectiveDuration: number;
|
|
157
|
+
if (duration !== undefined) {
|
|
158
|
+
effectiveDuration = duration;
|
|
159
|
+
} else if (fadeState.customDuration !== undefined) {
|
|
160
|
+
effectiveDuration = fadeState.customDuration;
|
|
161
|
+
} else {
|
|
162
|
+
effectiveDuration = config.duration;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (effectiveDuration === 0) {
|
|
166
|
+
// Instant resume
|
|
167
|
+
const targetVolume = fadeState.originalVolume > 0 ? fadeState.originalVolume : 1.0;
|
|
168
|
+
setChannelVolumeSync(channelNumber, targetVolume);
|
|
169
|
+
await resumeChannel(channelNumber);
|
|
170
|
+
fadeState.isPaused = false;
|
|
171
|
+
fadeState.isTransitioning = false;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Race condition fix: Ensure we have a valid original volume to restore to
|
|
176
|
+
const targetVolume = fadeState.originalVolume > 0 ? fadeState.originalVolume : 1.0;
|
|
177
|
+
|
|
178
|
+
// Mark as transitioning to prevent volume capture during rapid toggles
|
|
179
|
+
fadeState.isTransitioning = true;
|
|
180
|
+
|
|
181
|
+
// Set volume to 0, resume, then fade to original with resume curve
|
|
182
|
+
setChannelVolumeSync(channelNumber, 0);
|
|
183
|
+
await resumeChannel(channelNumber);
|
|
184
|
+
|
|
185
|
+
// Use the stored original volume, not current volume, to prevent race conditions
|
|
186
|
+
await transitionVolume(channelNumber, targetVolume, effectiveDuration, config.resumeCurve);
|
|
187
|
+
|
|
188
|
+
fadeState.isPaused = false;
|
|
189
|
+
fadeState.isTransitioning = false;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Toggles pause/resume state for a specific channel with integrated fade
|
|
194
|
+
* @param fadeType - Type of fade transition to apply when pausing
|
|
195
|
+
* @param channelNumber - The channel number to toggle (defaults to 0)
|
|
196
|
+
* @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
|
|
197
|
+
* @returns Promise that resolves when the toggle and fade are complete
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* await togglePauseWithFade(FadeType.Gentle, 0); // Toggle with gentle fade
|
|
201
|
+
* await togglePauseWithFade(FadeType.Dramatic, 0, 500); // Toggle with custom 500ms fade
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export const togglePauseWithFade = async (
|
|
205
|
+
fadeType: FadeType = FadeType.Gentle,
|
|
206
|
+
channelNumber: number = 0,
|
|
207
|
+
duration?: number
|
|
208
|
+
): Promise<void> => {
|
|
209
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
210
|
+
|
|
211
|
+
if (!channel || channel.queue.length === 0) return;
|
|
212
|
+
|
|
213
|
+
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
214
|
+
|
|
215
|
+
if (currentAudio.paused) {
|
|
216
|
+
await resumeWithFade(undefined, channelNumber, duration);
|
|
217
|
+
} else {
|
|
218
|
+
await pauseWithFade(fadeType, channelNumber, duration);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Pauses all currently playing audio across all channels with smooth volume fade
|
|
224
|
+
* @param fadeType - Type of fade transition to apply to all channels
|
|
225
|
+
* @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
|
|
226
|
+
* @returns Promise that resolves when all channels are paused and faded
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* await pauseAllWithFade(FadeType.Dramatic); // Pause everything with dramatic fade
|
|
230
|
+
* await pauseAllWithFade(FadeType.Gentle, 1200); // Pause all channels with custom 1.2s fade
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
export const pauseAllWithFade = async (
|
|
234
|
+
fadeType: FadeType = FadeType.Gentle,
|
|
235
|
+
duration?: number
|
|
236
|
+
): Promise<void> => {
|
|
237
|
+
const pausePromises: Promise<void>[] = [];
|
|
238
|
+
|
|
239
|
+
audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
|
|
240
|
+
pausePromises.push(pauseWithFade(fadeType, index, duration));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await Promise.all(pausePromises);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resumes all currently paused audio across all channels with smooth volume fade
|
|
248
|
+
* Uses automatically paired fade curves based on each channel's pause fade type, or allows override
|
|
249
|
+
* @param fadeType - Optional fade type to override stored fade types for all channels
|
|
250
|
+
* @param duration - Optional custom fade duration in milliseconds (uses stored or fadeType default if not provided)
|
|
251
|
+
* @returns Promise that resolves when all channels are resumed and faded
|
|
252
|
+
* @example
|
|
253
|
+
* ```typescript
|
|
254
|
+
* await resumeAllWithFade(); // Resume everything with paired fade curves
|
|
255
|
+
* await resumeAllWithFade(FadeType.Gentle, 800); // Override all channels with gentle fade over 800ms
|
|
256
|
+
* await resumeAllWithFade(undefined, 600); // Use stored fade types with custom 600ms duration
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
export const resumeAllWithFade = async (fadeType?: FadeType, duration?: number): Promise<void> => {
|
|
260
|
+
const resumePromises: Promise<void>[] = [];
|
|
261
|
+
|
|
262
|
+
audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
|
|
263
|
+
resumePromises.push(resumeWithFade(fadeType, index, duration));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await Promise.all(resumePromises);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Toggles pause/resume state for all channels with integrated fade
|
|
271
|
+
* If any channels are playing, all will be paused with fade
|
|
272
|
+
* If all channels are paused, all will be resumed with fade
|
|
273
|
+
* @param fadeType - Type of fade transition to apply when pausing
|
|
274
|
+
* @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
|
|
275
|
+
* @returns Promise that resolves when all toggles and fades are complete
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* await togglePauseAllWithFade(FadeType.Gentle); // Global toggle with gentle fade
|
|
279
|
+
* await togglePauseAllWithFade(FadeType.Dramatic, 600); // Global toggle with custom 600ms fade
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
export const togglePauseAllWithFade = async (
|
|
283
|
+
fadeType: FadeType = FadeType.Gentle,
|
|
284
|
+
duration?: number
|
|
285
|
+
): Promise<void> => {
|
|
286
|
+
let hasPlayingChannel: boolean = false;
|
|
287
|
+
|
|
288
|
+
// Check if any channel is currently playing
|
|
289
|
+
for (let i: number = 0; i < audioChannels.length; i++) {
|
|
290
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[i];
|
|
291
|
+
if (channel && channel.queue.length > 0) {
|
|
292
|
+
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
293
|
+
if (!currentAudio.paused && !currentAudio.ended) {
|
|
294
|
+
hasPlayingChannel = true;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// If any channel is playing, pause all with fade
|
|
301
|
+
// If no channels are playing, resume all with fade
|
|
302
|
+
if (hasPlayingChannel) {
|
|
303
|
+
await pauseAllWithFade(fadeType, duration);
|
|
304
|
+
} else {
|
|
305
|
+
await resumeAllWithFade(fadeType, duration);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
9
308
|
|
|
10
309
|
/**
|
|
11
310
|
* Pauses the currently playing audio in a specific channel
|
|
@@ -19,15 +318,19 @@ import { emitAudioPause, emitAudioResume } from './events';
|
|
|
19
318
|
*/
|
|
20
319
|
export const pauseChannel = async (channelNumber: number = 0): Promise<void> => {
|
|
21
320
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
22
|
-
|
|
321
|
+
|
|
23
322
|
if (channel && channel.queue.length > 0) {
|
|
24
323
|
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
25
|
-
|
|
324
|
+
|
|
26
325
|
if (!currentAudio.paused && !currentAudio.ended) {
|
|
27
326
|
currentAudio.pause();
|
|
28
327
|
channel.isPaused = true;
|
|
29
|
-
|
|
30
|
-
const audioInfo: AudioInfo | null = getAudioInfoFromElement(
|
|
328
|
+
|
|
329
|
+
const audioInfo: AudioInfo | null = getAudioInfoFromElement(
|
|
330
|
+
currentAudio,
|
|
331
|
+
channelNumber,
|
|
332
|
+
audioChannels
|
|
333
|
+
);
|
|
31
334
|
if (audioInfo) {
|
|
32
335
|
emitAudioPause(channelNumber, audioInfo, audioChannels);
|
|
33
336
|
}
|
|
@@ -47,16 +350,20 @@ export const pauseChannel = async (channelNumber: number = 0): Promise<void> =>
|
|
|
47
350
|
*/
|
|
48
351
|
export const resumeChannel = async (channelNumber: number = 0): Promise<void> => {
|
|
49
352
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
50
|
-
|
|
353
|
+
|
|
51
354
|
if (channel && channel.queue.length > 0) {
|
|
52
355
|
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
53
|
-
|
|
356
|
+
|
|
54
357
|
// Only resume if both the channel is marked as paused AND the audio element is actually paused AND not ended
|
|
55
358
|
if (channel.isPaused && currentAudio.paused && !currentAudio.ended) {
|
|
56
359
|
await currentAudio.play();
|
|
57
360
|
channel.isPaused = false;
|
|
58
|
-
|
|
59
|
-
const audioInfo: AudioInfo | null = getAudioInfoFromElement(
|
|
361
|
+
|
|
362
|
+
const audioInfo: AudioInfo | null = getAudioInfoFromElement(
|
|
363
|
+
currentAudio,
|
|
364
|
+
channelNumber,
|
|
365
|
+
audioChannels
|
|
366
|
+
);
|
|
60
367
|
if (audioInfo) {
|
|
61
368
|
emitAudioResume(channelNumber, audioInfo, audioChannels);
|
|
62
369
|
}
|
|
@@ -75,10 +382,10 @@ export const resumeChannel = async (channelNumber: number = 0): Promise<void> =>
|
|
|
75
382
|
*/
|
|
76
383
|
export const togglePauseChannel = async (channelNumber: number = 0): Promise<void> => {
|
|
77
384
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
78
|
-
|
|
385
|
+
|
|
79
386
|
if (channel && channel.queue.length > 0) {
|
|
80
387
|
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
81
|
-
|
|
388
|
+
|
|
82
389
|
if (currentAudio.paused) {
|
|
83
390
|
await resumeChannel(channelNumber);
|
|
84
391
|
} else {
|
|
@@ -97,11 +404,11 @@ export const togglePauseChannel = async (channelNumber: number = 0): Promise<voi
|
|
|
97
404
|
*/
|
|
98
405
|
export const pauseAllChannels = async (): Promise<void> => {
|
|
99
406
|
const pausePromises: Promise<void>[] = [];
|
|
100
|
-
|
|
407
|
+
|
|
101
408
|
audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
|
|
102
409
|
pausePromises.push(pauseChannel(index));
|
|
103
410
|
});
|
|
104
|
-
|
|
411
|
+
|
|
105
412
|
await Promise.all(pausePromises);
|
|
106
413
|
};
|
|
107
414
|
|
|
@@ -115,11 +422,11 @@ export const pauseAllChannels = async (): Promise<void> => {
|
|
|
115
422
|
*/
|
|
116
423
|
export const resumeAllChannels = async (): Promise<void> => {
|
|
117
424
|
const resumePromises: Promise<void>[] = [];
|
|
118
|
-
|
|
425
|
+
|
|
119
426
|
audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
|
|
120
427
|
resumePromises.push(resumeChannel(index));
|
|
121
428
|
});
|
|
122
|
-
|
|
429
|
+
|
|
123
430
|
await Promise.all(resumePromises);
|
|
124
431
|
};
|
|
125
432
|
|
|
@@ -135,7 +442,7 @@ export const resumeAllChannels = async (): Promise<void> => {
|
|
|
135
442
|
*/
|
|
136
443
|
export const isChannelPaused = (channelNumber: number = 0): boolean => {
|
|
137
444
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
138
|
-
return channel?.isPaused
|
|
445
|
+
return channel?.isPaused ?? false;
|
|
139
446
|
};
|
|
140
447
|
|
|
141
448
|
/**
|
|
@@ -150,9 +457,7 @@ export const isChannelPaused = (channelNumber: number = 0): boolean => {
|
|
|
150
457
|
* ```
|
|
151
458
|
*/
|
|
152
459
|
export const getAllChannelsPauseState = (): boolean[] => {
|
|
153
|
-
return audioChannels.map((channel: ExtendedAudioQueueChannel) =>
|
|
154
|
-
channel?.isPaused || false
|
|
155
|
-
);
|
|
460
|
+
return audioChannels.map((channel: ExtendedAudioQueueChannel) => channel?.isPaused ?? false);
|
|
156
461
|
};
|
|
157
462
|
|
|
158
463
|
/**
|
|
@@ -167,9 +472,9 @@ export const getAllChannelsPauseState = (): boolean[] => {
|
|
|
167
472
|
*/
|
|
168
473
|
export const togglePauseAllChannels = async (): Promise<void> => {
|
|
169
474
|
let hasPlayingChannel: boolean = false;
|
|
170
|
-
|
|
475
|
+
|
|
171
476
|
// Check if any channel is currently playing
|
|
172
|
-
for (let i = 0; i < audioChannels.length; i++) {
|
|
477
|
+
for (let i: number = 0; i < audioChannels.length; i++) {
|
|
173
478
|
const channel: ExtendedAudioQueueChannel = audioChannels[i];
|
|
174
479
|
if (channel && channel.queue.length > 0) {
|
|
175
480
|
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
@@ -179,7 +484,7 @@ export const togglePauseAllChannels = async (): Promise<void> => {
|
|
|
179
484
|
}
|
|
180
485
|
}
|
|
181
486
|
}
|
|
182
|
-
|
|
487
|
+
|
|
183
488
|
// If any channel is playing, pause all channels
|
|
184
489
|
// If no channels are playing, resume all channels
|
|
185
490
|
if (hasPlayingChannel) {
|
|
@@ -187,4 +492,4 @@ export const togglePauseAllChannels = async (): Promise<void> => {
|
|
|
187
492
|
} else {
|
|
188
493
|
await resumeAllChannels();
|
|
189
494
|
}
|
|
190
|
-
};
|
|
495
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* @fileoverview Type definitions for the audio-channel-queue package
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Symbol used as a key for global (channel-wide) progress callbacks
|
|
7
|
+
* This avoids the need for `null as any` type assertions
|
|
8
|
+
*/
|
|
9
|
+
export const GLOBAL_PROGRESS_KEY: unique symbol = Symbol('global-progress-callbacks');
|
|
10
|
+
|
|
5
11
|
/**
|
|
6
12
|
* Array of HTMLAudioElement objects representing an audio queue
|
|
7
13
|
*/
|
|
@@ -10,7 +16,7 @@ export type AudioQueue = HTMLAudioElement[];
|
|
|
10
16
|
/**
|
|
11
17
|
* Basic audio queue channel structure
|
|
12
18
|
*/
|
|
13
|
-
export
|
|
19
|
+
export interface AudioQueueChannel {
|
|
14
20
|
queue: AudioQueue;
|
|
15
21
|
}
|
|
16
22
|
|
|
@@ -29,7 +35,7 @@ export interface VolumeConfig {
|
|
|
29
35
|
/** Duration in milliseconds for volume restore transition (defaults to 500ms) */
|
|
30
36
|
restoreTransitionDuration?: number;
|
|
31
37
|
/** Easing function for volume transitions (defaults to 'ease-out') */
|
|
32
|
-
transitionEasing?:
|
|
38
|
+
transitionEasing?: EasingType;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/**
|
|
@@ -226,11 +232,59 @@ export interface ExtendedAudioQueueChannel {
|
|
|
226
232
|
audioPauseCallbacks: Set<AudioPauseCallback>;
|
|
227
233
|
audioResumeCallbacks: Set<AudioResumeCallback>;
|
|
228
234
|
audioStartCallbacks: Set<AudioStartCallback>;
|
|
235
|
+
fadeState?: ChannelFadeState;
|
|
229
236
|
isPaused?: boolean;
|
|
230
|
-
progressCallbacks: Map<HTMLAudioElement |
|
|
237
|
+
progressCallbacks: Map<HTMLAudioElement | typeof GLOBAL_PROGRESS_KEY, Set<ProgressCallback>>;
|
|
231
238
|
queue: HTMLAudioElement[];
|
|
232
239
|
queueChangeCallbacks: Set<QueueChangeCallback>;
|
|
233
240
|
retryConfig?: RetryConfig;
|
|
234
241
|
volume?: number;
|
|
235
242
|
volumeConfig?: VolumeConfig;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Easing function types for volume transitions
|
|
247
|
+
*/
|
|
248
|
+
export enum EasingType {
|
|
249
|
+
Linear = 'linear',
|
|
250
|
+
EaseIn = 'ease-in',
|
|
251
|
+
EaseOut = 'ease-out',
|
|
252
|
+
EaseInOut = 'ease-in-out'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Fade type for pause/resume operations with integrated volume transitions
|
|
257
|
+
*/
|
|
258
|
+
export enum FadeType {
|
|
259
|
+
Linear = 'linear',
|
|
260
|
+
Gentle = 'gentle',
|
|
261
|
+
Dramatic = 'dramatic'
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Configuration for fade transitions
|
|
266
|
+
*/
|
|
267
|
+
export interface FadeConfig {
|
|
268
|
+
/** Duration in milliseconds for the fade transition */
|
|
269
|
+
duration: number;
|
|
270
|
+
/** Easing curve to use when pausing (fading out) */
|
|
271
|
+
pauseCurve: EasingType;
|
|
272
|
+
/** Easing curve to use when resuming (fading in) */
|
|
273
|
+
resumeCurve: EasingType;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Internal fade state tracking for pause/resume with fade functionality
|
|
278
|
+
*/
|
|
279
|
+
export interface ChannelFadeState {
|
|
280
|
+
/** The original volume level before fading began */
|
|
281
|
+
originalVolume: number;
|
|
282
|
+
/** The type of fade being used */
|
|
283
|
+
fadeType: FadeType;
|
|
284
|
+
/** Whether the channel is currently paused due to fade */
|
|
285
|
+
isPaused: boolean;
|
|
286
|
+
/** Custom duration in milliseconds if specified (overrides fade type default) */
|
|
287
|
+
customDuration?: number;
|
|
288
|
+
/** Whether the channel is currently transitioning (during any fade operation) to prevent capturing intermediate volumes during rapid pause/resume toggles */
|
|
289
|
+
isTransitioning?: boolean;
|
|
236
290
|
}
|
package/src/utils.ts
CHANGED
|
@@ -40,14 +40,14 @@ export const extractFileName = (url: string): string => {
|
|
|
40
40
|
* const audioElement = new Audio('song.mp3');
|
|
41
41
|
* const info = getAudioInfoFromElement(audioElement);
|
|
42
42
|
* console.log(info?.progress); // Current progress as decimal (0-1)
|
|
43
|
-
*
|
|
43
|
+
*
|
|
44
44
|
* // With channel context for remainingInQueue
|
|
45
45
|
* const infoWithQueue = getAudioInfoFromElement(audioElement, 0, audioChannels);
|
|
46
46
|
* console.log(infoWithQueue?.remainingInQueue); // Number of items left in queue
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
49
|
export const getAudioInfoFromElement = (
|
|
50
|
-
audio: HTMLAudioElement,
|
|
50
|
+
audio: HTMLAudioElement,
|
|
51
51
|
channelNumber?: number,
|
|
52
52
|
audioChannels?: ExtendedAudioQueueChannel[]
|
|
53
53
|
): AudioInfo | null => {
|
|
@@ -60,7 +60,7 @@ export const getAudioInfoFromElement = (
|
|
|
60
60
|
|
|
61
61
|
// Calculate remainingInQueue if channel context is provided
|
|
62
62
|
let remainingInQueue: number = 0;
|
|
63
|
-
if (channelNumber !== undefined && audioChannels
|
|
63
|
+
if (channelNumber !== undefined && audioChannels?.[channelNumber]) {
|
|
64
64
|
const channel = audioChannels[channelNumber];
|
|
65
65
|
remainingInQueue = Math.max(0, channel.queue.length - 1); // Exclude current playing audio
|
|
66
66
|
}
|
|
@@ -91,7 +91,7 @@ export const getAudioInfoFromElement = (
|
|
|
91
91
|
* ```
|
|
92
92
|
*/
|
|
93
93
|
export const createQueueSnapshot = (
|
|
94
|
-
channelNumber: number,
|
|
94
|
+
channelNumber: number,
|
|
95
95
|
audioChannels: ExtendedAudioQueueChannel[]
|
|
96
96
|
): QueueSnapshot | null => {
|
|
97
97
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
@@ -109,10 +109,10 @@ export const createQueueSnapshot = (
|
|
|
109
109
|
return {
|
|
110
110
|
channelNumber,
|
|
111
111
|
currentIndex: 0, // Current playing is always index 0 in our queue structure
|
|
112
|
-
isPaused: channel.isPaused
|
|
112
|
+
isPaused: channel.isPaused ?? false,
|
|
113
113
|
items,
|
|
114
114
|
totalItems: channel.queue.length,
|
|
115
|
-
volume: channel.volume
|
|
115
|
+
volume: channel.volume ?? 1.0
|
|
116
116
|
};
|
|
117
117
|
};
|
|
118
118
|
|
|
@@ -131,4 +131,4 @@ export const createQueueSnapshot = (
|
|
|
131
131
|
export const cleanWebpackFilename = (fileName: string): string => {
|
|
132
132
|
// Remove webpack hash pattern: filename.hash.ext → filename.ext
|
|
133
133
|
return fileName.replace(/\.[a-f0-9]{8,}\./i, '.');
|
|
134
|
-
};
|
|
134
|
+
};
|