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/README.md +197 -313
- package/dist/core.d.ts +59 -1
- package/dist/core.js +333 -41
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +37 -18
- package/dist/events.js +21 -14
- package/dist/index.d.ts +9 -8
- package/dist/index.js +23 -3
- package/dist/info.d.ts +17 -6
- package/dist/info.js +89 -17
- package/dist/pause.d.ts +24 -12
- package/dist/pause.js +93 -41
- package/dist/queue-manipulation.d.ts +104 -0
- package/dist/queue-manipulation.js +319 -0
- package/dist/types.d.ts +58 -11
- package/dist/types.js +18 -1
- package/dist/utils.d.ts +25 -0
- package/dist/utils.js +102 -13
- package/dist/volume.d.ts +14 -1
- package/dist/volume.js +201 -60
- package/package.json +18 -3
- package/src/core.ts +437 -81
- package/src/errors.ts +516 -480
- package/src/events.ts +36 -27
- package/src/index.ts +68 -43
- package/src/info.ts +129 -30
- package/src/pause.ts +169 -88
- package/src/queue-manipulation.ts +378 -0
- package/src/types.ts +63 -11
- package/src/utils.ts +117 -16
- package/src/volume.ts +250 -81
package/src/volume.ts
CHANGED
|
@@ -2,19 +2,47 @@
|
|
|
2
2
|
* @fileoverview Volume management functions for the audio-channel-queue package
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ExtendedAudioQueueChannel,
|
|
7
|
+
VolumeConfig,
|
|
8
|
+
FadeType,
|
|
9
|
+
FadeConfig,
|
|
10
|
+
EasingType,
|
|
11
|
+
TimerType,
|
|
12
|
+
MAX_CHANNELS
|
|
13
|
+
} from './types';
|
|
6
14
|
import { audioChannels } from './info';
|
|
7
15
|
|
|
8
16
|
// Store active volume transitions to handle interruptions
|
|
9
|
-
const activeTransitions
|
|
17
|
+
const activeTransitions: Map<number, number> = new Map();
|
|
18
|
+
// Track which timer type was used for each channel
|
|
19
|
+
const timerTypes: Map<number, TimerType> = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Global volume ducking configuration
|
|
23
|
+
* Stores the volume ducking settings that apply to all channels
|
|
24
|
+
*/
|
|
25
|
+
let globalVolumeConfig: VolumeConfig | null = null;
|
|
10
26
|
|
|
11
27
|
/**
|
|
12
28
|
* Predefined fade configurations for different transition types
|
|
13
29
|
*/
|
|
14
|
-
const
|
|
15
|
-
[FadeType.Dramatic]: {
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
const fadeConfigs: Record<FadeType, FadeConfig> = {
|
|
31
|
+
[FadeType.Dramatic]: {
|
|
32
|
+
duration: 800,
|
|
33
|
+
pauseCurve: EasingType.EaseIn,
|
|
34
|
+
resumeCurve: EasingType.EaseOut
|
|
35
|
+
},
|
|
36
|
+
[FadeType.Gentle]: {
|
|
37
|
+
duration: 800,
|
|
38
|
+
pauseCurve: EasingType.EaseOut,
|
|
39
|
+
resumeCurve: EasingType.EaseIn
|
|
40
|
+
},
|
|
41
|
+
[FadeType.Linear]: {
|
|
42
|
+
duration: 800,
|
|
43
|
+
pauseCurve: EasingType.Linear,
|
|
44
|
+
resumeCurve: EasingType.Linear
|
|
45
|
+
}
|
|
18
46
|
};
|
|
19
47
|
|
|
20
48
|
/**
|
|
@@ -28,7 +56,7 @@ const FADE_CONFIGS: Record<FadeType, FadeConfig> = {
|
|
|
28
56
|
* ```
|
|
29
57
|
*/
|
|
30
58
|
export const getFadeConfig = (fadeType: FadeType): FadeConfig => {
|
|
31
|
-
return { ...
|
|
59
|
+
return { ...fadeConfigs[fadeType] };
|
|
32
60
|
};
|
|
33
61
|
|
|
34
62
|
/**
|
|
@@ -38,7 +66,7 @@ const easingFunctions: Record<EasingType, (t: number) => number> = {
|
|
|
38
66
|
[EasingType.Linear]: (t: number): number => t,
|
|
39
67
|
[EasingType.EaseIn]: (t: number): number => t * t,
|
|
40
68
|
[EasingType.EaseOut]: (t: number): number => t * (2 - t),
|
|
41
|
-
[EasingType.EaseInOut]: (t: number): number => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
|
|
69
|
+
[EasingType.EaseInOut]: (t: number): number => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)
|
|
42
70
|
};
|
|
43
71
|
|
|
44
72
|
/**
|
|
@@ -65,11 +93,24 @@ export const transitionVolume = async (
|
|
|
65
93
|
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
66
94
|
const startVolume: number = currentAudio.volume;
|
|
67
95
|
const volumeDelta: number = targetVolume - startVolume;
|
|
68
|
-
|
|
96
|
+
|
|
69
97
|
// Cancel any existing transition for this channel
|
|
70
98
|
if (activeTransitions.has(channelNumber)) {
|
|
71
|
-
|
|
99
|
+
const transitionId = activeTransitions.get(channelNumber);
|
|
100
|
+
const timerType = timerTypes.get(channelNumber);
|
|
101
|
+
if (transitionId) {
|
|
102
|
+
// Cancel based on the timer type that was actually used
|
|
103
|
+
if (
|
|
104
|
+
timerType === TimerType.RequestAnimationFrame &&
|
|
105
|
+
typeof cancelAnimationFrame !== 'undefined'
|
|
106
|
+
) {
|
|
107
|
+
cancelAnimationFrame(transitionId);
|
|
108
|
+
} else if (timerType === TimerType.Timeout) {
|
|
109
|
+
clearTimeout(transitionId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
72
112
|
activeTransitions.delete(channelNumber);
|
|
113
|
+
timerTypes.delete(channelNumber);
|
|
73
114
|
}
|
|
74
115
|
|
|
75
116
|
// If no change needed, resolve immediately
|
|
@@ -78,8 +119,8 @@ export const transitionVolume = async (
|
|
|
78
119
|
return Promise.resolve();
|
|
79
120
|
}
|
|
80
121
|
|
|
81
|
-
// Handle zero duration - instant change
|
|
82
|
-
if (duration
|
|
122
|
+
// Handle zero or negative duration - instant change
|
|
123
|
+
if (duration <= 0) {
|
|
83
124
|
channel.volume = targetVolume;
|
|
84
125
|
if (channel.queue.length > 0) {
|
|
85
126
|
channel.queue[0].volume = targetVolume;
|
|
@@ -95,10 +136,10 @@ export const transitionVolume = async (
|
|
|
95
136
|
const elapsed: number = performance.now() - startTime;
|
|
96
137
|
const progress: number = Math.min(elapsed / duration, 1);
|
|
97
138
|
const easedProgress: number = easingFn(progress);
|
|
98
|
-
|
|
99
|
-
const currentVolume: number = startVolume +
|
|
139
|
+
|
|
140
|
+
const currentVolume: number = startVolume + volumeDelta * easedProgress;
|
|
100
141
|
const clampedVolume: number = Math.max(0, Math.min(1, currentVolume));
|
|
101
|
-
|
|
142
|
+
|
|
102
143
|
// Apply volume to both channel config and current audio
|
|
103
144
|
channel.volume = clampedVolume;
|
|
104
145
|
if (channel.queue.length > 0) {
|
|
@@ -108,16 +149,19 @@ export const transitionVolume = async (
|
|
|
108
149
|
if (progress >= 1) {
|
|
109
150
|
// Transition complete
|
|
110
151
|
activeTransitions.delete(channelNumber);
|
|
152
|
+
timerTypes.delete(channelNumber);
|
|
111
153
|
resolve();
|
|
112
154
|
} else {
|
|
113
155
|
// Use requestAnimationFrame in browser, setTimeout in tests
|
|
114
156
|
if (typeof requestAnimationFrame !== 'undefined') {
|
|
115
157
|
const rafId = requestAnimationFrame(updateVolume);
|
|
116
158
|
activeTransitions.set(channelNumber, rafId as unknown as number);
|
|
159
|
+
timerTypes.set(channelNumber, TimerType.RequestAnimationFrame);
|
|
117
160
|
} else {
|
|
118
161
|
// In test environment, use shorter intervals
|
|
119
162
|
const timeoutId = setTimeout(updateVolume, 1);
|
|
120
163
|
activeTransitions.set(channelNumber, timeoutId as unknown as number);
|
|
164
|
+
timerTypes.set(channelNumber, TimerType.Timeout);
|
|
121
165
|
}
|
|
122
166
|
}
|
|
123
167
|
};
|
|
@@ -132,6 +176,7 @@ export const transitionVolume = async (
|
|
|
132
176
|
* @param volume - Volume level (0-1)
|
|
133
177
|
* @param transitionDuration - Optional transition duration in milliseconds
|
|
134
178
|
* @param easing - Optional easing function
|
|
179
|
+
* @throws Error if the channel number exceeds the maximum allowed channels
|
|
135
180
|
* @example
|
|
136
181
|
* ```typescript
|
|
137
182
|
* setChannelVolume(0, 0.5); // Set channel 0 to 50%
|
|
@@ -139,13 +184,23 @@ export const transitionVolume = async (
|
|
|
139
184
|
* ```
|
|
140
185
|
*/
|
|
141
186
|
export const setChannelVolume = async (
|
|
142
|
-
channelNumber: number,
|
|
187
|
+
channelNumber: number,
|
|
143
188
|
volume: number,
|
|
144
189
|
transitionDuration?: number,
|
|
145
190
|
easing?: EasingType
|
|
146
191
|
): Promise<void> => {
|
|
147
192
|
const clampedVolume: number = Math.max(0, Math.min(1, volume));
|
|
148
|
-
|
|
193
|
+
|
|
194
|
+
// Check channel number limits
|
|
195
|
+
if (channelNumber < 0) {
|
|
196
|
+
throw new Error('Channel number must be non-negative');
|
|
197
|
+
}
|
|
198
|
+
if (channelNumber >= MAX_CHANNELS) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Channel number ${channelNumber} exceeds maximum allowed channels (${MAX_CHANNELS})`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
149
204
|
if (!audioChannels[channelNumber]) {
|
|
150
205
|
audioChannels[channelNumber] = {
|
|
151
206
|
audioCompleteCallbacks: new Set(),
|
|
@@ -189,7 +244,7 @@ export const setChannelVolume = async (
|
|
|
189
244
|
*/
|
|
190
245
|
export const getChannelVolume = (channelNumber: number = 0): number => {
|
|
191
246
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
192
|
-
return channel?.volume
|
|
247
|
+
return channel?.volume ?? 1.0;
|
|
193
248
|
};
|
|
194
249
|
|
|
195
250
|
/**
|
|
@@ -204,9 +259,7 @@ export const getChannelVolume = (channelNumber: number = 0): number => {
|
|
|
204
259
|
* ```
|
|
205
260
|
*/
|
|
206
261
|
export const getAllChannelsVolume = (): number[] => {
|
|
207
|
-
return audioChannels.map((channel: ExtendedAudioQueueChannel) =>
|
|
208
|
-
channel?.volume || 1.0
|
|
209
|
-
);
|
|
262
|
+
return audioChannels.map((channel: ExtendedAudioQueueChannel) => channel?.volume ?? 1.0);
|
|
210
263
|
};
|
|
211
264
|
|
|
212
265
|
/**
|
|
@@ -229,6 +282,7 @@ export const setAllChannelsVolume = async (volume: number): Promise<void> => {
|
|
|
229
282
|
* Configures volume ducking for channels. When the priority channel plays audio,
|
|
230
283
|
* all other channels will be automatically reduced to the ducking volume level
|
|
231
284
|
* @param config - Volume ducking configuration
|
|
285
|
+
* @throws Error if the priority channel number exceeds the maximum allowed channels
|
|
232
286
|
* @example
|
|
233
287
|
* ```typescript
|
|
234
288
|
* // When channel 1 plays, reduce all other channels to 20% volume
|
|
@@ -240,8 +294,23 @@ export const setAllChannelsVolume = async (volume: number): Promise<void> => {
|
|
|
240
294
|
* ```
|
|
241
295
|
*/
|
|
242
296
|
export const setVolumeDucking = (config: VolumeConfig): void => {
|
|
243
|
-
|
|
244
|
-
|
|
297
|
+
const { priorityChannel } = config;
|
|
298
|
+
|
|
299
|
+
// Check priority channel limits
|
|
300
|
+
if (priorityChannel < 0) {
|
|
301
|
+
throw new Error('Priority channel number must be non-negative');
|
|
302
|
+
}
|
|
303
|
+
if (priorityChannel >= MAX_CHANNELS) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Priority channel ${priorityChannel} exceeds maximum allowed channels (${MAX_CHANNELS})`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Store the configuration globally
|
|
310
|
+
globalVolumeConfig = config;
|
|
311
|
+
|
|
312
|
+
// Ensure we have enough channels for the priority channel
|
|
313
|
+
while (audioChannels.length <= priorityChannel) {
|
|
245
314
|
audioChannels.push({
|
|
246
315
|
audioCompleteCallbacks: new Set(),
|
|
247
316
|
audioErrorCallbacks: new Set(),
|
|
@@ -255,25 +324,6 @@ export const setVolumeDucking = (config: VolumeConfig): void => {
|
|
|
255
324
|
volume: 1.0
|
|
256
325
|
});
|
|
257
326
|
}
|
|
258
|
-
|
|
259
|
-
// Apply the config to all existing channels
|
|
260
|
-
audioChannels.forEach((channel: ExtendedAudioQueueChannel, index: number) => {
|
|
261
|
-
if (!audioChannels[index]) {
|
|
262
|
-
audioChannels[index] = {
|
|
263
|
-
audioCompleteCallbacks: new Set(),
|
|
264
|
-
audioErrorCallbacks: new Set(),
|
|
265
|
-
audioPauseCallbacks: new Set(),
|
|
266
|
-
audioResumeCallbacks: new Set(),
|
|
267
|
-
audioStartCallbacks: new Set(),
|
|
268
|
-
isPaused: false,
|
|
269
|
-
progressCallbacks: new Map(),
|
|
270
|
-
queue: [],
|
|
271
|
-
queueChangeCallbacks: new Set(),
|
|
272
|
-
volume: 1.0
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
audioChannels[index].volumeConfig = config;
|
|
276
|
-
});
|
|
277
327
|
};
|
|
278
328
|
|
|
279
329
|
/**
|
|
@@ -284,11 +334,7 @@ export const setVolumeDucking = (config: VolumeConfig): void => {
|
|
|
284
334
|
* ```
|
|
285
335
|
*/
|
|
286
336
|
export const clearVolumeDucking = (): void => {
|
|
287
|
-
|
|
288
|
-
if (channel) {
|
|
289
|
-
delete channel.volumeConfig;
|
|
290
|
-
}
|
|
291
|
-
});
|
|
337
|
+
globalVolumeConfig = null;
|
|
292
338
|
};
|
|
293
339
|
|
|
294
340
|
/**
|
|
@@ -297,31 +343,40 @@ export const clearVolumeDucking = (): void => {
|
|
|
297
343
|
* @internal
|
|
298
344
|
*/
|
|
299
345
|
export const applyVolumeDucking = async (activeChannelNumber: number): Promise<void> => {
|
|
346
|
+
// Check if ducking is configured and this channel is the priority channel
|
|
347
|
+
if (!globalVolumeConfig || globalVolumeConfig.priorityChannel !== activeChannelNumber) {
|
|
348
|
+
return; // No ducking configured for this channel
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const config = globalVolumeConfig;
|
|
300
352
|
const transitionPromises: Promise<void>[] = [];
|
|
353
|
+
const duration = config.duckTransitionDuration ?? 250;
|
|
354
|
+
const easing = config.transitionEasing ?? EasingType.EaseOut;
|
|
301
355
|
|
|
356
|
+
// Duck all channels except the priority channel
|
|
302
357
|
audioChannels.forEach((channel: ExtendedAudioQueueChannel, channelNumber: number) => {
|
|
303
|
-
if (channel
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
358
|
+
if (!channel || channel.queue.length === 0) {
|
|
359
|
+
return; // Skip channels without audio
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (channelNumber === activeChannelNumber) {
|
|
363
|
+
// This is the priority channel - set to priority volume
|
|
364
|
+
// Only change audio volume, preserve channel.volume as desired volume
|
|
365
|
+
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
366
|
+
transitionPromises.push(
|
|
367
|
+
transitionAudioVolume(currentAudio, config.priorityVolume, duration, easing)
|
|
368
|
+
);
|
|
369
|
+
} else {
|
|
370
|
+
// This is a background channel - duck it
|
|
371
|
+
// Only change audio volume, preserve channel.volume as desired volume
|
|
372
|
+
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
373
|
+
transitionPromises.push(
|
|
374
|
+
transitionAudioVolume(currentAudio, config.duckingVolume, duration, easing)
|
|
375
|
+
);
|
|
321
376
|
}
|
|
322
377
|
});
|
|
323
378
|
|
|
324
|
-
|
|
379
|
+
// Wait for all transitions to complete
|
|
325
380
|
await Promise.all(transitionPromises);
|
|
326
381
|
};
|
|
327
382
|
|
|
@@ -345,32 +400,146 @@ export const fadeVolume = async (
|
|
|
345
400
|
easing: EasingType = EasingType.EaseOut
|
|
346
401
|
): Promise<void> => {
|
|
347
402
|
return transitionVolume(channelNumber, targetVolume, duration, easing);
|
|
348
|
-
};
|
|
403
|
+
};
|
|
349
404
|
|
|
350
405
|
/**
|
|
351
|
-
* Restores normal volume levels when priority channel
|
|
406
|
+
* Restores normal volume levels when priority channel queue becomes empty
|
|
352
407
|
* @param stoppedChannelNumber - The channel that just stopped playing
|
|
353
408
|
* @internal
|
|
354
409
|
*/
|
|
355
410
|
export const restoreVolumeLevels = async (stoppedChannelNumber: number): Promise<void> => {
|
|
411
|
+
// Check if ducking is configured and this channel is the priority channel
|
|
412
|
+
if (!globalVolumeConfig || globalVolumeConfig.priorityChannel !== stoppedChannelNumber) {
|
|
413
|
+
return; // No ducking configured for this channel
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Check if the priority channel queue is now empty
|
|
417
|
+
const priorityChannel = audioChannels[stoppedChannelNumber];
|
|
418
|
+
if (priorityChannel && priorityChannel.queue.length > 0) {
|
|
419
|
+
return; // Priority channel still has audio queued, don't restore yet
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const config = globalVolumeConfig;
|
|
356
423
|
const transitionPromises: Promise<void>[] = [];
|
|
357
424
|
|
|
425
|
+
// Restore volume for all channels EXCEPT the priority channel
|
|
358
426
|
audioChannels.forEach((channel: ExtendedAudioQueueChannel, channelNumber: number) => {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (stoppedChannelNumber === config.priorityChannel) {
|
|
363
|
-
const duration = config.restoreTransitionDuration || 500;
|
|
364
|
-
const easing = config.transitionEasing || EasingType.EaseOut;
|
|
365
|
-
|
|
366
|
-
// Priority channel stopped, restore normal volumes
|
|
367
|
-
transitionPromises.push(
|
|
368
|
-
transitionVolume(channelNumber, channel.volume || 1.0, duration, easing)
|
|
369
|
-
);
|
|
370
|
-
}
|
|
427
|
+
// Skip the priority channel itself and channels without audio
|
|
428
|
+
if (channelNumber === stoppedChannelNumber || !channel || channel.queue.length === 0) {
|
|
429
|
+
return;
|
|
371
430
|
}
|
|
431
|
+
|
|
432
|
+
// Restore this channel to its desired volume
|
|
433
|
+
const duration = config.restoreTransitionDuration ?? 500;
|
|
434
|
+
const easing = config.transitionEasing ?? EasingType.EaseOut;
|
|
435
|
+
const targetVolume = channel.volume ?? 1.0;
|
|
436
|
+
|
|
437
|
+
// Only transition the audio element volume, keep channel.volume as the desired volume
|
|
438
|
+
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
439
|
+
transitionPromises.push(transitionAudioVolume(currentAudio, targetVolume, duration, easing));
|
|
372
440
|
});
|
|
373
441
|
|
|
374
442
|
// Wait for all transitions to complete
|
|
375
443
|
await Promise.all(transitionPromises);
|
|
376
|
-
};
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Transitions only the audio element volume without affecting channel.volume
|
|
448
|
+
* This is used for ducking/restoration where channel.volume represents desired volume
|
|
449
|
+
* @param audio - The audio element to transition
|
|
450
|
+
* @param targetVolume - Target volume level (0-1)
|
|
451
|
+
* @param duration - Transition duration in milliseconds
|
|
452
|
+
* @param easing - Easing function type
|
|
453
|
+
* @returns Promise that resolves when transition completes
|
|
454
|
+
* @internal
|
|
455
|
+
*/
|
|
456
|
+
const transitionAudioVolume = async (
|
|
457
|
+
audio: HTMLAudioElement,
|
|
458
|
+
targetVolume: number,
|
|
459
|
+
duration: number = 250,
|
|
460
|
+
easing: EasingType = EasingType.EaseOut
|
|
461
|
+
): Promise<void> => {
|
|
462
|
+
const startVolume: number = audio.volume;
|
|
463
|
+
const volumeDelta: number = targetVolume - startVolume;
|
|
464
|
+
|
|
465
|
+
// If no change needed, resolve immediately
|
|
466
|
+
if (Math.abs(volumeDelta) < 0.001) {
|
|
467
|
+
return Promise.resolve();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Handle zero or negative duration - instant change
|
|
471
|
+
if (duration <= 0) {
|
|
472
|
+
audio.volume = Math.max(0, Math.min(1, targetVolume));
|
|
473
|
+
return Promise.resolve();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const startTime: number = performance.now();
|
|
477
|
+
const easingFn = easingFunctions[easing];
|
|
478
|
+
|
|
479
|
+
return new Promise<void>((resolve) => {
|
|
480
|
+
const updateVolume = (): void => {
|
|
481
|
+
const elapsed: number = performance.now() - startTime;
|
|
482
|
+
const progress: number = Math.min(elapsed / duration, 1);
|
|
483
|
+
const easedProgress: number = easingFn(progress);
|
|
484
|
+
|
|
485
|
+
const currentVolume: number = startVolume + volumeDelta * easedProgress;
|
|
486
|
+
const clampedVolume: number = Math.max(0, Math.min(1, currentVolume));
|
|
487
|
+
|
|
488
|
+
// Only apply volume to audio element, not channel.volume
|
|
489
|
+
audio.volume = clampedVolume;
|
|
490
|
+
|
|
491
|
+
if (progress >= 1) {
|
|
492
|
+
resolve();
|
|
493
|
+
} else {
|
|
494
|
+
// Use requestAnimationFrame in browser, setTimeout in tests
|
|
495
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
496
|
+
requestAnimationFrame(updateVolume);
|
|
497
|
+
} else {
|
|
498
|
+
setTimeout(updateVolume, 1);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
updateVolume();
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Cancels any active volume transition for a specific channel
|
|
509
|
+
* @param channelNumber - The channel number to cancel transitions for
|
|
510
|
+
* @internal
|
|
511
|
+
*/
|
|
512
|
+
export const cancelVolumeTransition = (channelNumber: number): void => {
|
|
513
|
+
if (activeTransitions.has(channelNumber)) {
|
|
514
|
+
const transitionId = activeTransitions.get(channelNumber);
|
|
515
|
+
const timerType = timerTypes.get(channelNumber);
|
|
516
|
+
|
|
517
|
+
if (transitionId) {
|
|
518
|
+
// Cancel based on the timer type that was actually used
|
|
519
|
+
if (
|
|
520
|
+
timerType === TimerType.RequestAnimationFrame &&
|
|
521
|
+
typeof cancelAnimationFrame !== 'undefined'
|
|
522
|
+
) {
|
|
523
|
+
cancelAnimationFrame(transitionId);
|
|
524
|
+
} else if (timerType === TimerType.Timeout) {
|
|
525
|
+
clearTimeout(transitionId);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
activeTransitions.delete(channelNumber);
|
|
530
|
+
timerTypes.delete(channelNumber);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Cancels all active volume transitions across all channels
|
|
536
|
+
* @internal
|
|
537
|
+
*/
|
|
538
|
+
export const cancelAllVolumeTransitions = (): void => {
|
|
539
|
+
// Get all active channel numbers to avoid modifying Map while iterating
|
|
540
|
+
const activeChannels = Array.from(activeTransitions.keys());
|
|
541
|
+
|
|
542
|
+
activeChannels.forEach((channelNumber) => {
|
|
543
|
+
cancelVolumeTransition(channelNumber);
|
|
544
|
+
});
|
|
545
|
+
};
|