audio-channel-queue 1.6.0 → 1.8.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/volume.ts CHANGED
@@ -1,328 +1,376 @@
1
- /**
2
- * @fileoverview Volume management functions for the audio-channel-queue package
3
- */
4
-
5
- import { ExtendedAudioQueueChannel, VolumeConfig } from './types';
6
- import { audioChannels } from './info';
7
-
8
- // Store active volume transitions to handle interruptions
9
- const activeTransitions = new Map<number, number>();
10
-
11
- /**
12
- * Easing functions for smooth volume transitions
13
- */
14
- const easingFunctions = {
15
- linear: (t: number): number => t,
16
- 'ease-in': (t: number): number => t * t,
17
- 'ease-out': (t: number): number => t * (2 - t),
18
- 'ease-in-out': (t: number): number => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
19
- };
20
-
21
- /**
22
- * Smoothly transitions volume for a specific channel over time
23
- * @param channelNumber - The channel number to transition
24
- * @param targetVolume - Target volume level (0-1)
25
- * @param duration - Transition duration in milliseconds
26
- * @param easing - Easing function type
27
- * @returns Promise that resolves when transition completes
28
- * @example
29
- * ```typescript
30
- * await transitionVolume(0, 0.2, 500, 'ease-out'); // Duck to 20% over 500ms
31
- * ```
32
- */
33
- export const transitionVolume = async (
34
- channelNumber: number,
35
- targetVolume: number,
36
- duration: number = 250,
37
- easing: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' = 'ease-out'
38
- ): Promise<void> => {
39
- const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
40
- if (!channel || channel.queue.length === 0) return;
41
-
42
- const currentAudio: HTMLAudioElement = channel.queue[0];
43
- const startVolume: number = currentAudio.volume;
44
- const volumeDelta: number = targetVolume - startVolume;
45
-
46
- // Cancel any existing transition for this channel
47
- if (activeTransitions.has(channelNumber)) {
48
- clearTimeout(activeTransitions.get(channelNumber));
49
- activeTransitions.delete(channelNumber);
50
- }
51
-
52
- // If no change needed, resolve immediately
53
- if (Math.abs(volumeDelta) < 0.001) {
54
- channel.volume = targetVolume;
55
- return Promise.resolve();
56
- }
57
-
58
- // Handle zero duration - instant change
59
- if (duration <= 0) {
60
- channel.volume = targetVolume;
61
- if (channel.queue.length > 0) {
62
- channel.queue[0].volume = targetVolume;
63
- }
64
- return Promise.resolve();
65
- }
66
-
67
- const startTime: number = performance.now();
68
- const easingFn = easingFunctions[easing];
69
-
70
- return new Promise<void>((resolve) => {
71
- const updateVolume = (): void => {
72
- const elapsed: number = performance.now() - startTime;
73
- const progress: number = Math.min(elapsed / duration, 1);
74
- const easedProgress: number = easingFn(progress);
75
-
76
- const currentVolume: number = startVolume + (volumeDelta * easedProgress);
77
- const clampedVolume: number = Math.max(0, Math.min(1, currentVolume));
78
-
79
- // Apply volume to both channel config and current audio
80
- channel.volume = clampedVolume;
81
- if (channel.queue.length > 0) {
82
- channel.queue[0].volume = clampedVolume;
83
- }
84
-
85
- if (progress >= 1) {
86
- // Transition complete
87
- activeTransitions.delete(channelNumber);
88
- resolve();
89
- } else {
90
- // Use requestAnimationFrame in browser, setTimeout in tests
91
- if (typeof requestAnimationFrame !== 'undefined') {
92
- const rafId = requestAnimationFrame(updateVolume);
93
- activeTransitions.set(channelNumber, rafId as unknown as number);
94
- } else {
95
- // In test environment, use shorter intervals
96
- const timeoutId = setTimeout(updateVolume, 1);
97
- activeTransitions.set(channelNumber, timeoutId as unknown as number);
98
- }
99
- }
100
- };
101
-
102
- updateVolume();
103
- });
104
- };
105
-
106
- /**
107
- * Sets the volume for a specific channel with optional smooth transition
108
- * @param channelNumber - The channel number to set volume for
109
- * @param volume - Volume level (0-1)
110
- * @param transitionDuration - Optional transition duration in milliseconds
111
- * @param easing - Optional easing function
112
- * @example
113
- * ```typescript
114
- * setChannelVolume(0, 0.5); // Set channel 0 to 50%
115
- * setChannelVolume(0, 0.5, 300, 'ease-out'); // Smooth transition over 300ms
116
- * ```
117
- */
118
- export const setChannelVolume = async (
119
- channelNumber: number,
120
- volume: number,
121
- transitionDuration?: number,
122
- easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'
123
- ): Promise<void> => {
124
- const clampedVolume: number = Math.max(0, Math.min(1, volume));
125
-
126
- if (!audioChannels[channelNumber]) {
127
- audioChannels[channelNumber] = {
128
- audioCompleteCallbacks: new Set(),
129
- audioPauseCallbacks: new Set(),
130
- audioResumeCallbacks: new Set(),
131
- audioStartCallbacks: new Set(),
132
- isPaused: false,
133
- progressCallbacks: new Map(),
134
- queue: [],
135
- queueChangeCallbacks: new Set(),
136
- volume: clampedVolume
137
- };
138
- return;
139
- }
140
-
141
- if (transitionDuration && transitionDuration > 0) {
142
- // Smooth transition
143
- await transitionVolume(channelNumber, clampedVolume, transitionDuration, easing);
144
- } else {
145
- // Instant change (backward compatibility)
146
- audioChannels[channelNumber].volume = clampedVolume;
147
- const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
148
- if (channel.queue.length > 0) {
149
- const currentAudio: HTMLAudioElement = channel.queue[0];
150
- currentAudio.volume = clampedVolume;
151
- }
152
- }
153
- };
154
-
155
- /**
156
- * Gets the current volume for a specific channel
157
- * @param channelNumber - The channel number to get volume for (defaults to 0)
158
- * @returns Current volume level (0-1) or 1.0 if channel doesn't exist
159
- * @example
160
- * ```typescript
161
- * const volume = getChannelVolume(0);
162
- * const defaultChannelVolume = getChannelVolume(); // Gets channel 0
163
- * console.log(`Channel 0 volume: ${volume * 100}%`);
164
- * ```
165
- */
166
- export const getChannelVolume = (channelNumber: number = 0): number => {
167
- const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
168
- return channel?.volume || 1.0;
169
- };
170
-
171
- /**
172
- * Gets the volume levels for all channels
173
- * @returns Array of volume levels (0-1) for each channel
174
- * @example
175
- * ```typescript
176
- * const volumes = getAllChannelsVolume();
177
- * volumes.forEach((volume, index) => {
178
- * console.log(`Channel ${index}: ${volume * 100}%`);
179
- * });
180
- * ```
181
- */
182
- export const getAllChannelsVolume = (): number[] => {
183
- return audioChannels.map((channel: ExtendedAudioQueueChannel) =>
184
- channel?.volume || 1.0
185
- );
186
- };
187
-
188
- /**
189
- * Sets volume for all channels to the same level
190
- * @param volume - Volume level (0-1) to apply to all channels
191
- * @example
192
- * ```typescript
193
- * await setAllChannelsVolume(0.6); // Set all channels to 60% volume
194
- * ```
195
- */
196
- export const setAllChannelsVolume = async (volume: number): Promise<void> => {
197
- const promises: Promise<void>[] = [];
198
- audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
199
- promises.push(setChannelVolume(index, volume));
200
- });
201
- await Promise.all(promises);
202
- };
203
-
204
- /**
205
- * Configures volume ducking for channels. When the priority channel plays audio,
206
- * all other channels will be automatically reduced to the ducking volume level
207
- * @param config - Volume ducking configuration
208
- * @example
209
- * ```typescript
210
- * // When channel 1 plays, reduce all other channels to 20% volume
211
- * setVolumeDucking({
212
- * priorityChannel: 1,
213
- * priorityVolume: 1.0,
214
- * duckingVolume: 0.2
215
- * });
216
- * ```
217
- */
218
- export const setVolumeDucking = (config: VolumeConfig): void => {
219
- // First, ensure we have enough channels for the priority channel
220
- while (audioChannels.length <= config.priorityChannel) {
221
- audioChannels.push({
222
- audioCompleteCallbacks: new Set(),
223
- audioPauseCallbacks: new Set(),
224
- audioResumeCallbacks: new Set(),
225
- audioStartCallbacks: new Set(),
226
- isPaused: false,
227
- progressCallbacks: new Map(),
228
- queue: [],
229
- queueChangeCallbacks: new Set(),
230
- volume: 1.0
231
- });
232
- }
233
-
234
- // Apply the config to all existing channels
235
- audioChannels.forEach((channel: ExtendedAudioQueueChannel, index: number) => {
236
- if (!audioChannels[index]) {
237
- audioChannels[index] = {
238
- audioCompleteCallbacks: new Set(),
239
- audioPauseCallbacks: new Set(),
240
- audioResumeCallbacks: new Set(),
241
- audioStartCallbacks: new Set(),
242
- isPaused: false,
243
- progressCallbacks: new Map(),
244
- queue: [],
245
- queueChangeCallbacks: new Set(),
246
- volume: 1.0
247
- };
248
- }
249
- audioChannels[index].volumeConfig = config;
250
- });
251
- };
252
-
253
- /**
254
- * Removes volume ducking configuration from all channels
255
- * @example
256
- * ```typescript
257
- * clearVolumeDucking(); // Remove all volume ducking effects
258
- * ```
259
- */
260
- export const clearVolumeDucking = (): void => {
261
- audioChannels.forEach((channel: ExtendedAudioQueueChannel) => {
262
- if (channel) {
263
- delete channel.volumeConfig;
264
- }
265
- });
266
- };
267
-
268
- /**
269
- * Applies volume ducking effects based on current playback state with smooth transitions
270
- * @param activeChannelNumber - The channel that just started playing
271
- * @internal
272
- */
273
- export const applyVolumeDucking = async (activeChannelNumber: number): Promise<void> => {
274
- const transitionPromises: Promise<void>[] = [];
275
-
276
- audioChannels.forEach((channel: ExtendedAudioQueueChannel, channelNumber: number) => {
277
- if (channel?.volumeConfig) {
278
- const config: VolumeConfig = channel.volumeConfig;
279
-
280
- if (activeChannelNumber === config.priorityChannel) {
281
- const duration = config.duckTransitionDuration || 250;
282
- const easing = config.transitionEasing || 'ease-out';
283
-
284
- // Priority channel is active, duck other channels
285
- if (channelNumber === config.priorityChannel) {
286
- transitionPromises.push(
287
- transitionVolume(channelNumber, config.priorityVolume, duration, easing)
288
- );
289
- } else {
290
- transitionPromises.push(
291
- transitionVolume(channelNumber, config.duckingVolume, duration, easing)
292
- );
293
- }
294
- }
295
- }
296
- });
297
-
298
- // Wait for all transitions to complete
299
- await Promise.all(transitionPromises);
300
- };
301
-
302
- /**
303
- * Restores normal volume levels when priority channel stops with smooth transitions
304
- * @param stoppedChannelNumber - The channel that just stopped playing
305
- * @internal
306
- */
307
- export const restoreVolumeLevels = async (stoppedChannelNumber: number): Promise<void> => {
308
- const transitionPromises: Promise<void>[] = [];
309
-
310
- audioChannels.forEach((channel: ExtendedAudioQueueChannel, channelNumber: number) => {
311
- if (channel?.volumeConfig) {
312
- const config: VolumeConfig = channel.volumeConfig;
313
-
314
- if (stoppedChannelNumber === config.priorityChannel) {
315
- const duration = config.restoreTransitionDuration || 500;
316
- const easing = config.transitionEasing || 'ease-out';
317
-
318
- // Priority channel stopped, restore normal volumes
319
- transitionPromises.push(
320
- transitionVolume(channelNumber, channel.volume || 1.0, duration, easing)
321
- );
322
- }
323
- }
324
- });
325
-
326
- // Wait for all transitions to complete
327
- await Promise.all(transitionPromises);
1
+ /**
2
+ * @fileoverview Volume management functions for the audio-channel-queue package
3
+ */
4
+
5
+ import { ExtendedAudioQueueChannel, VolumeConfig, FadeType, FadeConfig, EasingType } from './types';
6
+ import { audioChannels } from './info';
7
+
8
+ // Store active volume transitions to handle interruptions
9
+ const activeTransitions = new Map<number, number>();
10
+
11
+ /**
12
+ * Predefined fade configurations for different transition types
13
+ */
14
+ const FADE_CONFIGS: Record<FadeType, FadeConfig> = {
15
+ [FadeType.Dramatic]: { duration: 800, pauseCurve: EasingType.EaseIn, resumeCurve: EasingType.EaseOut },
16
+ [FadeType.Gentle]: { duration: 800, pauseCurve: EasingType.EaseOut, resumeCurve: EasingType.EaseIn },
17
+ [FadeType.Linear]: { duration: 800, pauseCurve: EasingType.Linear, resumeCurve: EasingType.Linear }
18
+ };
19
+
20
+ /**
21
+ * Gets the fade configuration for a specific fade type
22
+ * @param fadeType - The fade type to get configuration for
23
+ * @returns Fade configuration object
24
+ * @example
25
+ * ```typescript
26
+ * const config = getFadeConfig('gentle');
27
+ * console.log(`Gentle fade duration: ${config.duration}ms`);
28
+ * ```
29
+ */
30
+ export const getFadeConfig = (fadeType: FadeType): FadeConfig => {
31
+ return { ...FADE_CONFIGS[fadeType] };
32
+ };
33
+
34
+ /**
35
+ * Easing functions for smooth volume transitions
36
+ */
37
+ const easingFunctions: Record<EasingType, (t: number) => number> = {
38
+ [EasingType.Linear]: (t: number): number => t,
39
+ [EasingType.EaseIn]: (t: number): number => t * t,
40
+ [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
42
+ };
43
+
44
+ /**
45
+ * Smoothly transitions volume for a specific channel over time
46
+ * @param channelNumber - The channel number to transition
47
+ * @param targetVolume - Target volume level (0-1)
48
+ * @param duration - Transition duration in milliseconds
49
+ * @param easing - Easing function type
50
+ * @returns Promise that resolves when transition completes
51
+ * @example
52
+ * ```typescript
53
+ * await transitionVolume(0, 0.2, 500, 'ease-out'); // Duck to 20% over 500ms
54
+ * ```
55
+ */
56
+ export const transitionVolume = async (
57
+ channelNumber: number,
58
+ targetVolume: number,
59
+ duration: number = 250,
60
+ easing: EasingType = EasingType.EaseOut
61
+ ): Promise<void> => {
62
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
63
+ if (!channel || channel.queue.length === 0) return;
64
+
65
+ const currentAudio: HTMLAudioElement = channel.queue[0];
66
+ const startVolume: number = currentAudio.volume;
67
+ const volumeDelta: number = targetVolume - startVolume;
68
+
69
+ // Cancel any existing transition for this channel
70
+ if (activeTransitions.has(channelNumber)) {
71
+ clearTimeout(activeTransitions.get(channelNumber));
72
+ activeTransitions.delete(channelNumber);
73
+ }
74
+
75
+ // If no change needed, resolve immediately
76
+ if (Math.abs(volumeDelta) < 0.001) {
77
+ channel.volume = targetVolume;
78
+ return Promise.resolve();
79
+ }
80
+
81
+ // Handle zero duration - instant change
82
+ if (duration === 0) {
83
+ channel.volume = targetVolume;
84
+ if (channel.queue.length > 0) {
85
+ channel.queue[0].volume = targetVolume;
86
+ }
87
+ return Promise.resolve();
88
+ }
89
+
90
+ const startTime: number = performance.now();
91
+ const easingFn = easingFunctions[easing];
92
+
93
+ return new Promise<void>((resolve) => {
94
+ const updateVolume = (): void => {
95
+ const elapsed: number = performance.now() - startTime;
96
+ const progress: number = Math.min(elapsed / duration, 1);
97
+ const easedProgress: number = easingFn(progress);
98
+
99
+ const currentVolume: number = startVolume + (volumeDelta * easedProgress);
100
+ const clampedVolume: number = Math.max(0, Math.min(1, currentVolume));
101
+
102
+ // Apply volume to both channel config and current audio
103
+ channel.volume = clampedVolume;
104
+ if (channel.queue.length > 0) {
105
+ channel.queue[0].volume = clampedVolume;
106
+ }
107
+
108
+ if (progress >= 1) {
109
+ // Transition complete
110
+ activeTransitions.delete(channelNumber);
111
+ resolve();
112
+ } else {
113
+ // Use requestAnimationFrame in browser, setTimeout in tests
114
+ if (typeof requestAnimationFrame !== 'undefined') {
115
+ const rafId = requestAnimationFrame(updateVolume);
116
+ activeTransitions.set(channelNumber, rafId as unknown as number);
117
+ } else {
118
+ // In test environment, use shorter intervals
119
+ const timeoutId = setTimeout(updateVolume, 1);
120
+ activeTransitions.set(channelNumber, timeoutId as unknown as number);
121
+ }
122
+ }
123
+ };
124
+
125
+ updateVolume();
126
+ });
127
+ };
128
+
129
+ /**
130
+ * Sets the volume for a specific channel with optional smooth transition
131
+ * @param channelNumber - The channel number to set volume for
132
+ * @param volume - Volume level (0-1)
133
+ * @param transitionDuration - Optional transition duration in milliseconds
134
+ * @param easing - Optional easing function
135
+ * @example
136
+ * ```typescript
137
+ * setChannelVolume(0, 0.5); // Set channel 0 to 50%
138
+ * setChannelVolume(0, 0.5, 300, 'ease-out'); // Smooth transition over 300ms
139
+ * ```
140
+ */
141
+ export const setChannelVolume = async (
142
+ channelNumber: number,
143
+ volume: number,
144
+ transitionDuration?: number,
145
+ easing?: EasingType
146
+ ): Promise<void> => {
147
+ const clampedVolume: number = Math.max(0, Math.min(1, volume));
148
+
149
+ if (!audioChannels[channelNumber]) {
150
+ audioChannels[channelNumber] = {
151
+ audioCompleteCallbacks: new Set(),
152
+ audioErrorCallbacks: new Set(),
153
+ audioPauseCallbacks: new Set(),
154
+ audioResumeCallbacks: new Set(),
155
+ audioStartCallbacks: new Set(),
156
+ isPaused: false,
157
+ progressCallbacks: new Map(),
158
+ queue: [],
159
+ queueChangeCallbacks: new Set(),
160
+ volume: clampedVolume
161
+ };
162
+ return;
163
+ }
164
+
165
+ if (transitionDuration && transitionDuration > 0) {
166
+ // Smooth transition
167
+ await transitionVolume(channelNumber, clampedVolume, transitionDuration, easing);
168
+ } else {
169
+ // Instant change (backward compatibility)
170
+ audioChannels[channelNumber].volume = clampedVolume;
171
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
172
+ if (channel.queue.length > 0) {
173
+ const currentAudio: HTMLAudioElement = channel.queue[0];
174
+ currentAudio.volume = clampedVolume;
175
+ }
176
+ }
177
+ };
178
+
179
+ /**
180
+ * Gets the current volume for a specific channel
181
+ * @param channelNumber - The channel number to get volume for (defaults to 0)
182
+ * @returns Current volume level (0-1) or 1.0 if channel doesn't exist
183
+ * @example
184
+ * ```typescript
185
+ * const volume = getChannelVolume(0);
186
+ * const defaultChannelVolume = getChannelVolume(); // Gets channel 0
187
+ * console.log(`Channel 0 volume: ${volume * 100}%`);
188
+ * ```
189
+ */
190
+ export const getChannelVolume = (channelNumber: number = 0): number => {
191
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
192
+ return channel?.volume || 1.0;
193
+ };
194
+
195
+ /**
196
+ * Gets the volume levels for all channels
197
+ * @returns Array of volume levels (0-1) for each channel
198
+ * @example
199
+ * ```typescript
200
+ * const volumes = getAllChannelsVolume();
201
+ * volumes.forEach((volume, index) => {
202
+ * console.log(`Channel ${index}: ${volume * 100}%`);
203
+ * });
204
+ * ```
205
+ */
206
+ export const getAllChannelsVolume = (): number[] => {
207
+ return audioChannels.map((channel: ExtendedAudioQueueChannel) =>
208
+ channel?.volume || 1.0
209
+ );
210
+ };
211
+
212
+ /**
213
+ * Sets volume for all channels to the same level
214
+ * @param volume - Volume level (0-1) to apply to all channels
215
+ * @example
216
+ * ```typescript
217
+ * await setAllChannelsVolume(0.6); // Set all channels to 60% volume
218
+ * ```
219
+ */
220
+ export const setAllChannelsVolume = async (volume: number): Promise<void> => {
221
+ const promises: Promise<void>[] = [];
222
+ audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
223
+ promises.push(setChannelVolume(index, volume));
224
+ });
225
+ await Promise.all(promises);
226
+ };
227
+
228
+ /**
229
+ * Configures volume ducking for channels. When the priority channel plays audio,
230
+ * all other channels will be automatically reduced to the ducking volume level
231
+ * @param config - Volume ducking configuration
232
+ * @example
233
+ * ```typescript
234
+ * // When channel 1 plays, reduce all other channels to 20% volume
235
+ * setVolumeDucking({
236
+ * priorityChannel: 1,
237
+ * priorityVolume: 1.0,
238
+ * duckingVolume: 0.2
239
+ * });
240
+ * ```
241
+ */
242
+ export const setVolumeDucking = (config: VolumeConfig): void => {
243
+ // First, ensure we have enough channels for the priority channel
244
+ while (audioChannels.length <= config.priorityChannel) {
245
+ audioChannels.push({
246
+ audioCompleteCallbacks: new Set(),
247
+ audioErrorCallbacks: new Set(),
248
+ audioPauseCallbacks: new Set(),
249
+ audioResumeCallbacks: new Set(),
250
+ audioStartCallbacks: new Set(),
251
+ isPaused: false,
252
+ progressCallbacks: new Map(),
253
+ queue: [],
254
+ queueChangeCallbacks: new Set(),
255
+ volume: 1.0
256
+ });
257
+ }
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
+ };
278
+
279
+ /**
280
+ * Removes volume ducking configuration from all channels
281
+ * @example
282
+ * ```typescript
283
+ * clearVolumeDucking(); // Remove all volume ducking effects
284
+ * ```
285
+ */
286
+ export const clearVolumeDucking = (): void => {
287
+ audioChannels.forEach((channel: ExtendedAudioQueueChannel) => {
288
+ if (channel) {
289
+ delete channel.volumeConfig;
290
+ }
291
+ });
292
+ };
293
+
294
+ /**
295
+ * Applies volume ducking effects based on current playback state with smooth transitions
296
+ * @param activeChannelNumber - The channel that just started playing
297
+ * @internal
298
+ */
299
+ export const applyVolumeDucking = async (activeChannelNumber: number): Promise<void> => {
300
+ const transitionPromises: Promise<void>[] = [];
301
+
302
+ audioChannels.forEach((channel: ExtendedAudioQueueChannel, channelNumber: number) => {
303
+ if (channel?.volumeConfig) {
304
+ const config: VolumeConfig = channel.volumeConfig;
305
+
306
+ if (activeChannelNumber === config.priorityChannel) {
307
+ const duration = config.duckTransitionDuration || 250;
308
+ const easing = config.transitionEasing || EasingType.EaseOut;
309
+
310
+ // Priority channel is active, duck other channels
311
+ if (channelNumber === config.priorityChannel) {
312
+ transitionPromises.push(
313
+ transitionVolume(channelNumber, config.priorityVolume, duration, easing)
314
+ );
315
+ } else {
316
+ transitionPromises.push(
317
+ transitionVolume(channelNumber, config.duckingVolume, duration, easing)
318
+ );
319
+ }
320
+ }
321
+ }
322
+ });
323
+
324
+ // Wait for all transitions to complete
325
+ await Promise.all(transitionPromises);
326
+ };
327
+
328
+ /**
329
+ * Fades the volume for a specific channel over time (alias for transitionVolume with improved naming)
330
+ * @param channelNumber - The channel number to fade
331
+ * @param targetVolume - Target volume level (0-1)
332
+ * @param duration - Fade duration in milliseconds (defaults to 250)
333
+ * @param easing - Easing function type (defaults to 'ease-out')
334
+ * @returns Promise that resolves when fade completes
335
+ * @example
336
+ * ```typescript
337
+ * await fadeVolume(0, 0, 800, 'ease-in'); // Fade out over 800ms
338
+ * await fadeVolume(0, 1, 600, 'ease-out'); // Fade in over 600ms
339
+ * ```
340
+ */
341
+ export const fadeVolume = async (
342
+ channelNumber: number,
343
+ targetVolume: number,
344
+ duration: number = 250,
345
+ easing: EasingType = EasingType.EaseOut
346
+ ): Promise<void> => {
347
+ return transitionVolume(channelNumber, targetVolume, duration, easing);
348
+ };
349
+
350
+ /**
351
+ * Restores normal volume levels when priority channel stops with smooth transitions
352
+ * @param stoppedChannelNumber - The channel that just stopped playing
353
+ * @internal
354
+ */
355
+ export const restoreVolumeLevels = async (stoppedChannelNumber: number): Promise<void> => {
356
+ const transitionPromises: Promise<void>[] = [];
357
+
358
+ audioChannels.forEach((channel: ExtendedAudioQueueChannel, channelNumber: number) => {
359
+ if (channel?.volumeConfig) {
360
+ const config: VolumeConfig = channel.volumeConfig;
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
+ }
371
+ }
372
+ });
373
+
374
+ // Wait for all transitions to complete
375
+ await Promise.all(transitionPromises);
328
376
  };