audioq 2.0.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 ADDED
@@ -0,0 +1,523 @@
1
+ /**
2
+ * @fileoverview Pause and resume management functions for the audioq package
3
+ */
4
+
5
+ import {
6
+ ExtendedAudioQueueChannel,
7
+ AudioInfo,
8
+ FadeType,
9
+ FadeConfig,
10
+ ChannelFadeState
11
+ } from './types';
12
+ import { audioChannels } from './info';
13
+ import { getAudioInfoFromElement } from './utils';
14
+ import { emitAudioPause, emitAudioResume } from './events';
15
+ import { transitionVolume, getFadeConfig } from './volume';
16
+ import { setWebAudioVolume } from './web-audio';
17
+
18
+ /**
19
+ * Gets the current volume for a channel, accounting for synchronous state
20
+ * @param channelNumber - The channel number
21
+ * @returns Current volume level (0-1)
22
+ */
23
+ const getChannelVolumeSync = (channelNumber: number): number => {
24
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
25
+ return channel?.volume ?? 1.0;
26
+ };
27
+
28
+ /**
29
+ * Sets the channel volume synchronously in internal state
30
+ * @param channelNumber - The channel number
31
+ * @param volume - Volume level (0-1)
32
+ */
33
+ const setChannelVolumeSync = (channelNumber: number, volume: number): void => {
34
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
35
+ if (channel) {
36
+ channel.volume = volume;
37
+ if (channel.queue.length > 0) {
38
+ const audio = channel.queue[0];
39
+ if (channel.webAudioNodes) {
40
+ const nodes = channel.webAudioNodes.get(audio);
41
+ if (nodes) {
42
+ // Update the gain node when Web Audio is active
43
+ setWebAudioVolume(nodes.gainNode, volume);
44
+ }
45
+ }
46
+ audio.volume = volume;
47
+ }
48
+ }
49
+ };
50
+
51
+ /**
52
+ * Pauses the currently playing audio in a specific channel with smooth volume fade
53
+ * @param fadeType - Type of fade transition to apply
54
+ * @param channelNumber - The channel number to pause (defaults to 0)
55
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
56
+ * @returns Promise that resolves when the pause and fade are complete
57
+ * @example
58
+ * ```typescript
59
+ * await pauseWithFade(FadeType.Gentle, 0); // Pause with gentle fade out over 800ms
60
+ * await pauseWithFade(FadeType.Dramatic, 1, 1500); // Pause with dramatic fade out over 1.5s
61
+ * await pauseWithFade(FadeType.Linear, 2, 500); // Linear pause with custom 500ms fade
62
+ * ```
63
+ */
64
+ export const pauseWithFade = async (
65
+ fadeType: FadeType = FadeType.Gentle,
66
+ channelNumber: number = 0,
67
+ duration?: number
68
+ ): Promise<void> => {
69
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
70
+
71
+ if (!channel || channel.queue.length === 0) {
72
+ return;
73
+ }
74
+
75
+ const currentAudio: HTMLAudioElement = channel.queue[0];
76
+
77
+ // Don't pause if already paused or ended
78
+ if (currentAudio.paused || currentAudio.ended) {
79
+ return;
80
+ }
81
+
82
+ const config: FadeConfig = getFadeConfig(fadeType);
83
+ const effectiveDuration: number = duration ?? config.duration;
84
+
85
+ // Race condition fix: Use existing fadeState originalVolume if already transitioning,
86
+ // otherwise capture current volume
87
+ let originalVolume: number;
88
+ if (channel.fadeState?.isTransitioning) {
89
+ // We're already in any kind of transition (pause or resume), preserve original volume
90
+ originalVolume = channel.fadeState.originalVolume;
91
+ } else {
92
+ // First fade or no transition in progress, capture current volume
93
+ // But ensure we don't capture a volume of 0 during a transition
94
+ const currentVolume = getChannelVolumeSync(channelNumber);
95
+ originalVolume = currentVolume > 0 ? currentVolume : (channel.fadeState?.originalVolume ?? 1.0);
96
+ }
97
+
98
+ // Store fade state for resumeWithFade to use (including custom duration)
99
+ channel.fadeState = {
100
+ customDuration: duration,
101
+ fadeType,
102
+ isPaused: true,
103
+ isTransitioning: true,
104
+ originalVolume
105
+ };
106
+
107
+ if (effectiveDuration === 0) {
108
+ // Instant pause
109
+ await pauseChannel(channelNumber);
110
+ // Reset volume to original for resume (synchronously to avoid state issues)
111
+ setChannelVolumeSync(channelNumber, originalVolume);
112
+ // Mark transition as complete for instant pause
113
+ if (channel.fadeState) {
114
+ channel.fadeState.isTransitioning = false;
115
+ }
116
+ return;
117
+ }
118
+
119
+ // Fade to 0 with pause curve, then pause
120
+ await transitionVolume(channelNumber, 0, effectiveDuration, config.pauseCurve);
121
+ // Pause the audio
122
+ await pauseChannel(channelNumber);
123
+
124
+ // Restore channel.volume for resume, but DON'T restore gain node to prevent blip
125
+ // The gain node will be restored during the resume fade
126
+ channel.volume = originalVolume;
127
+
128
+ // Mark transition as complete
129
+ if (channel.fadeState) {
130
+ channel.fadeState.isTransitioning = false;
131
+ }
132
+ };
133
+
134
+ /**
135
+ * Resumes the currently paused audio in a specific channel with smooth volume fade
136
+ * Uses the complementary fade curve automatically based on the pause fade type, or allows override
137
+ * @param fadeType - Optional fade type to override the stored fade type from pause
138
+ * @param channelNumber - The channel number to resume (defaults to 0)
139
+ * @param duration - Optional custom fade duration in milliseconds (uses stored or fadeType default if not provided)
140
+ * @returns Promise that resolves when the resume and fade are complete
141
+ * @example
142
+ * ```typescript
143
+ * await resumeWithFade(); // Resume with automatically paired fade curve from pause
144
+ * await resumeWithFade(FadeType.Dramatic, 0); // Override with dramatic fade
145
+ * await resumeWithFade(FadeType.Linear, 0, 1000); // Override with linear fade over 1 second
146
+ * ```
147
+ */
148
+ export const resumeWithFade = async (
149
+ fadeType?: FadeType,
150
+ channelNumber: number = 0,
151
+ duration?: number
152
+ ): Promise<void> => {
153
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
154
+
155
+ if (!channel || channel.queue.length === 0) {
156
+ return;
157
+ }
158
+ const audio = channel.queue[0];
159
+ const fadeState: ChannelFadeState | undefined = channel.fadeState;
160
+
161
+ if (!fadeState?.isPaused) {
162
+ // Fall back to regular resume if no fade state
163
+ await resumeChannel(channelNumber);
164
+ return;
165
+ }
166
+
167
+ // Use provided fadeType or fall back to stored fadeType from pause
168
+ const effectiveFadeType: FadeType = fadeType ?? fadeState.fadeType;
169
+ const config: FadeConfig = getFadeConfig(effectiveFadeType);
170
+
171
+ // Determine effective duration: custom parameter > stored custom > fadeType default
172
+ let effectiveDuration: number;
173
+ if (duration !== undefined) {
174
+ effectiveDuration = duration;
175
+ } else if (fadeState.customDuration !== undefined) {
176
+ effectiveDuration = fadeState.customDuration;
177
+ } else {
178
+ effectiveDuration = config.duration;
179
+ }
180
+
181
+ if (effectiveDuration === 0) {
182
+ // Instant resume
183
+ const targetVolume = fadeState.originalVolume > 0 ? fadeState.originalVolume : 1.0;
184
+
185
+ setChannelVolumeSync(channelNumber, targetVolume);
186
+ await resumeChannel(channelNumber);
187
+
188
+ fadeState.isPaused = false;
189
+ fadeState.isTransitioning = false;
190
+ return;
191
+ }
192
+
193
+ // Race condition fix: Ensure we have a valid original volume to restore to
194
+ const targetVolume = fadeState.originalVolume > 0 ? fadeState.originalVolume : 1.0;
195
+
196
+ // Mark as transitioning to prevent volume capture during rapid toggles
197
+ fadeState.isTransitioning = true;
198
+
199
+ // Ensure gain node is at 0 before resuming (should already be from pause)
200
+ // Don't touch audio.volume when Web Audio is active - iOS may reset it
201
+ // Don't touch channel.volume - it should stay at originalVolume
202
+ if (channel.webAudioNodes) {
203
+ const nodes = channel.webAudioNodes.get(audio);
204
+ if (nodes) {
205
+ setWebAudioVolume(nodes.gainNode, 0);
206
+ }
207
+ } else {
208
+ // Fallback for non-Web Audio: set audio.volume directly
209
+ audio.volume = 0;
210
+ }
211
+
212
+ await resumeChannel(channelNumber);
213
+
214
+ // Use the stored original volume, not current volume, to prevent race conditions
215
+ await transitionVolume(channelNumber, targetVolume, effectiveDuration, config.resumeCurve);
216
+ fadeState.isPaused = false;
217
+ fadeState.isTransitioning = false;
218
+ };
219
+
220
+ /**
221
+ * Toggles pause/resume state for a specific channel with integrated fade
222
+ * @param fadeType - Type of fade transition to apply when pausing
223
+ * @param channelNumber - The channel number to toggle (defaults to 0)
224
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
225
+ * @returns Promise that resolves when the toggle and fade are complete
226
+ * @example
227
+ * ```typescript
228
+ * await togglePauseWithFade(FadeType.Gentle, 0); // Toggle with gentle fade
229
+ * await togglePauseWithFade(FadeType.Dramatic, 0, 500); // Toggle with custom 500ms fade
230
+ * ```
231
+ */
232
+ export const togglePauseWithFade = async (
233
+ fadeType: FadeType = FadeType.Gentle,
234
+ channelNumber: number = 0,
235
+ duration?: number
236
+ ): Promise<void> => {
237
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
238
+
239
+ if (!channel || channel.queue.length === 0) return;
240
+
241
+ const currentAudio: HTMLAudioElement = channel.queue[0];
242
+
243
+ if (currentAudio.paused) {
244
+ await resumeWithFade(undefined, channelNumber, duration);
245
+ } else {
246
+ await pauseWithFade(fadeType, channelNumber, duration);
247
+ }
248
+ };
249
+
250
+ /**
251
+ * Pauses all currently playing audio across all channels with smooth volume fade
252
+ * @param fadeType - Type of fade transition to apply to all channels
253
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
254
+ * @returns Promise that resolves when all channels are paused and faded
255
+ * @example
256
+ * ```typescript
257
+ * await pauseAllWithFade(FadeType.Dramatic); // Pause everything with dramatic fade
258
+ * await pauseAllWithFade(FadeType.Gentle, 1200); // Pause all channels with custom 1.2s fade
259
+ * ```
260
+ */
261
+ export const pauseAllWithFade = async (
262
+ fadeType: FadeType = FadeType.Gentle,
263
+ duration?: number
264
+ ): Promise<void> => {
265
+ const pausePromises: Promise<void>[] = [];
266
+
267
+ audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
268
+ pausePromises.push(pauseWithFade(fadeType, index, duration));
269
+ });
270
+
271
+ await Promise.all(pausePromises);
272
+ };
273
+
274
+ /**
275
+ * Resumes all currently paused audio across all channels with smooth volume fade
276
+ * Uses automatically paired fade curves based on each channel's pause fade type, or allows override
277
+ * @param fadeType - Optional fade type to override stored fade types for all channels
278
+ * @param duration - Optional custom fade duration in milliseconds (uses stored or fadeType default if not provided)
279
+ * @returns Promise that resolves when all channels are resumed and faded
280
+ * @example
281
+ * ```typescript
282
+ * await resumeAllWithFade(); // Resume everything with paired fade curves
283
+ * await resumeAllWithFade(FadeType.Gentle, 800); // Override all channels with gentle fade over 800ms
284
+ * await resumeAllWithFade(undefined, 600); // Use stored fade types with custom 600ms duration
285
+ * ```
286
+ */
287
+ export const resumeAllWithFade = async (fadeType?: FadeType, duration?: number): Promise<void> => {
288
+ const resumePromises: Promise<void>[] = [];
289
+
290
+ audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
291
+ resumePromises.push(resumeWithFade(fadeType, index, duration));
292
+ });
293
+
294
+ await Promise.all(resumePromises);
295
+ };
296
+
297
+ /**
298
+ * Toggles pause/resume state for all channels with integrated fade
299
+ * If any channels are playing, all will be paused with fade
300
+ * If all channels are paused, all will be resumed with fade
301
+ * @param fadeType - Type of fade transition to apply when pausing
302
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
303
+ * @returns Promise that resolves when all toggles and fades are complete
304
+ * @example
305
+ * ```typescript
306
+ * await togglePauseAllWithFade(FadeType.Gentle); // Global toggle with gentle fade
307
+ * await togglePauseAllWithFade(FadeType.Dramatic, 600); // Global toggle with custom 600ms fade
308
+ * ```
309
+ */
310
+ export const togglePauseAllWithFade = async (
311
+ fadeType: FadeType = FadeType.Gentle,
312
+ duration?: number
313
+ ): Promise<void> => {
314
+ let hasPlayingChannel: boolean = false;
315
+
316
+ // Check if any channel is currently playing
317
+ for (let i: number = 0; i < audioChannels.length; i++) {
318
+ const channel: ExtendedAudioQueueChannel = audioChannels[i];
319
+ if (channel && channel.queue.length > 0) {
320
+ const currentAudio: HTMLAudioElement = channel.queue[0];
321
+ if (!currentAudio.paused && !currentAudio.ended) {
322
+ hasPlayingChannel = true;
323
+ break;
324
+ }
325
+ }
326
+ }
327
+
328
+ // If any channel is playing, pause all with fade
329
+ // If no channels are playing, resume all with fade
330
+ if (hasPlayingChannel) {
331
+ await pauseAllWithFade(fadeType, duration);
332
+ } else {
333
+ await resumeAllWithFade(fadeType, duration);
334
+ }
335
+ };
336
+
337
+ /**
338
+ * Pauses the currently playing audio in a specific channel
339
+ * @param channelNumber - The channel number to pause (defaults to 0)
340
+ * @returns Promise that resolves when the audio is paused
341
+ * @example
342
+ * ```typescript
343
+ * await pauseChannel(0); // Pause audio in channel 0
344
+ * await pauseChannel(); // Pause audio in default channel
345
+ * ```
346
+ */
347
+ export const pauseChannel = async (channelNumber: number = 0): Promise<void> => {
348
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
349
+
350
+ if (channel && channel.queue.length > 0) {
351
+ const currentAudio: HTMLAudioElement = channel.queue[0];
352
+
353
+ if (!currentAudio.paused && !currentAudio.ended) {
354
+ currentAudio.pause();
355
+ channel.isPaused = true;
356
+
357
+ const audioInfo: AudioInfo | null = getAudioInfoFromElement(
358
+ currentAudio,
359
+ channelNumber,
360
+ audioChannels
361
+ );
362
+ if (audioInfo) {
363
+ emitAudioPause(channelNumber, audioInfo, audioChannels);
364
+ }
365
+ }
366
+ }
367
+ };
368
+
369
+ /**
370
+ * Resumes the currently paused audio in a specific channel
371
+ * @param channelNumber - The channel number to resume (defaults to 0)
372
+ * @returns Promise that resolves when the audio starts playing
373
+ * @example
374
+ * ```typescript
375
+ * await resumeChannel(0); // Resume audio in channel 0
376
+ * await resumeChannel(); // Resume audio in default channel
377
+ * ```
378
+ */
379
+ export const resumeChannel = async (channelNumber: number = 0): Promise<void> => {
380
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
381
+
382
+ if (channel && channel.queue.length > 0) {
383
+ const currentAudio: HTMLAudioElement = channel.queue[0];
384
+
385
+ // Only resume if both the channel is marked as paused AND the audio element is actually paused AND not ended
386
+ if (channel.isPaused && currentAudio.paused && !currentAudio.ended) {
387
+ await currentAudio.play();
388
+ channel.isPaused = false;
389
+
390
+ const audioInfo: AudioInfo | null = getAudioInfoFromElement(
391
+ currentAudio,
392
+ channelNumber,
393
+ audioChannels
394
+ );
395
+ if (audioInfo) {
396
+ emitAudioResume(channelNumber, audioInfo, audioChannels);
397
+ }
398
+ }
399
+ }
400
+ };
401
+
402
+ /**
403
+ * Toggles pause/resume state for a specific channel
404
+ * @param channelNumber - The channel number to toggle (defaults to 0)
405
+ * @returns Promise that resolves when the toggle is complete
406
+ * @example
407
+ * ```typescript
408
+ * await togglePauseChannel(0); // Toggle pause state for channel 0
409
+ * ```
410
+ */
411
+ export const togglePauseChannel = async (channelNumber: number = 0): Promise<void> => {
412
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
413
+
414
+ if (channel && channel.queue.length > 0) {
415
+ const currentAudio: HTMLAudioElement = channel.queue[0];
416
+
417
+ if (currentAudio.paused) {
418
+ await resumeChannel(channelNumber);
419
+ } else {
420
+ await pauseChannel(channelNumber);
421
+ }
422
+ }
423
+ };
424
+
425
+ /**
426
+ * Pauses all currently playing audio across all channels
427
+ * @returns Promise that resolves when all audio is paused
428
+ * @example
429
+ * ```typescript
430
+ * await pauseAllChannels(); // Pause everything
431
+ * ```
432
+ */
433
+ export const pauseAllChannels = async (): Promise<void> => {
434
+ const pausePromises: Promise<void>[] = [];
435
+
436
+ audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
437
+ pausePromises.push(pauseChannel(index));
438
+ });
439
+
440
+ await Promise.all(pausePromises);
441
+ };
442
+
443
+ /**
444
+ * Resumes all currently paused audio across all channels
445
+ * @returns Promise that resolves when all audio is resumed
446
+ * @example
447
+ * ```typescript
448
+ * await resumeAllChannels(); // Resume everything that was paused
449
+ * ```
450
+ */
451
+ export const resumeAllChannels = async (): Promise<void> => {
452
+ const resumePromises: Promise<void>[] = [];
453
+
454
+ audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
455
+ resumePromises.push(resumeChannel(index));
456
+ });
457
+
458
+ await Promise.all(resumePromises);
459
+ };
460
+
461
+ /**
462
+ * Checks if a specific channel is currently paused
463
+ * @param channelNumber - The channel number to check (defaults to 0)
464
+ * @returns True if the channel is paused, false otherwise
465
+ * @example
466
+ * ```typescript
467
+ * const isPaused = isChannelPaused(0);
468
+ * console.log(`Channel 0 is ${isPaused ? 'paused' : 'playing'}`);
469
+ * ```
470
+ */
471
+ export const isChannelPaused = (channelNumber: number = 0): boolean => {
472
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
473
+ return channel?.isPaused ?? false;
474
+ };
475
+
476
+ /**
477
+ * Gets the pause state of all channels
478
+ * @returns Array of boolean values indicating pause state for each channel
479
+ * @example
480
+ * ```typescript
481
+ * const pauseStates = getAllChannelsPauseState();
482
+ * pauseStates.forEach((isPaused, index) => {
483
+ * console.log(`Channel ${index}: ${isPaused ? 'paused' : 'playing'}`);
484
+ * });
485
+ * ```
486
+ */
487
+ export const getAllChannelsPauseState = (): boolean[] => {
488
+ return audioChannels.map((channel: ExtendedAudioQueueChannel) => channel?.isPaused ?? false);
489
+ };
490
+
491
+ /**
492
+ * Toggles pause/resume state for all channels globally
493
+ * If any channels are currently playing, all channels will be paused
494
+ * If all channels are paused, all channels will be resumed
495
+ * @returns Promise that resolves when the toggle is complete
496
+ * @example
497
+ * ```typescript
498
+ * await togglePauseAllChannels(); // Pause all if any are playing, resume all if all are paused
499
+ * ```
500
+ */
501
+ export const togglePauseAllChannels = async (): Promise<void> => {
502
+ let hasPlayingChannel: boolean = false;
503
+
504
+ // Check if any channel is currently playing
505
+ for (let i: number = 0; i < audioChannels.length; i++) {
506
+ const channel: ExtendedAudioQueueChannel = audioChannels[i];
507
+ if (channel && channel.queue.length > 0) {
508
+ const currentAudio: HTMLAudioElement = channel.queue[0];
509
+ if (!currentAudio.paused && !currentAudio.ended) {
510
+ hasPlayingChannel = true;
511
+ break;
512
+ }
513
+ }
514
+ }
515
+
516
+ // If any channel is playing, pause all channels
517
+ // If no channels are playing, resume all channels
518
+ if (hasPlayingChannel) {
519
+ await pauseAllChannels();
520
+ } else {
521
+ await resumeAllChannels();
522
+ }
523
+ };