audio-channel-queue 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/pause.ts CHANGED
@@ -2,20 +2,17 @@
2
2
  * @fileoverview Pause and resume management functions for the audio-channel-queue package
3
3
  */
4
4
 
5
- import { ExtendedAudioQueueChannel, AudioInfo, FadeType, FadeConfig, ChannelFadeState, EasingType } from './types';
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';
9
- import { transitionVolume } from './volume';
10
-
11
- /**
12
- * Predefined fade configurations for different transition types
13
- */
14
- const FADE_CONFIGS: Record<FadeType, FadeConfig> = {
15
- [FadeType.Linear]: { duration: 800, pauseCurve: EasingType.Linear, resumeCurve: EasingType.Linear },
16
- [FadeType.Gentle]: { duration: 800, pauseCurve: EasingType.EaseOut, resumeCurve: EasingType.EaseIn },
17
- [FadeType.Dramatic]: { duration: 800, pauseCurve: EasingType.EaseIn, resumeCurve: EasingType.EaseOut }
18
- };
15
+ import { transitionVolume, getFadeConfig } from './volume';
19
16
 
20
17
  /**
21
18
  * Gets the current volume for a channel, accounting for synchronous state
@@ -24,7 +21,7 @@ const FADE_CONFIGS: Record<FadeType, FadeConfig> = {
24
21
  */
25
22
  const getChannelVolumeSync = (channelNumber: number): number => {
26
23
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
27
- return channel?.volume || 1.0;
24
+ return channel?.volume ?? 1.0;
28
25
  };
29
26
 
30
27
  /**
@@ -46,46 +43,77 @@ const setChannelVolumeSync = (channelNumber: number, volume: number): void => {
46
43
  * Pauses the currently playing audio in a specific channel with smooth volume fade
47
44
  * @param fadeType - Type of fade transition to apply
48
45
  * @param channelNumber - The channel number to pause (defaults to 0)
46
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
49
47
  * @returns Promise that resolves when the pause and fade are complete
50
48
  * @example
51
49
  * ```typescript
52
50
  * await pauseWithFade(FadeType.Gentle, 0); // Pause with gentle fade out over 800ms
53
- * await pauseWithFade(FadeType.Dramatic, 1); // Pause with dramatic fade out over 800ms
54
- * await pauseWithFade(FadeType.Linear, 2); // Linear pause with 800ms fade
51
+ * await pauseWithFade(FadeType.Dramatic, 1, 1500); // Pause with dramatic fade out over 1.5s
52
+ * await pauseWithFade(FadeType.Linear, 2, 500); // Linear pause with custom 500ms fade
55
53
  * ```
56
54
  */
