audio-channel-queue 1.12.0 → 1.12.1-beta.2

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/volume.js CHANGED
@@ -12,13 +12,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  });
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.cancelAllVolumeTransitions = exports.cancelVolumeTransition = exports.restoreVolumeLevels = exports.applyVolumeDucking = exports.clearVolumeDucking = exports.setVolumeDucking = exports.setAllChannelsVolume = exports.getAllChannelsVolume = exports.getChannelVolume = exports.setChannelVolume = exports.transitionVolume = exports.getFadeConfig = void 0;
15
+ exports.cleanupWebAudioForAudio = exports.initializeWebAudioForAudio = exports.cancelAllVolumeTransitions = exports.cancelVolumeTransition = exports.restoreVolumeLevels = exports.applyVolumeDucking = exports.clearVolumeDucking = exports.setVolumeDucking = exports.getGlobalVolume = exports.setGlobalVolume = exports.setAllChannelsVolume = exports.getAllChannelsVolume = exports.getChannelVolume = exports.setChannelVolume = exports.transitionVolume = exports.getFadeConfig = void 0;
16
16
  const types_1 = require("./types");
17
17
  const info_1 = require("./info");
18
+ const web_audio_1 = require("./web-audio");
18
19
  // Store active volume transitions to handle interruptions
19
20
  const activeTransitions = new Map();
20
21
  // Track which timer type was used for each channel
21
22
  const timerTypes = new Map();
