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/dist/volume.js ADDED
@@ -0,0 +1,644 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Volume management functions for the audioq package
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.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
+ const types_1 = require("./types");
17
+ const info_1 = require("./info");
18
+ const web_audio_1 = require("./web-audio");
19
+ // Store active volume transitions to handle interruptions
20
+ const activeTransitions = new Map();
21
+ // Track which timer type was used for each channel
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;
28
+ /**
29
+ * Global volume ducking configuration
30
+ * Stores the volume ducking settings that apply to all channels
31
+ */
32
+ let globalVolumeConfig = null;
33
+ /**
34
+ * Predefined fade configurations for different transition types
35
+ */
36
+ const fadeConfigs = {
37
+ [types_1.FadeType.Dramatic]: {
38
+ duration: 800,
39
+ pauseCurve: types_1.EasingType.EaseIn,
40
+ resumeCurve: types_1.EasingType.EaseOut
41
+ },
42
+ [types_1.FadeType.Gentle]: {
43
+ duration: 800,
44
+ pauseCurve: types_1.EasingType.EaseOut,
45
+ resumeCurve: types_1.EasingType.EaseIn
46
+ },
47
+ [types_1.FadeType.Linear]: {
48
+ duration: 800,
49
+ pauseCurve: types_1.EasingType.Linear,
50
+ resumeCurve: types_1.EasingType.Linear
51
+ }
52
+ };
53
+ /**
54
+ * Gets the fade configuration for a specific fade type
55
+ * @param fadeType - The fade type to get configuration for
56
+ * @returns Fade configuration object
57
+ * @example
58
+ * ```typescript
59
+ * const config = getFadeConfig('gentle');
60
+ * console.log(`Gentle fade duration: ${config.duration}ms`);
61
+ * ```
62
+ */
63
+ const getFadeConfig = (fadeType) => {
64
+ return Object.assign({}, fadeConfigs[fadeType]);
65
+ };
66
+ exports.getFadeConfig = getFadeConfig;
67
+ /**
68
+ * Easing functions for smooth volume transitions
69
+ */
70
+ const easingFunctions = {
71
+ [types_1.EasingType.Linear]: (t) => t,
72
+ [types_1.EasingType.EaseIn]: (t) => t * t,
73
+ [types_1.EasingType.EaseOut]: (t) => t * (2 - t),
74
+ [types_1.EasingType.EaseInOut]: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)
75
+ };
76
+ /**
77
+ * Smoothly transitions volume for a specific channel over time
78
+ * @param channelNumber - The channel number to transition
79
+ * @param targetVolume - Target volume level (0-1)
80
+ * @param duration - Transition duration in milliseconds
81
+ * @param easing - Easing function type
82
+ * @returns Promise that resolves when transition completes
83
+ * @example
84
+ * ```typescript
85
+ * await transitionVolume(0, 0.2, 500, 'ease-out'); // Duck to 20% over 500ms
86
+ * ```
87
+ */
88
+ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __awaiter(void 0, [channelNumber_1, targetVolume_1, ...args_1], void 0, function* (channelNumber, targetVolume, duration = 250, easing = types_1.EasingType.EaseOut) {
89
+ const channel = info_1.audioChannels[channelNumber];
90
+ if (!channel || channel.queue.length === 0) {
91
+ return;
92
+ }
93
+ const currentAudio = channel.queue[0];
94
+ // When Web Audio is active, read the actual start volume from the gain node
95
+ // This is critical for iOS where audio.volume is ignored when Web Audio is active
96
+ let startVolume = currentAudio.volume;
97
+ if (channel.webAudioNodes) {
98
+ const nodes = channel.webAudioNodes.get(currentAudio);
99
+ if (nodes) {
100
+ startVolume = nodes.gainNode.gain.value;
101
+ }
102
+ }
103
+ const volumeDelta = targetVolume - startVolume;
104
+ // Cancel any existing transition for this channel
105
+ if (activeTransitions.has(channelNumber)) {
106
+ const transitionId = activeTransitions.get(channelNumber);
107
+ const timerType = timerTypes.get(channelNumber);
108
+ if (transitionId) {
109
+ // Cancel based on the timer type that was actually used
110
+ if (timerType === types_1.TimerType.RequestAnimationFrame &&
111
+ typeof cancelAnimationFrame !== 'undefined') {
112
+ cancelAnimationFrame(transitionId);
113
+ }
114
+ else if (timerType === types_1.TimerType.Timeout) {
115
+ clearTimeout(transitionId);
116
+ }
117
+ }
118
+ activeTransitions.delete(channelNumber);
119
+ timerTypes.delete(channelNumber);
120
+ }
121
+ // If no change needed, resolve immediately
122
+ if (Math.abs(volumeDelta) < 0.001) {
123
+ channel.volume = targetVolume;
124
+ return Promise.resolve();
125
+ }
126
+ // Handle zero or negative duration - instant change
127
+ if (duration <= 0) {
128
+ channel.volume = targetVolume;
129
+ if (channel.queue.length > 0) {
130
+ channel.queue[0].volume = targetVolume;
131
+ }
132
+ return Promise.resolve();
133
+ }
134
+ const startTime = performance.now();
135
+ const easingFn = easingFunctions[easing];
136
+ return new Promise((resolve) => {
137
+ const updateVolume = () => __awaiter(void 0, void 0, void 0, function* () {
138
+ const elapsed = performance.now() - startTime;
139
+ const progress = Math.min(elapsed / duration, 1);
140
+ const easedProgress = easingFn(progress);
141
+ const currentVolume = startVolume + volumeDelta * easedProgress;
142
+ const clampedVolume = Math.max(0, Math.min(1, currentVolume));
143
+ // Apply volume to both channel config and current audio
144
+ channel.volume = clampedVolume;
145
+ if (channel.queue.length > 0) {
146
+ yield setVolumeForAudio(channel.queue[0], clampedVolume, channelNumber);
147
+ }
148
+ if (progress >= 1) {
149
+ // Transition complete
150
+ activeTransitions.delete(channelNumber);
151
+ timerTypes.delete(channelNumber);
152
+ resolve();
153
+ }
154
+ else {
155
+ // Use requestAnimationFrame in browser, setTimeout in tests
156
+ if (typeof requestAnimationFrame !== 'undefined') {
157
+ const rafId = requestAnimationFrame(() => updateVolume());
158
+ activeTransitions.set(channelNumber, rafId);
159
+ timerTypes.set(channelNumber, types_1.TimerType.RequestAnimationFrame);
160
+ }
161
+ else {
162
+ // In test environment, use shorter intervals
163
+ const timeoutId = setTimeout(() => updateVolume(), 1);
164
+ activeTransitions.set(channelNumber, timeoutId);
165
+ timerTypes.set(channelNumber, types_1.TimerType.Timeout);
166
+ }
167
+ }
168
+ });
169
+ updateVolume();
170
+ });
171
+ });
172
+ exports.transitionVolume = transitionVolume;
173
+ /**
174
+ * Sets the volume for a specific channel with optional smooth transition
175
+ * Automatically uses Web Audio API on iOS devices for enhanced volume control
176
+ * @param channelNumber - The channel number to set volume for
177
+ * @param volume - Volume level (0-1)
178
+ * @param transitionDuration - Optional transition duration in milliseconds
179
+ * @param easing - Optional easing function
180
+ * @throws Error if the channel number exceeds the maximum allowed channels
181
+ * @example
182
+ * ```typescript
183
+ * setChannelVolume(0, 0.5); // Set channel 0 to 50%
184
+ * setChannelVolume(0, 0.5, 300, 'ease-out'); // Smooth transition over 300ms
185
+ * ```
186
+ */
187
+ const setChannelVolume = (channelNumber, volume, transitionDuration, easing) => __awaiter(void 0, void 0, void 0, function* () {
188
+ const clampedVolume = Math.max(0, Math.min(1, volume));
189
+ // Check channel number limits
190
+ if (channelNumber < 0) {
191
+ throw new Error('Channel number must be non-negative');
192
+ }
193
+ if (channelNumber >= types_1.MAX_CHANNELS) {
194
+ throw new Error(`Channel number ${channelNumber} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
195
+ }
196
+ if (!info_1.audioChannels[channelNumber]) {
197
+ info_1.audioChannels[channelNumber] = {
198
+ audioCompleteCallbacks: new Set(),
199
+ audioErrorCallbacks: new Set(),
200
+ audioPauseCallbacks: new Set(),
201
+ audioResumeCallbacks: new Set(),
202
+ audioStartCallbacks: new Set(),
203
+ isPaused: false,
204
+ progressCallbacks: new Map(),
205
+ queue: [],
206
+ queueChangeCallbacks: new Set(),
207
+ volume: clampedVolume
208
+ };
209
+ return;
210
+ }
211
+ const channel = info_1.audioChannels[channelNumber];
212
+ // Initialize Web Audio API if needed and supported
213
+ if ((0, web_audio_1.shouldUseWebAudio)() && !channel.webAudioContext) {
214
+ yield initializeWebAudioForChannel(channelNumber);
215
+ }
216
+ if (transitionDuration && transitionDuration > 0) {
217
+ // Smooth transition
218
+ yield (0, exports.transitionVolume)(channelNumber, clampedVolume, transitionDuration, easing);
219
+ }
220
+ else {
221
+ // Instant change (backward compatibility)
222
+ channel.volume = clampedVolume;
223
+ if (channel.queue.length > 0) {
224
+ const currentAudio = channel.queue[0];
225
+ yield setVolumeForAudio(currentAudio, clampedVolume, channelNumber);
226
+ }
227
+ }
228
+ });
229
+ exports.setChannelVolume = setChannelVolume;
230
+ /**
231
+ * Gets the current volume for a specific channel
232
+ * @param channelNumber - The channel number to get volume for (defaults to 0)
233
+ * @returns Current volume level (0-1) or 1.0 if channel doesn't exist
234
+ * @example
235
+ * ```typescript
236
+ * const volume = getChannelVolume(0);
237
+ * const defaultChannelVolume = getChannelVolume(); // Gets channel 0
238
+ * console.log(`Channel 0 volume: ${volume * 100}%`);
239
+ * ```
240
+ */
241
+ const getChannelVolume = (channelNumber = 0) => {
242
+ var _a;
243
+ const channel = info_1.audioChannels[channelNumber];
244
+ return (_a = channel === null || channel === void 0 ? void 0 : channel.volume) !== null && _a !== void 0 ? _a : 1.0;
245
+ };
246
+ exports.getChannelVolume = getChannelVolume;
247
+ /**
248
+ * Gets the volume levels for all channels
249
+ * @returns Array of volume levels (0-1) for each channel
250
+ * @example
251
+ * ```typescript
252
+ * const volumes = getAllChannelsVolume();
253
+ * volumes.forEach((volume, index) => {
254
+ * console.log(`Channel ${index}: ${volume * 100}%`);
255
+ * });
256
+ * ```
257
+ */
258
+ const getAllChannelsVolume = () => {
259
+ return info_1.audioChannels.map((channel) => { var _a; return (_a = channel === null || channel === void 0 ? void 0 : channel.volume) !== null && _a !== void 0 ? _a : 1.0; });
260
+ };
261
+ exports.getAllChannelsVolume = getAllChannelsVolume;
262
+ /**
263
+ * Sets volume for all channels to the same level
264
+ * @param volume - Volume level (0-1) to apply to all channels
265
+ * @example
266
+ * ```typescript
267
+ * await setAllChannelsVolume(0.6); // Set all channels to 60% volume
268
+ * ```
269
+ */
270
+ const setAllChannelsVolume = (volume) => __awaiter(void 0, void 0, void 0, function* () {
271
+ const promises = [];
272
+ info_1.audioChannels.forEach((_channel, index) => {
273
+ promises.push((0, exports.setChannelVolume)(index, volume));
274
+ });
275
+ yield Promise.all(promises);
276
+ });
277
+ exports.setAllChannelsVolume = setAllChannelsVolume;
278
+ /**
279
+ * Sets the global volume multiplier that affects all channels
280
+ * This acts as a global volume control - individual channel volumes are multiplied by this value
281
+ * @param volume - Global volume level (0-1, will be clamped to this range)
282
+ * @example
283
+ * ```typescript
284
+ * // Set channel-specific volumes
285
+ * await setChannelVolume(0, 0.8); // SFX at 80%
286
+ * await setChannelVolume(1, 0.6); // Music at 60%
287
+ *
288
+ * // Apply global volume of 50% - all channels play at half their set volume
289
+ * await setGlobalVolume(0.5); // SFX now plays at 40%, music at 30%
290
+ * ```
291
+ */
292
+ const setGlobalVolume = (volume) => __awaiter(void 0, void 0, void 0, function* () {
293
+ // Clamp to valid range
294
+ globalVolume = Math.max(0, Math.min(1, volume));
295
+ // Update all currently playing audio to reflect the new global volume
296
+ // Note: setVolumeForAudio internally multiplies channel.volume by globalVolume
297
+ const updatePromises = [];
298
+ info_1.audioChannels.forEach((channel, channelNumber) => {
299
+ if (channel && channel.queue.length > 0) {
300
+ const currentAudio = channel.queue[0];
301
+ updatePromises.push(setVolumeForAudio(currentAudio, channel.volume, channelNumber));
302
+ }
303
+ });
304
+ yield Promise.all(updatePromises);
305
+ });
306
+ exports.setGlobalVolume = setGlobalVolume;
307
+ /**
308
+ * Gets the current global volume multiplier
309
+ * @returns Current global volume level (0-1), defaults to 1.0
310
+ * @example
311
+ * ```typescript
312
+ * const globalVol = getGlobalVolume();
313
+ * console.log(`Global volume is ${globalVol * 100}%`);
314
+ * ```
315
+ */
316
+ const getGlobalVolume = () => {
317
+ return globalVolume;
318
+ };
319
+ exports.getGlobalVolume = getGlobalVolume;
320
+ /**
321
+ * Configures volume ducking for channels. When the priority channel plays audio,
322
+ * all other channels will be automatically reduced to the ducking volume level
323
+ * @param config - Volume ducking configuration
324
+ * @throws Error if the priority channel number exceeds the maximum allowed channels
325
+ * @example
326
+ * ```typescript
327
+ * // When channel 1 plays, reduce all other channels to 20% volume
328
+ * setVolumeDucking({
329
+ * priorityChannel: 1,
330
+ * priorityVolume: 1.0,
331
+ * duckingVolume: 0.2
332
+ * });
333
+ * ```
334
+ */
335
+ const setVolumeDucking = (config) => {
336
+ const { priorityChannel } = config;
337
+ // Check priority channel limits
338
+ if (priorityChannel < 0) {
339
+ throw new Error('Priority channel number must be non-negative');
340
+ }
341
+ if (priorityChannel >= types_1.MAX_CHANNELS) {
342
+ throw new Error(`Priority channel ${priorityChannel} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
343
+ }
344
+ // Store the configuration globally
345
+ globalVolumeConfig = config;
346
+ // Ensure we have enough channels for the priority channel
347
+ while (info_1.audioChannels.length <= priorityChannel) {
348
+ info_1.audioChannels.push({
349
+ audioCompleteCallbacks: new Set(),
350
+ audioErrorCallbacks: new Set(),
351
+ audioPauseCallbacks: new Set(),
352
+ audioResumeCallbacks: new Set(),
353
+ audioStartCallbacks: new Set(),
354
+ isPaused: false,
355
+ progressCallbacks: new Map(),
356
+ queue: [],
357
+ queueChangeCallbacks: new Set(),
358
+ volume: 1.0
359
+ });
360
+ }
361
+ };
362
+ exports.setVolumeDucking = setVolumeDucking;
363
+ /**
364
+ * Removes volume ducking configuration from all channels
365
+ * @example
366
+ * ```typescript
367
+ * clearVolumeDucking(); // Remove all volume ducking effects
368
+ * ```
369
+ */
370
+ const clearVolumeDucking = () => {
371
+ globalVolumeConfig = null;
372
+ };
373
+ exports.clearVolumeDucking = clearVolumeDucking;
374
+ /**
375
+ * Applies volume ducking effects based on current playback state with smooth transitions
376
+ * @param activeChannelNumber - The channel that just started playing
377
+ * @internal
378
+ */
379
+ const applyVolumeDucking = (activeChannelNumber) => __awaiter(void 0, void 0, void 0, function* () {
380
+ var _a, _b;
381
+ // Check if ducking is configured and this channel is the priority channel
382
+ if (!globalVolumeConfig || globalVolumeConfig.priorityChannel !== activeChannelNumber) {
383
+ return; // No ducking configured for this channel
384
+ }
385
+ const config = globalVolumeConfig;
386
+ const transitionPromises = [];
387
+ const duration = (_a = config.duckTransitionDuration) !== null && _a !== void 0 ? _a : 250;
388
+ const easing = (_b = config.transitionEasing) !== null && _b !== void 0 ? _b : types_1.EasingType.EaseOut;
389
+ // Duck all channels except the priority channel
390
+ info_1.audioChannels.forEach((channel, channelNumber) => {
391
+ if (!channel || channel.queue.length === 0) {
392
+ return; // Skip channels without audio
393
+ }
394
+ if (channelNumber === activeChannelNumber) {
395
+ // This is the priority channel - set to priority volume
396
+ // Only change audio volume, preserve channel.volume as desired volume
397
+ const currentAudio = channel.queue[0];
398
+ transitionPromises.push(transitionAudioVolume(currentAudio, config.priorityVolume, duration, easing, channelNumber));
399
+ }
400
+ else {
401
+ // This is a background channel - duck it
402
+ // Only change audio volume, preserve channel.volume as desired volume
403
+ const currentAudio = channel.queue[0];
404
+ transitionPromises.push(transitionAudioVolume(currentAudio, config.duckingVolume, duration, easing, channelNumber));
405
+ }
406
+ });
407
+ // Wait for all transitions to complete
408
+ yield Promise.all(transitionPromises);
409
+ });
410
+ exports.applyVolumeDucking = applyVolumeDucking;
411
+ /**
412
+ * Restores normal volume levels when priority channel queue becomes empty
413
+ * @param stoppedChannelNumber - The channel that just stopped playing
414
+ * @internal
415
+ */
416
+ const restoreVolumeLevels = (stoppedChannelNumber) => __awaiter(void 0, void 0, void 0, function* () {
417
+ // Check if ducking is configured and this channel is the priority channel
418
+ if (!globalVolumeConfig || globalVolumeConfig.priorityChannel !== stoppedChannelNumber) {
419
+ return; // No ducking configured for this channel
420
+ }
421
+ // Check if the priority channel queue is now empty
422
+ const priorityChannel = info_1.audioChannels[stoppedChannelNumber];
423
+ if (priorityChannel && priorityChannel.queue.length > 0) {
424
+ return; // Priority channel still has audio queued, don't restore yet
425
+ }
426
+ const config = globalVolumeConfig;
427
+ const transitionPromises = [];
428
+ // Restore volume for all channels EXCEPT the priority channel
429
+ info_1.audioChannels.forEach((channel, channelNumber) => {
430
+ var _a, _b, _c;
431
+ // Skip the priority channel itself and channels without audio
432
+ if (channelNumber === stoppedChannelNumber || !channel || channel.queue.length === 0) {
433
+ return;
434
+ }
435
+ // Restore this channel to its desired volume
436
+ const duration = (_a = config.restoreTransitionDuration) !== null && _a !== void 0 ? _a : 250;
437
+ const easing = (_b = config.transitionEasing) !== null && _b !== void 0 ? _b : types_1.EasingType.EaseOut;
438
+ const targetVolume = (_c = channel.volume) !== null && _c !== void 0 ? _c : 1.0;
439
+ // Only transition the audio element volume, keep channel.volume as the desired volume
440
+ const currentAudio = channel.queue[0];
441
+ transitionPromises.push(transitionAudioVolume(currentAudio, targetVolume, duration, easing, channelNumber));
442
+ });
443
+ // Wait for all transitions to complete
444
+ yield Promise.all(transitionPromises);
445
+ });
446
+ exports.restoreVolumeLevels = restoreVolumeLevels;
447
+ /**
448
+ * Transitions only the audio element volume without affecting channel.volume
449
+ * This is used for ducking/restoration where channel.volume represents desired volume
450
+ * Uses Web Audio API when available for enhanced volume control
451
+ * @param audio - The audio element to transition
452
+ * @param targetVolume - Target volume level (0-1)
453
+ * @param duration - Transition duration in milliseconds
454
+ * @param easing - Easing function type
455
+ * @param channelNumber - The channel number this audio belongs to (for Web Audio API)
456
+ * @returns Promise that resolves when transition completes
457
+ * @internal
458
+ */
459
+ 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) {
460
+ // Apply global volume multiplier
461
+ const actualTargetVolume = targetVolume * globalVolume;
462
+ // Try to use Web Audio API if available and channel number is provided
463
+ if (channelNumber !== undefined) {
464
+ const channel = info_1.audioChannels[channelNumber];
465
+ if ((channel === null || channel === void 0 ? void 0 : channel.webAudioContext) && channel.webAudioNodes) {
466
+ const nodes = channel.webAudioNodes.get(audio);
467
+ if (nodes) {
468
+ // Use Web Audio API for smooth transitions
469
+ (0, web_audio_1.setWebAudioVolume)(nodes.gainNode, actualTargetVolume, duration);
470
+ // Also update the audio element's volume property for consistency
471
+ audio.volume = actualTargetVolume;
472
+ return;
473
+ }
474
+ }
475
+ }
476
+ // Fallback to standard HTMLAudioElement volume control with manual transition
477
+ const startVolume = audio.volume;
478
+ const volumeDelta = actualTargetVolume - startVolume;
479
+ // If no change needed, resolve immediately
480
+ if (Math.abs(volumeDelta) < 0.001) {
481
+ return Promise.resolve();
482
+ }
483
+ // Handle zero or negative duration - instant change
484
+ if (duration <= 0) {
485
+ audio.volume = actualTargetVolume;
486
+ return Promise.resolve();
487
+ }
488
+ const startTime = performance.now();
489
+ const easingFn = easingFunctions[easing];
490
+ return new Promise((resolve) => {
491
+ const updateVolume = () => {
492
+ const elapsed = performance.now() - startTime;
493
+ const progress = Math.min(elapsed / duration, 1);
494
+ const easedProgress = easingFn(progress);
495
+ const currentVolume = startVolume + volumeDelta * easedProgress;
496
+ const clampedVolume = Math.max(0, Math.min(1, currentVolume));
497
+ // Only apply volume to audio element, not channel.volume
498
+ audio.volume = clampedVolume;
499
+ if (progress >= 1) {
500
+ resolve();
501
+ }
502
+ else {
503
+ // Use requestAnimationFrame in browser, setTimeout in tests
504
+ if (typeof requestAnimationFrame !== 'undefined') {
505
+ requestAnimationFrame(updateVolume);
506
+ }
507
+ else {
508
+ // In test environment, use longer intervals to prevent stack overflow
509
+ setTimeout(updateVolume, 16);
510
+ }
511
+ }
512
+ };
513
+ updateVolume();
514
+ });
515
+ });
516
+ /**
517
+ * Cancels any active volume transition for a specific channel
518
+ * @param channelNumber - The channel number to cancel transitions for
519
+ * @internal
520
+ */
521
+ const cancelVolumeTransition = (channelNumber) => {
522
+ if (activeTransitions.has(channelNumber)) {
523
+ const transitionId = activeTransitions.get(channelNumber);
524
+ const timerType = timerTypes.get(channelNumber);
525
+ if (transitionId) {
526
+ // Cancel based on the timer type that was actually used
527
+ if (timerType === types_1.TimerType.RequestAnimationFrame &&
528
+ typeof cancelAnimationFrame !== 'undefined') {
529
+ cancelAnimationFrame(transitionId);
530
+ }
531
+ else if (timerType === types_1.TimerType.Timeout) {
532
+ clearTimeout(transitionId);
533
+ }
534
+ }
535
+ activeTransitions.delete(channelNumber);
536
+ timerTypes.delete(channelNumber);
537
+ }
538
+ };
539
+ exports.cancelVolumeTransition = cancelVolumeTransition;
540
+ /**
541
+ * Cancels all active volume transitions across all channels
542
+ * @internal
543
+ */
544
+ const cancelAllVolumeTransitions = () => {
545
+ // Get all active channel numbers to avoid modifying Map while iterating
546
+ const activeChannels = Array.from(activeTransitions.keys());
547
+ activeChannels.forEach((channelNumber) => {
548
+ (0, exports.cancelVolumeTransition)(channelNumber);
549
+ });
550
+ };
551
+ exports.cancelAllVolumeTransitions = cancelAllVolumeTransitions;
552
+ /**
553
+ * Initializes Web Audio API for a specific channel
554
+ * @param channelNumber - The channel number to initialize Web Audio for
555
+ * @internal
556
+ */
557
+ const initializeWebAudioForChannel = (channelNumber) => __awaiter(void 0, void 0, void 0, function* () {
558
+ const channel = info_1.audioChannels[channelNumber];
559
+ if (!channel || channel.webAudioContext)
560
+ return;
561
+ const audioContext = (0, web_audio_1.getAudioContext)();
562
+ if (!audioContext) {
563
+ throw new Error('AudioContext creation failed');
564
+ }
565
+ // Resume audio context if needed (for autoplay policy)
566
+ yield (0, web_audio_1.resumeAudioContext)(audioContext);
567
+ channel.webAudioContext = audioContext;
568
+ channel.webAudioNodes = new Map();
569
+ // Initialize Web Audio nodes for existing audio elements
570
+ for (const audio of channel.queue) {
571
+ const nodes = (0, web_audio_1.createWebAudioNodes)(audio, audioContext);
572
+ if (!nodes) {
573
+ throw new Error('Node creation failed');
574
+ }
575
+ channel.webAudioNodes.set(audio, nodes);
576
+ // Set initial volume to match channel volume
577
+ nodes.gainNode.gain.value = channel.volume;
578
+ }
579
+ });
580
+ /**
581
+ * Sets volume for an audio element using the appropriate method (Web Audio API or standard)
582
+ * @param audio - The audio element to set volume for
583
+ * @param volume - Channel volume level (0-1) - will be multiplied by global volume
584
+ * @param channelNumber - The channel number this audio belongs to
585
+ * @param transitionDuration - Optional transition duration in milliseconds
586
+ * @internal
587
+ */
588
+ const setVolumeForAudio = (audio, volume, channelNumber, transitionDuration) => __awaiter(void 0, void 0, void 0, function* () {
589
+ const channel = info_1.audioChannels[channelNumber];
590
+ // Apply global volume multiplier to the channel volume
591
+ const actualVolume = volume * globalVolume;
592
+ // Use Web Audio API if available and initialized
593
+ if ((channel === null || channel === void 0 ? void 0 : channel.webAudioContext) && channel.webAudioNodes) {
594
+ const nodes = channel.webAudioNodes.get(audio);
595
+ if (nodes) {
596
+ (0, web_audio_1.setWebAudioVolume)(nodes.gainNode, actualVolume, transitionDuration);
597
+ return;
598
+ }
599
+ }
600
+ // Fallback to standard HTMLAudioElement volume control
601
+ audio.volume = actualVolume;
602
+ });
603
+ /**
604
+ * Initializes Web Audio API nodes for a new audio element
605
+ * @param audio - The audio element to initialize nodes for
606
+ * @param channelNumber - The channel number this audio belongs to
607
+ * @internal
608
+ */
609
+ const initializeWebAudioForAudio = (audio, channelNumber) => __awaiter(void 0, void 0, void 0, function* () {
610
+ const channel = info_1.audioChannels[channelNumber];
611
+ if (!channel)
612
+ return;
613
+ // Initialize Web Audio API for the channel if needed
614
+ if ((0, web_audio_1.shouldUseWebAudio)() && !channel.webAudioContext) {
615
+ yield initializeWebAudioForChannel(channelNumber);
616
+ }
617
+ // Create nodes for this specific audio element
618
+ if (channel.webAudioContext && channel.webAudioNodes && !channel.webAudioNodes.has(audio)) {
619
+ const nodes = (0, web_audio_1.createWebAudioNodes)(audio, channel.webAudioContext);
620
+ if (nodes) {
621
+ channel.webAudioNodes.set(audio, nodes);
622
+ // Set initial volume to match channel volume with global volume multiplier
623
+ nodes.gainNode.gain.value = channel.volume * globalVolume;
624
+ }
625
+ }
626
+ });
627
+ exports.initializeWebAudioForAudio = initializeWebAudioForAudio;
628
+ /**
629
+ * Cleans up Web Audio API nodes for an audio element
630
+ * @param audio - The audio element to clean up nodes for
631
+ * @param channelNumber - The channel number this audio belongs to
632
+ * @internal
633
+ */
634
+ const cleanupWebAudioForAudio = (audio, channelNumber) => {
635
+ const channel = info_1.audioChannels[channelNumber];
636
+ if (!(channel === null || channel === void 0 ? void 0 : channel.webAudioNodes))
637
+ return;
638
+ const nodes = channel.webAudioNodes.get(audio);
639
+ if (nodes) {
640
+ (0, web_audio_1.cleanupWebAudioNodes)(nodes);
641
+ channel.webAudioNodes.delete(audio);
642
+ }
643
+ };
644
+ exports.cleanupWebAudioForAudio = cleanupWebAudioForAudio;