57
- export const pauseWithFade = async (fadeType: FadeType = FadeType.Gentle, channelNumber: number = 0): Promise<void> => {
55
+ export const pauseWithFade = async (
56
+ fadeType: FadeType = FadeType.Gentle,
57
+ channelNumber: number = 0,
58
+ duration?: number
59
+ ): Promise<void> => {
58
60
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
59
-
61
+
60
62
  if (!channel || channel.queue.length === 0) return;
61
-
63
+
62
64
  const currentAudio: HTMLAudioElement = channel.queue[0];
63
-
65
+
64
66
  // Don't pause if already paused or ended
65
67
  if (currentAudio.paused || currentAudio.ended) return;
66
-
67
- const config: FadeConfig = FADE_CONFIGS[fadeType];
68
- const originalVolume: number = getChannelVolumeSync(channelNumber);
69
-
70
- // Store fade state for resumeWithFade to use
68
+
69
+ const config: FadeConfig = getFadeConfig(fadeType);
70
+ const effectiveDuration: number = duration ?? config.duration;
71
+
72
+ // Race condition fix: Use existing fadeState originalVolume if already transitioning,
73
+ // otherwise capture current volume
74
+ let originalVolume: number;
75
+ if (channel.fadeState?.isTransitioning) {
76
+ // We're already in any kind of transition (pause or resume), preserve original volume
77
+ originalVolume = channel.fadeState.originalVolume;
78
+ } else {
79
+ // First fade or no transition in progress, capture current volume
80
+ // But ensure we don't capture a volume of 0 during a transition
81
+ const currentVolume = getChannelVolumeSync(channelNumber);
82
+ originalVolume = currentVolume > 0 ? currentVolume : (channel.fadeState?.originalVolume ?? 1.0);
83
+ }
84
+
85
+ // Store fade state for resumeWithFade to use (including custom duration)
71
86
  channel.fadeState = {
72
- originalVolume,
87
+ customDuration: duration,
73
88
  fadeType,
74
- isPaused: true
89
+ isPaused: true,
90
+ isTransitioning: true,
91
+ originalVolume
75
92
  };
76
-
77
- if (config.duration === 0) {
93
+
94
+ if (effectiveDuration === 0) {
78
95
  // Instant pause
79
96
  await pauseChannel(channelNumber);
97
+ // Reset volume to original for resume (synchronously to avoid state issues)
98
+ setChannelVolumeSync(channelNumber, originalVolume);
99
+ // Mark transition as complete for instant pause
100
+ if (channel.fadeState) {
101
+ channel.fadeState.isTransitioning = false;
102
+ }
80
103
  return;
81
104
  }
82
-
105
+
83
106
  // Fade to 0 with pause curve, then pause
84
- await transitionVolume(channelNumber, 0, config.duration, config.pauseCurve);
107
+ await transitionVolume(channelNumber, 0, effectiveDuration, config.pauseCurve);
85
108
  await pauseChannel(channelNumber);
86
-
109
+
87
110
  // Reset volume to original for resume (synchronously to avoid state issues)
88
111
  setChannelVolumeSync(channelNumber, originalVolume);
112
+
113
+ // Mark transition as complete
114
+ if (channel.fadeState) {
115
+ channel.fadeState.isTransitioning = false;
116
+ }
89
117
  };
90
118
 
91
119
  /**
@@ -93,104 +121,146 @@ export const pauseWithFade = async (fadeType: FadeType = FadeType.Gentle, channe
93
121
  * Uses the complementary fade curve automatically based on the pause fade type, or allows override
94
122
  * @param fadeType - Optional fade type to override the stored fade type from pause
95
123
  * @param channelNumber - The channel number to resume (defaults to 0)
124
+ * @param duration - Optional custom fade duration in milliseconds (uses stored or fadeType default if not provided)
96
125
  * @returns Promise that resolves when the resume and fade are complete
97
126
  * @example
98
127
  * ```typescript
99
128
  * await resumeWithFade(); // Resume with automatically paired fade curve from pause
100
129
  * await resumeWithFade(FadeType.Dramatic, 0); // Override with dramatic fade
101
- * await resumeWithFade(FadeType.Linear); // Override with linear fade on default channel
130
+ * await resumeWithFade(FadeType.Linear, 0, 1000); // Override with linear fade over 1 second
102
131
  * ```
103
132
  */
104
- export const resumeWithFade = async (fadeType?: FadeType, channelNumber: number = 0): Promise<void> => {
133
+ export const resumeWithFade = async (
134
+ fadeType?: FadeType,
135
+ channelNumber: number = 0,
136
+ duration?: number
137
+ ): Promise<void> => {
105
138
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
106
-
139
+
107
140
  if (!channel || channel.queue.length === 0) return;
108
-
141
+
109
142
  const fadeState: ChannelFadeState | undefined = channel.fadeState;
110
- if (!fadeState || !fadeState.isPaused) {
143
+ if (!fadeState?.isPaused) {
111
144
  // Fall back to regular resume if no fade state
112
145
  await resumeChannel(channelNumber);
113
146
  return;
114
147
  }
115
-
148
+
116
149
  // Use provided fadeType or fall back to stored fadeType from pause
117
- const effectiveFadeType: FadeType = fadeType || fadeState.fadeType;
118
- const config: FadeConfig = FADE_CONFIGS[effectiveFadeType];
119
-
120
- if (config.duration === 0) {
150
+ const effectiveFadeType: FadeType = fadeType ?? fadeState.fadeType;
151
+ const config: FadeConfig = getFadeConfig(effectiveFadeType);
152
+
153
+ // Determine effective duration: custom parameter > stored custom > fadeType default
154
+ let effectiveDuration: number;
155
+ if (duration !== undefined) {
156
+ effectiveDuration = duration;
157
+ } else if (fadeState.customDuration !== undefined) {
158
+ effectiveDuration = fadeState.customDuration;
159
+ } else {
160
+ effectiveDuration = config.duration;
161
+ }
162
+
163
+ if (effectiveDuration === 0) {
121
164
  // Instant resume
165
+ const targetVolume = fadeState.originalVolume > 0 ? fadeState.originalVolume : 1.0;
166
+ setChannelVolumeSync(channelNumber, targetVolume);
122
167
  await resumeChannel(channelNumber);
123
168
  fadeState.isPaused = false;
169
+ fadeState.isTransitioning = false;
124
170
  return;
125
171
  }
126
-
172
+
173
+ // Race condition fix: Ensure we have a valid original volume to restore to
174
+ const targetVolume = fadeState.originalVolume > 0 ? fadeState.originalVolume : 1.0;
175
+
176
+ // Mark as transitioning to prevent volume capture during rapid toggles
177
+ fadeState.isTransitioning = true;
178
+
127
179
  // Set volume to 0, resume, then fade to original with resume curve
128
180
  setChannelVolumeSync(channelNumber, 0);
129
181
  await resumeChannel(channelNumber);
130
- await transitionVolume(channelNumber, fadeState.originalVolume, config.duration, config.resumeCurve);
131
-
182
+
183
+ // Use the stored original volume, not current volume, to prevent race conditions
184
+ await transitionVolume(channelNumber, targetVolume, effectiveDuration, config.resumeCurve);
185
+
132
186
  fadeState.isPaused = false;
187
+ fadeState.isTransitioning = false;
133
188
  };
134
189
 
135
190
  /**
136
191
  * Toggles pause/resume state for a specific channel with integrated fade
137
192
  * @param fadeType - Type of fade transition to apply when pausing
138
193
  * @param channelNumber - The channel number to toggle (defaults to 0)
194
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
139
195
  * @returns Promise that resolves when the toggle and fade are complete
140
196
  * @example
141
197
  * ```typescript
142
198
  * await togglePauseWithFade(FadeType.Gentle, 0); // Toggle with gentle fade
199
+ * await togglePauseWithFade(FadeType.Dramatic, 0, 500); // Toggle with custom 500ms fade
143
200
  * ```
144
201
  */
145
- export const togglePauseWithFade = async (fadeType: FadeType = FadeType.Gentle, channelNumber: number = 0): Promise<void> => {
202
+ export const togglePauseWithFade = async (
203
+ fadeType: FadeType = FadeType.Gentle,
204
+ channelNumber: number = 0,
205
+ duration?: number
206
+ ): Promise<void> => {
146
207
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
147
-
208
+
148
209
  if (!channel || channel.queue.length === 0) return;
149
-
210
+
150
211
  const currentAudio: HTMLAudioElement = channel.queue[0];
151
-
212
+
152
213
  if (currentAudio.paused) {
153
- await resumeWithFade(undefined, channelNumber);
214
+ await resumeWithFade(undefined, channelNumber, duration);
154
215
  } else {
155
- await pauseWithFade(fadeType, channelNumber);
216
+ await pauseWithFade(fadeType, channelNumber, duration);
156
217
  }
157
218
  };
158
219
 
159
220
  /**
160
221
  * Pauses all currently playing audio across all channels with smooth volume fade
161
222
  * @param fadeType - Type of fade transition to apply to all channels
223
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
162
224
  * @returns Promise that resolves when all channels are paused and faded
163
225
  * @example
164
226
  * ```typescript
165
- * await pauseAllWithFade('dramatic'); // Pause everything with dramatic fade
227
+ * await pauseAllWithFade(FadeType.Dramatic); // Pause everything with dramatic fade
228
+ * await pauseAllWithFade(FadeType.Gentle, 1200); // Pause all channels with custom 1.2s fade
166
229
  * ```
167
230
  */
168
- export const pauseAllWithFade = async (fadeType: FadeType = FadeType.Gentle): Promise<void> => {
231
+ export const pauseAllWithFade = async (
232
+ fadeType: FadeType = FadeType.Gentle,
233
+ duration?: number
234
+ ): Promise<void> => {
169
235
  const pausePromises: Promise<void>[] = [];
170
-
236
+
171
237
  audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
172
- pausePromises.push(pauseWithFade(fadeType, index));
238
+ pausePromises.push(pauseWithFade(fadeType, index, duration));
173
239
  });
174
-
240
+
175
241
  await Promise.all(pausePromises);
176
242
  };
177
243
 
178
244
  /**
179
245
  * Resumes all currently paused audio across all channels with smooth volume fade
180
- * Uses automatically paired fade curves based on each channel's pause fade type
246
+ * Uses automatically paired fade curves based on each channel's pause fade type, or allows override
247
+ * @param fadeType - Optional fade type to override stored fade types for all channels
248
+ * @param duration - Optional custom fade duration in milliseconds (uses stored or fadeType default if not provided)
181
249
  * @returns Promise that resolves when all channels are resumed and faded
182
250
  * @example
183
251
  * ```typescript
184
252
  * await resumeAllWithFade(); // Resume everything with paired fade curves
253
+ * await resumeAllWithFade(FadeType.Gentle, 800); // Override all channels with gentle fade over 800ms
254
+ * await resumeAllWithFade(undefined, 600); // Use stored fade types with custom 600ms duration
185
255
  * ```
186
256
  */
187
- export const resumeAllWithFade = async (): Promise<void> => {
257
+ export const resumeAllWithFade = async (fadeType?: FadeType, duration?: number): Promise<void> => {
188
258
  const resumePromises: Promise<void>[] = [];
189
-
259
+
190
260
  audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
191
- resumePromises.push(resumeWithFade(undefined, index));
261
+ resumePromises.push(resumeWithFade(fadeType, index, duration));
192
262
  });
193
-
263
+
194
264
  await Promise.all(resumePromises);
195
265
  };
196
266
 
@@ -199,17 +269,22 @@ export const resumeAllWithFade = async (): Promise<void> => {
199
269
  * If any channels are playing, all will be paused with fade
200
270
  * If all channels are paused, all will be resumed with fade
201
271
  * @param fadeType - Type of fade transition to apply when pausing
272
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
202
273
  * @returns Promise that resolves when all toggles and fades are complete
203
274
  * @example
204
275
  * ```typescript
205
- * await togglePauseAllWithFade('gentle'); // Global toggle with gentle fade
276
+ * await togglePauseAllWithFade(FadeType.Gentle); // Global toggle with gentle fade
277
+ * await togglePauseAllWithFade(FadeType.Dramatic, 600); // Global toggle with custom 600ms fade
206
278
  * ```
207
279
  */
208
- export const togglePauseAllWithFade = async (fadeType: FadeType = FadeType.Gentle): Promise<void> => {
280
+ export const togglePauseAllWithFade = async (
281
+ fadeType: FadeType = FadeType.Gentle,
282
+ duration?: number
283
+ ): Promise<void> => {
209
284
  let hasPlayingChannel: boolean = false;
210
-
285
+
211
286
  // Check if any channel is currently playing
212
- for (let i = 0; i < audioChannels.length; i++) {
287
+ for (let i: number = 0; i < audioChannels.length; i++) {
213
288
  const channel: ExtendedAudioQueueChannel = audioChannels[i];
214
289
  if (channel && channel.queue.length > 0) {
215
290
  const currentAudio: HTMLAudioElement = channel.queue[0];
@@ -219,13 +294,13 @@ export const togglePauseAllWithFade = async (fadeType: FadeType = FadeType.Gentl
219
294
  }
220
295
  }
221
296
  }
222
-
297
+
223
298
  // If any channel is playing, pause all with fade
224
299
  // If no channels are playing, resume all with fade
225
300
  if (hasPlayingChannel) {
226
- await pauseAllWithFade(fadeType);
301
+ await pauseAllWithFade(fadeType, duration);
227
302
  } else {
228
- await resumeAllWithFade();
303
+ await resumeAllWithFade(fadeType, duration);
229
304
  }
230
305
  };
231
306
 
@@ -241,15 +316,19 @@ export const togglePauseAllWithFade = async (fadeType: FadeType = FadeType.Gentl
241
316
  */
242
317
  export const pauseChannel = async (channelNumber: number = 0): Promise<void> => {
243
318
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
244
-
319
+
245
320
  if (channel && channel.queue.length > 0) {
246
321
  const currentAudio: HTMLAudioElement = channel.queue[0];
247
-
322
+
248
323
  if (!currentAudio.paused && !currentAudio.ended) {
249
324
  currentAudio.pause();
250
325
  channel.isPaused = true;
251
-
252
- const audioInfo: AudioInfo | null = getAudioInfoFromElement(currentAudio, channelNumber, audioChannels);
326
+
327
+ const audioInfo: AudioInfo | null = getAudioInfoFromElement(
328
+ currentAudio,
329
+ channelNumber,
330
+ audioChannels
331
+ );
253
332
  if (audioInfo) {
254
333
  emitAudioPause(channelNumber, audioInfo, audioChannels);
255
334
  }
@@ -269,16 +348,20 @@ export const pauseChannel = async (channelNumber: number = 0): Promise<void> =>
269
348
  */
270
349
  export const resumeChannel = async (channelNumber: number = 0): Promise<void> => {
271
350
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
272
-
351
+
273
352
  if (channel && channel.queue.length > 0) {
274
353
  const currentAudio: HTMLAudioElement = channel.queue[0];
275
-
354
+
276
355
  // Only resume if both the channel is marked as paused AND the audio element is actually paused AND not ended
277
356
  if (channel.isPaused && currentAudio.paused && !currentAudio.ended) {
278
357
  await currentAudio.play();
279
358
  channel.isPaused = false;
280
-
281
- const audioInfo: AudioInfo | null = getAudioInfoFromElement(currentAudio, channelNumber, audioChannels);
359
+
360
+ const audioInfo: AudioInfo | null = getAudioInfoFromElement(
361
+ currentAudio,
362
+ channelNumber,
363
+ audioChannels
364
+ );
282
365
  if (audioInfo) {
283
366
  emitAudioResume(channelNumber, audioInfo, audioChannels);
284
367
  }
@@ -297,10 +380,10 @@ export const resumeChannel = async (channelNumber: number = 0): Promise<void> =>
297
380
  */
298
381
  export const togglePauseChannel = async (channelNumber: number = 0): Promise<void> => {
299
382
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
300
-
383
+
301
384
  if (channel && channel.queue.length > 0) {
302
385
  const currentAudio: HTMLAudioElement = channel.queue[0];
303
-
386
+
304
387
  if (currentAudio.paused) {
305
388
  await resumeChannel(channelNumber);
306
389
  } else {
@@ -319,11 +402,11 @@ export const togglePauseChannel = async (channelNumber: number = 0): Promise<voi
319
402
  */
320
403
  export const pauseAllChannels = async (): Promise<void> => {
321
404
  const pausePromises: Promise<void>[] = [];
322
-
405
+
323
406
  audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
324
407
  pausePromises.push(pauseChannel(index));
325
408
  });
326
-
409
+
327
410
  await Promise.all(pausePromises);
328
411
  };
329
412
 
@@ -337,11 +420,11 @@ export const pauseAllChannels = async (): Promise<void> => {
337
420
  */
338
421
  export const resumeAllChannels = async (): Promise<void> => {
339
422
  const resumePromises: Promise<void>[] = [];
340
-
423
+
341
424
  audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
342
425
  resumePromises.push(resumeChannel(index));
343
426
  });
344
-
427
+
345
428
  await Promise.all(resumePromises);
346
429
  };
347
430
 
@@ -357,7 +440,7 @@ export const resumeAllChannels = async (): Promise<void> => {
357
440
  */
358
441
  export const isChannelPaused = (channelNumber: number = 0): boolean => {
359
442
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
360
- return channel?.isPaused || false;
443
+ return channel?.isPaused ?? false;
361
444
  };
362
445
 
363
446
  /**
@@ -372,9 +455,7 @@ export const isChannelPaused = (channelNumber: number = 0): boolean => {
372
455
  * ```
373
456
  */
374
457
  export const getAllChannelsPauseState = (): boolean[] => {
375
- return audioChannels.map((channel: ExtendedAudioQueueChannel) =>
376
- channel?.isPaused || false
377
- );
458
+ return audioChannels.map((channel: ExtendedAudioQueueChannel) => channel?.isPaused ?? false);
378
459
  };
379
460
 
380
461
  /**
@@ -389,9 +470,9 @@ export const getAllChannelsPauseState = (): boolean[] => {
389
470
  */
390
471
  export const togglePauseAllChannels = async (): Promise<void> => {
391
472
  let hasPlayingChannel: boolean = false;
392
-
473
+
393
474
  // Check if any channel is currently playing
394
- for (let i = 0; i < audioChannels.length; i++) {
475
+ for (let i: number = 0; i < audioChannels.length; i++) {
395
476
  const channel: ExtendedAudioQueueChannel = audioChannels[i];
396
477
  if (channel && channel.queue.length > 0) {
397
478
  const currentAudio: HTMLAudioElement = channel.queue[0];
@@ -401,7 +482,7 @@ export const togglePauseAllChannels = async (): Promise<void> => {
401
482
  }
402
483
  }
403
484
  }
404
-
485
+
405
486
  // If any channel is playing, pause all channels
406
487
  // If no channels are playing, resume all channels
407
488
  if (hasPlayingChannel) {
@@ -409,4 +490,4 @@ export const togglePauseAllChannels = async (): Promise<void> => {
409
490
  } else {
410
491
  await resumeAllChannels();
411
492
  }
412
- };
493
+ };