23
+ /**
24
+ * Global volume multiplier that affects all channels
25
+ * Acts as a global volume control (0-1)
26
+ */
27
+ let globalVolume = 1.0;
22
28
  /**
23
29
  * Global volume ducking configuration
24
30
  * Stores the volume ducking settings that apply to all channels
@@ -119,7 +125,7 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
119
125
  const startTime = performance.now();
120
126
  const easingFn = easingFunctions[easing];
121
127
  return new Promise((resolve) => {
122
- const updateVolume = () => {
128
+ const updateVolume = () => __awaiter(void 0, void 0, void 0, function* () {
123
129
  const elapsed = performance.now() - startTime;
124
130
  const progress = Math.min(elapsed / duration, 1);
125
131
  const easedProgress = easingFn(progress);
@@ -128,7 +134,7 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
128
134
  // Apply volume to both channel config and current audio
129
135
  channel.volume = clampedVolume;
130
136
  if (channel.queue.length > 0) {
131
- channel.queue[0].volume = clampedVolume;
137
+ yield setVolumeForAudio(channel.queue[0], clampedVolume, channelNumber);
132
138
  }
133
139
  if (progress >= 1) {
134
140
  // Transition complete
@@ -139,24 +145,25 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
139
145
  else {
140
146
  // Use requestAnimationFrame in browser, setTimeout in tests
141
147
  if (typeof requestAnimationFrame !== 'undefined') {
142
- const rafId = requestAnimationFrame(updateVolume);
148
+ const rafId = requestAnimationFrame(() => updateVolume());
143
149
  activeTransitions.set(channelNumber, rafId);
144
150
  timerTypes.set(channelNumber, types_1.TimerType.RequestAnimationFrame);
145
151
  }
146
152
  else {
147
153
  // In test environment, use shorter intervals
148
- const timeoutId = setTimeout(updateVolume, 1);
154
+ const timeoutId = setTimeout(() => updateVolume(), 1);
149
155
  activeTransitions.set(channelNumber, timeoutId);
150
156
  timerTypes.set(channelNumber, types_1.TimerType.Timeout);
151
157
  }
152
158
  }
153
- };
159
+ });
154
160
  updateVolume();
155
161
  });
156
162
  });
157
163
  exports.transitionVolume = transitionVolume;
158
164
  /**
159
165
  * Sets the volume for a specific channel with optional smooth transition
166
+ * Automatically uses Web Audio API on iOS devices for enhanced volume control
160
167
  * @param channelNumber - The channel number to set volume for
161
168
  * @param volume - Volume level (0-1)
162
169
  * @param transitionDuration - Optional transition duration in milliseconds
@@ -192,17 +199,21 @@ const setChannelVolume = (channelNumber, volume, transitionDuration, easing) =>
192
199
  };
193
200
  return;
194
201
  }
202
+ const channel = info_1.audioChannels[channelNumber];
203
+ // Initialize Web Audio API if needed and supported
204
+ if ((0, web_audio_1.shouldUseWebAudio)() && !channel.webAudioContext) {
205
+ yield initializeWebAudioForChannel(channelNumber);
206
+ }
195
207
  if (transitionDuration && transitionDuration > 0) {
196
208
  // Smooth transition
197
209
  yield (0, exports.transitionVolume)(channelNumber, clampedVolume, transitionDuration, easing);
198
210
  }
199
211
  else {
200
212
  // Instant change (backward compatibility)
201
- info_1.audioChannels[channelNumber].volume = clampedVolume;
202
- const channel = info_1.audioChannels[channelNumber];
213
+ channel.volume = clampedVolume;
203
214
  if (channel.queue.length > 0) {
204
215
  const currentAudio = channel.queue[0];
205
- currentAudio.volume = clampedVolume;
216
+ yield setVolumeForAudio(currentAudio, clampedVolume, channelNumber);
206
217
  }
207
218
  }
208
219
  });
@@ -255,6 +266,48 @@ const setAllChannelsVolume = (volume) => __awaiter(void 0, void 0, void 0, funct
255
266
  yield Promise.all(promises);
256
267
  });
257
268
  exports.setAllChannelsVolume = setAllChannelsVolume;
269
+ /**
270
+ * Sets the global volume multiplier that affects all channels
271
+ * This acts as a global volume control - individual channel volumes are multiplied by this value
272
+ * @param volume - Global volume level (0-1, will be clamped to this range)
273
+ * @example
274
+ * ```typescript
275
+ * // Set channel-specific volumes
276
+ * await setChannelVolume(0, 0.8); // SFX at 80%
277
+ * await setChannelVolume(1, 0.6); // Music at 60%
278
+ *
279
+ * // Apply global volume of 50% - all channels play at half their set volume
280
+ * await setGlobalVolume(0.5); // SFX now plays at 40%, music at 30%
281
+ * ```
282
+ */
283
+ const setGlobalVolume = (volume) => __awaiter(void 0, void 0, void 0, function* () {
284
+ // Clamp to valid range
285
+ globalVolume = Math.max(0, Math.min(1, volume));
286
+ // Update all currently playing audio to reflect the new global volume
287
+ // Note: setVolumeForAudio internally multiplies channel.volume by globalVolume
288
+ const updatePromises = [];
289
+ info_1.audioChannels.forEach((channel, channelNumber) => {
290
+ if (channel && channel.queue.length > 0) {
291
+ const currentAudio = channel.queue[0];
292
+ updatePromises.push(setVolumeForAudio(currentAudio, channel.volume, channelNumber));
293
+ }
294
+ });
295
+ yield Promise.all(updatePromises);
296
+ });
297
+ exports.setGlobalVolume = setGlobalVolume;
298
+ /**
299
+ * Gets the current global volume multiplier
300
+ * @returns Current global volume level (0-1), defaults to 1.0
301
+ * @example
302
+ * ```typescript
303
+ * const globalVol = getGlobalVolume();
304
+ * console.log(`Global volume is ${globalVol * 100}%`);
305
+ * ```
306
+ */
307
+ const getGlobalVolume = () => {
308
+ return globalVolume;
309
+ };
310
+ exports.getGlobalVolume = getGlobalVolume;
258
311
  /**
259
312
  * Configures volume ducking for channels. When the priority channel plays audio,
260
313
  * all other channels will be automatically reduced to the ducking volume level
@@ -333,13 +386,13 @@ const applyVolumeDucking = (activeChannelNumber) => __awaiter(void 0, void 0, vo
333
386
  // This is the priority channel - set to priority volume
334
387
  // Only change audio volume, preserve channel.volume as desired volume
335
388
  const currentAudio = channel.queue[0];
336
- transitionPromises.push(transitionAudioVolume(currentAudio, config.priorityVolume, duration, easing));
389
+ transitionPromises.push(transitionAudioVolume(currentAudio, config.priorityVolume, duration, easing, channelNumber));
337
390
  }
338
391
  else {
339
392
  // This is a background channel - duck it
340
393
  // Only change audio volume, preserve channel.volume as desired volume
341
394
  const currentAudio = channel.queue[0];
342
- transitionPromises.push(transitionAudioVolume(currentAudio, config.duckingVolume, duration, easing));
395
+ transitionPromises.push(transitionAudioVolume(currentAudio, config.duckingVolume, duration, easing, channelNumber));
343
396
  }
344
397
  });
345
398
  // Wait for all transitions to complete
@@ -376,7 +429,7 @@ const restoreVolumeLevels = (stoppedChannelNumber) => __awaiter(void 0, void 0,
376
429
  const targetVolume = (_c = channel.volume) !== null && _c !== void 0 ? _c : 1.0;
377
430
  // Only transition the audio element volume, keep channel.volume as the desired volume
378
431
  const currentAudio = channel.queue[0];
379
- transitionPromises.push(transitionAudioVolume(currentAudio, targetVolume, duration, easing));
432
+ transitionPromises.push(transitionAudioVolume(currentAudio, targetVolume, duration, easing, channelNumber));
380
433
  });
381
434
  // Wait for all transitions to complete
382
435
  yield Promise.all(transitionPromises);
@@ -385,23 +438,42 @@ exports.restoreVolumeLevels = restoreVolumeLevels;
385
438
  /**
386
439
  * Transitions only the audio element volume without affecting channel.volume
387
440
  * This is used for ducking/restoration where channel.volume represents desired volume
441
+ * Uses Web Audio API when available for enhanced volume control
388
442
  * @param audio - The audio element to transition
389
443
  * @param targetVolume - Target volume level (0-1)
390
444
  * @param duration - Transition duration in milliseconds
391
445
  * @param easing - Easing function type
446
+ * @param channelNumber - The channel number this audio belongs to (for Web Audio API)
392
447
  * @returns Promise that resolves when transition completes
393
448
  * @internal
394
449
  */
395
- const transitionAudioVolume = (audio_1, targetVolume_1, ...args_1) => __awaiter(void 0, [audio_1, targetVolume_1, ...args_1], void 0, function* (audio, targetVolume, duration = 250, easing = types_1.EasingType.EaseOut) {
450
+ const transitionAudioVolume = (audio_1, targetVolume_1, ...args_1) => __awaiter(void 0, [audio_1, targetVolume_1, ...args_1], void 0, function* (audio, targetVolume, duration = 250, easing = types_1.EasingType.EaseOut, channelNumber) {
451
+ // Apply global volume multiplier
452
+ const actualTargetVolume = targetVolume * globalVolume;
453
+ // Try to use Web Audio API if available and channel number is provided
454
+ if (channelNumber !== undefined) {
455
+ const channel = info_1.audioChannels[channelNumber];
456
+ if ((channel === null || channel === void 0 ? void 0 : channel.webAudioContext) && channel.webAudioNodes) {
457
+ const nodes = channel.webAudioNodes.get(audio);
458
+ if (nodes) {
459
+ // Use Web Audio API for smooth transitions
460
+ (0, web_audio_1.setWebAudioVolume)(nodes.gainNode, actualTargetVolume, duration);
461
+ // Also update the audio element's volume property for consistency
462
+ audio.volume = actualTargetVolume;
463
+ return;
464
+ }
465
+ }
466
+ }
467
+ // Fallback to standard HTMLAudioElement volume control with manual transition
396
468
  const startVolume = audio.volume;
397
- const volumeDelta = targetVolume - startVolume;
469
+ const volumeDelta = actualTargetVolume - startVolume;
398
470
  // If no change needed, resolve immediately
399
471
  if (Math.abs(volumeDelta) < 0.001) {
400
472
  return Promise.resolve();
401
473
  }
402
474
  // Handle zero or negative duration - instant change
403
475
  if (duration <= 0) {
404
- audio.volume = Math.max(0, Math.min(1, targetVolume));
476
+ audio.volume = actualTargetVolume;
405
477
  return Promise.resolve();
406
478
  }
407
479
  const startTime = performance.now();
@@ -424,7 +496,8 @@ const transitionAudioVolume = (audio_1, targetVolume_1, ...args_1) => __awaiter(
424
496
  requestAnimationFrame(updateVolume);
425
497
  }
426
498
  else {
427
- setTimeout(updateVolume, 1);
499
+ // In test environment, use longer intervals to prevent stack overflow
500
+ setTimeout(updateVolume, 16);
428
501
  }
429
502
  }
430
503
  };
@@ -467,3 +540,96 @@ const cancelAllVolumeTransitions = () => {
467
540
  });
468
541
  };
469
542
  exports.cancelAllVolumeTransitions = cancelAllVolumeTransitions;
543
+ /**
544
+ * Initializes Web Audio API for a specific channel
545
+ * @param channelNumber - The channel number to initialize Web Audio for
546
+ * @internal
547
+ */
548
+ const initializeWebAudioForChannel = (channelNumber) => __awaiter(void 0, void 0, void 0, function* () {
549
+ const channel = info_1.audioChannels[channelNumber];
550
+ if (!channel || channel.webAudioContext)
551
+ return;
552
+ const audioContext = (0, web_audio_1.getAudioContext)();
553
+ if (!audioContext) {
554
+ throw new Error('AudioContext creation failed');
555
+ }
556
+ // Resume audio context if needed (for autoplay policy)
557
+ yield (0, web_audio_1.resumeAudioContext)(audioContext);
558
+ channel.webAudioContext = audioContext;
559
+ channel.webAudioNodes = new Map();
560
+ // Initialize Web Audio nodes for existing audio elements
561
+ for (const audio of channel.queue) {
562
+ const nodes = (0, web_audio_1.createWebAudioNodes)(audio, audioContext);
563
+ if (!nodes) {
564
+ throw new Error('Node creation failed');
565
+ }
566
+ channel.webAudioNodes.set(audio, nodes);
567
+ // Set initial volume to match channel volume
568
+ nodes.gainNode.gain.value = channel.volume;
569
+ }
570
+ });
571
+ /**
572
+ * Sets volume for an audio element using the appropriate method (Web Audio API or standard)
573
+ * @param audio - The audio element to set volume for
574
+ * @param volume - Channel volume level (0-1) - will be multiplied by global volume
575
+ * @param channelNumber - The channel number this audio belongs to
576
+ * @param transitionDuration - Optional transition duration in milliseconds
577
+ * @internal
578
+ */
579
+ const setVolumeForAudio = (audio, volume, channelNumber, transitionDuration) => __awaiter(void 0, void 0, void 0, function* () {
580
+ const channel = info_1.audioChannels[channelNumber];
581
+ // Apply global volume multiplier to the channel volume
582
+ const actualVolume = volume * globalVolume;
583
+ // Use Web Audio API if available and initialized
584
+ if ((channel === null || channel === void 0 ? void 0 : channel.webAudioContext) && channel.webAudioNodes) {
585
+ const nodes = channel.webAudioNodes.get(audio);
586
+ if (nodes) {
587
+ (0, web_audio_1.setWebAudioVolume)(nodes.gainNode, actualVolume, transitionDuration);
588
+ return;
589
+ }
590
+ }
591
+ // Fallback to standard HTMLAudioElement volume control
592
+ audio.volume = actualVolume;
593
+ });
594
+ /**
595
+ * Initializes Web Audio API nodes for a new audio element
596
+ * @param audio - The audio element to initialize nodes for
597
+ * @param channelNumber - The channel number this audio belongs to
598
+ * @internal
599
+ */
600
+ const initializeWebAudioForAudio = (audio, channelNumber) => __awaiter(void 0, void 0, void 0, function* () {
601
+ const channel = info_1.audioChannels[channelNumber];
602
+ if (!channel)
603
+ return;
604
+ // Initialize Web Audio API for the channel if needed
605
+ if ((0, web_audio_1.shouldUseWebAudio)() && !channel.webAudioContext) {
606
+ yield initializeWebAudioForChannel(channelNumber);
607
+ }
608
+ // Create nodes for this specific audio element
609
+ if (channel.webAudioContext && channel.webAudioNodes && !channel.webAudioNodes.has(audio)) {
610
+ const nodes = (0, web_audio_1.createWebAudioNodes)(audio, channel.webAudioContext);
611
+ if (nodes) {
612
+ channel.webAudioNodes.set(audio, nodes);
613
+ // Set initial volume to match channel volume
614
+ nodes.gainNode.gain.value = channel.volume;
615
+ }
616
+ }
617
+ });
618
+ exports.initializeWebAudioForAudio = initializeWebAudioForAudio;
619
+ /**
620
+ * Cleans up Web Audio API nodes for an audio element
621
+ * @param audio - The audio element to clean up nodes for
622
+ * @param channelNumber - The channel number this audio belongs to
623
+ * @internal
624
+ */
625
+ const cleanupWebAudioForAudio = (audio, channelNumber) => {
626
+ const channel = info_1.audioChannels[channelNumber];
627
+ if (!(channel === null || channel === void 0 ? void 0 : channel.webAudioNodes))
628
+ return;
629
+ const nodes = channel.webAudioNodes.get(audio);
630
+ if (nodes) {
631
+ (0, web_audio_1.cleanupWebAudioNodes)(nodes);
632
+ channel.webAudioNodes.delete(audio);
633
+ }
634
+ };
635
+ exports.cleanupWebAudioForAudio = cleanupWebAudioForAudio;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @fileoverview Web Audio API support for enhanced volume control on iOS and other platforms
3
+ */
4
+ import { WebAudioConfig, WebAudioSupport, WebAudioNodeSet } from './types';
5
+ /**
6
+ * Detects if the current device is iOS
7
+ * @returns True if the device is iOS, false otherwise
8
+ * @example
9
+ * ```typescript
10
+ * if (isIOSDevice()) {
11
+ * console.log('Running on iOS device');
12
+ * }
13
+ * ```
14
+ */
15
+ export declare const isIOSDevice: () => boolean;
16
+ /**
17
+ * Checks if Web Audio API is available in the current environment
18
+ * @returns True if Web Audio API is supported, false otherwise
19
+ * @example
20
+ * ```typescript
21
+ * if (isWebAudioSupported()) {
22
+ * console.log('Web Audio API is available');
23
+ * }
24
+ * ```
25
+ */
26
+ export declare const isWebAudioSupported: () => boolean;
27
+ /**
28
+ * Determines if Web Audio API should be used based on configuration and device detection
29
+ * @returns True if Web Audio API should be used, false otherwise
30
+ * @example
31
+ * ```typescript
32
+ * if (shouldUseWebAudio()) {
33
+ * // Use Web Audio API for volume control
34
+ * }
35
+ * ```
36
+ */
37
+ export declare const shouldUseWebAudio: () => boolean;
38
+ /**
39
+ * Gets information about Web Audio API support and usage
40
+ * @returns Object containing Web Audio API support information
41
+ * @example
42
+ * ```typescript
43
+ * const support = getWebAudioSupport();
44
+ * console.log(`Using Web Audio: ${support.usingWebAudio}`);
45
+ * console.log(`Reason: ${support.reason}`);
46
+ * ```
47
+ */
48
+ export declare const getWebAudioSupport: () => WebAudioSupport;
49
+ /**
50
+ * Configures Web Audio API usage
51
+ * @param config - Configuration options for Web Audio API
52
+ * @example
53
+ * ```typescript
54
+ * // Force Web Audio API usage on all devices
55
+ * setWebAudioConfig({ forceWebAudio: true });
56
+ *
57
+ * // Disable Web Audio API entirely
58
+ * setWebAudioConfig({ enabled: false });
59
+ * ```
60
+ */
61
+ export declare const setWebAudioConfig: (config: Partial<WebAudioConfig>) => void;
62
+ /**
63
+ * Gets the current Web Audio API configuration
64
+ * @returns Current Web Audio API configuration
65
+ * @example
66
+ * ```typescript
67
+ * const config = getWebAudioConfig();
68
+ * console.log(`Web Audio enabled: ${config.enabled}`);
69
+ * ```
70
+ */
71
+ export declare const getWebAudioConfig: () => WebAudioConfig;
72
+ /**
73
+ * Creates or gets an AudioContext for Web Audio API operations
74
+ * @returns AudioContext instance or null if not supported
75
+ * @example
76
+ * ```typescript
77
+ * const context = getAudioContext();
78
+ * if (context) {
79
+ * console.log('Audio context created successfully');
80
+ * }
81
+ * ```
82
+ */
83
+ export declare const getAudioContext: () => AudioContext | null;
84
+ /**
85
+ * Creates Web Audio API nodes for an audio element
86
+ * @param audioElement - The HTML audio element to create nodes for
87
+ * @param audioContext - The AudioContext to use
88
+ * @returns Web Audio API node set or null if creation fails
89
+ * @example
90
+ * ```typescript
91
+ * const audio = new Audio('song.mp3');
92
+ * const context = getAudioContext();
93
+ * if (context) {
94
+ * const nodes = createWebAudioNodes(audio, context);
95
+ * if (nodes) {
96
+ * nodes.gainNode.gain.value = 0.5; // Set volume to 50%
97
+ * }
98
+ * }
99
+ * ```
100
+ */
101
+ export declare const createWebAudioNodes: (audioElement: HTMLAudioElement, audioContext: AudioContext) => WebAudioNodeSet | null;
102
+ /**
103
+ * Sets volume using Web Audio API gain node
104
+ * @param gainNode - The gain node to set volume on
105
+ * @param volume - Volume level (0-1)
106
+ * @param transitionDuration - Optional transition duration in milliseconds
107
+ * @example
108
+ * ```typescript
109
+ * const nodes = createWebAudioNodes(audio, context);
110
+ * if (nodes) {
111
+ * setWebAudioVolume(nodes.gainNode, 0.5); // Set to 50% volume
112
+ * setWebAudioVolume(nodes.gainNode, 0.2, 300); // Fade to 20% over 300ms
113
+ * }
114
+ * ```
115
+ */
116
+ export declare const setWebAudioVolume: (gainNode: GainNode, volume: number, transitionDuration?: number) => void;
117
+ /**
118
+ * Gets the current volume from a Web Audio API gain node
119
+ * @param gainNode - The gain node to get volume from
120
+ * @returns Current volume level (0-1)
121
+ * @example
122
+ * ```typescript
123
+ * const nodes = createWebAudioNodes(audio, context);
124
+ * if (nodes) {
125
+ * const volume = getWebAudioVolume(nodes.gainNode);
126
+ * console.log(`Current volume: ${volume * 100}%`);
127
+ * }
128
+ * ```
129
+ */
130
+ export declare const getWebAudioVolume: (gainNode: GainNode) => number;
131
+ /**
132
+ * Resumes an AudioContext if it's in suspended state (required for autoplay policy)
133
+ * @param audioContext - The AudioContext to resume
134
+ * @returns Promise that resolves when context is resumed
135
+ * @example
136
+ * ```typescript
137
+ * const context = getAudioContext();
138
+ * if (context) {
139
+ * await resumeAudioContext(context);
140
+ * }
141
+ * ```
142
+ */
143
+ export declare const resumeAudioContext: (audioContext: AudioContext) => Promise<void>;
144
+ /**
145
+ * Cleans up Web Audio API nodes and connections
146
+ * @param nodes - The Web Audio API node set to clean up
147
+ * @example
148
+ * ```typescript
149
+ * const nodes = createWebAudioNodes(audio, context);
150
+ * if (nodes) {
151
+ * // Use nodes...
152
+ * cleanupWebAudioNodes(nodes); // Clean up when done
153
+ * }
154
+ * ```
155
+ */
156
+ export declare const cleanupWebAudioNodes: (nodes: WebAudioNodeSet) => void;