audio-channel-queue 1.9.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/dist/volume.js CHANGED
@@ -12,11 +12,18 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  });
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.restoreVolumeLevels = exports.fadeVolume = exports.applyVolumeDucking = exports.clearVolumeDucking = exports.setVolumeDucking = exports.setAllChannelsVolume = exports.getAllChannelsVolume = exports.getChannelVolume = exports.setChannelVolume = exports.transitionVolume = exports.getFadeConfig = void 0;
15
+ exports.cancelAllVolumeTransitions = exports.cancelVolumeTransition = exports.restoreVolumeLevels = exports.fadeVolume = exports.applyVolumeDucking = exports.clearVolumeDucking = exports.setVolumeDucking = 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
18
  // Store active volume transitions to handle interruptions
19
19
  const activeTransitions = new Map();
20
+ // Track which timer type was used for each channel
21
+ const timerTypes = new Map();
22
+ /**
23
+ * Global volume ducking configuration
24
+ * Stores the volume ducking settings that apply to all channels
25
+ */
26
+ let globalVolumeConfig = null;
20
27
  /**
21
28
  * Predefined fade configurations for different transition types
22
29
  */
@@ -82,22 +89,27 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
82
89
  // Cancel any existing transition for this channel
83
90
  if (activeTransitions.has(channelNumber)) {
84
91
  const transitionId = activeTransitions.get(channelNumber);
92
+ const timerType = timerTypes.get(channelNumber);
85
93
  if (transitionId) {
86
- // Handle both requestAnimationFrame and setTimeout IDs
87
- if (typeof cancelAnimationFrame !== 'undefined') {
94
+ // Cancel based on the timer type that was actually used
95
+ if (timerType === types_1.TimerType.RequestAnimationFrame &&
96
+ typeof cancelAnimationFrame !== 'undefined') {
88
97
  cancelAnimationFrame(transitionId);
89
98
  }
90
- clearTimeout(transitionId);
99
+ else if (timerType === types_1.TimerType.Timeout) {
100
+ clearTimeout(transitionId);
101
+ }
91
102
  }
92
103
  activeTransitions.delete(channelNumber);
104
+ timerTypes.delete(channelNumber);
93
105
  }
94
106
  // If no change needed, resolve immediately
95
107
  if (Math.abs(volumeDelta) < 0.001) {
96
108
  channel.volume = targetVolume;
97
109
  return Promise.resolve();
98
110
  }
99
- // Handle zero duration - instant change
100
- if (duration === 0) {
111
+ // Handle zero or negative duration - instant change
112
+ if (duration <= 0) {
101
113
  channel.volume = targetVolume;
102
114
  if (channel.queue.length > 0) {
103
115
  channel.queue[0].volume = targetVolume;
@@ -121,6 +133,7 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
121
133
  if (progress >= 1) {
122
134
  // Transition complete
123
135
  activeTransitions.delete(channelNumber);
136
+ timerTypes.delete(channelNumber);
124
137
  resolve();
125
138
  }
126
139
  else {
@@ -128,11 +141,13 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
128
141
  if (typeof requestAnimationFrame !== 'undefined') {
129
142
  const rafId = requestAnimationFrame(updateVolume);
130
143
  activeTransitions.set(channelNumber, rafId);
144
+ timerTypes.set(channelNumber, types_1.TimerType.RequestAnimationFrame);
131
145
  }
132
146
  else {
133
147
  // In test environment, use shorter intervals
134
148
  const timeoutId = setTimeout(updateVolume, 1);
135
149
  activeTransitions.set(channelNumber, timeoutId);
150
+ timerTypes.set(channelNumber, types_1.TimerType.Timeout);
136
151
  }
137
152
  }
138
153
  };
@@ -146,6 +161,7 @@ exports.transitionVolume = transitionVolume;
146
161
  * @param volume - Volume level (0-1)
147
162
  * @param transitionDuration - Optional transition duration in milliseconds
148
163
  * @param easing - Optional easing function
164
+ * @throws Error if the channel number exceeds the maximum allowed channels
149
165
  * @example
150
166
  * ```typescript
151
167
  * setChannelVolume(0, 0.5); // Set channel 0 to 50%
@@ -154,6 +170,13 @@ exports.transitionVolume = transitionVolume;
154
170
  */
155
171
  const setChannelVolume = (channelNumber, volume, transitionDuration, easing) => __awaiter(void 0, void 0, void 0, function* () {
156
172
  const clampedVolume = Math.max(0, Math.min(1, volume));
173
+ // Check channel number limits
174
+ if (channelNumber < 0) {
175
+ throw new Error('Channel number must be non-negative');
176
+ }
177
+ if (channelNumber >= types_1.MAX_CHANNELS) {
178
+ throw new Error(`Channel number ${channelNumber} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
179
+ }
157
180
  if (!info_1.audioChannels[channelNumber]) {
158
181
  info_1.audioChannels[channelNumber] = {
159
182
  audioCompleteCallbacks: new Set(),
@@ -236,6 +259,7 @@ exports.setAllChannelsVolume = setAllChannelsVolume;
236
259
  * Configures volume ducking for channels. When the priority channel plays audio,
237
260
  * all other channels will be automatically reduced to the ducking volume level
238
261
  * @param config - Volume ducking configuration
262
+ * @throws Error if the priority channel number exceeds the maximum allowed channels
239
263
  * @example
240
264
  * ```typescript
241
265
  * // When channel 1 plays, reduce all other channels to 20% volume
@@ -247,8 +271,18 @@ exports.setAllChannelsVolume = setAllChannelsVolume;
247
271
  * ```
248
272
  */
249
273
  const setVolumeDucking = (config) => {
250
- // First, ensure we have enough channels for the priority channel
251
- while (info_1.audioChannels.length <= config.priorityChannel) {
274
+ const { priorityChannel } = config;
275
+ // Check priority channel limits
276
+ if (priorityChannel < 0) {
277
+ throw new Error('Priority channel number must be non-negative');
278
+ }
279
+ if (priorityChannel >= types_1.MAX_CHANNELS) {
280
+ throw new Error(`Priority channel ${priorityChannel} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
281
+ }
282
+ // Store the configuration globally
283
+ globalVolumeConfig = config;
284
+ // Ensure we have enough channels for the priority channel
285
+ while (info_1.audioChannels.length <= priorityChannel) {
252
286
  info_1.audioChannels.push({
253
287
  audioCompleteCallbacks: new Set(),
254
288
  audioErrorCallbacks: new Set(),
@@ -262,24 +296,6 @@ const setVolumeDucking = (config) => {
262
296
  volume: 1.0
263
297
  });
264
298
  }
265
- // Apply the config to all existing channels
266
- info_1.audioChannels.forEach((channel, index) => {
267
- if (!info_1.audioChannels[index]) {
268
- info_1.audioChannels[index] = {
269
- audioCompleteCallbacks: new Set(),
270
- audioErrorCallbacks: new Set(),
271
- audioPauseCallbacks: new Set(),
272
- audioResumeCallbacks: new Set(),
273
- audioStartCallbacks: new Set(),
274
- isPaused: false,
275
- progressCallbacks: new Map(),
276
- queue: [],
277
- queueChangeCallbacks: new Set(),
278
- volume: 1.0
279
- };
280
- }
281
- info_1.audioChannels[index].volumeConfig = config;
282
- });
283
299
  };
284
300
  exports.setVolumeDucking = setVolumeDucking;
285
301
  /**
@@ -290,11 +306,7 @@ exports.setVolumeDucking = setVolumeDucking;
290
306
  * ```
291
307
  */
292
308
  const clearVolumeDucking = () => {
293
- info_1.audioChannels.forEach((channel) => {
294
- if (channel) {
295
- delete channel.volumeConfig;
296
- }
297
- });
309
+ globalVolumeConfig = null;
298
310
  };
299
311
  exports.clearVolumeDucking = clearVolumeDucking;
300
312
  /**
@@ -303,22 +315,31 @@ exports.clearVolumeDucking = clearVolumeDucking;
303
315
  * @internal
304
316
  */
305
317
  const applyVolumeDucking = (activeChannelNumber) => __awaiter(void 0, void 0, void 0, function* () {
318
+ var _a, _b;
319
+ // Check if ducking is configured and this channel is the priority channel
320
+ if (!globalVolumeConfig || globalVolumeConfig.priorityChannel !== activeChannelNumber) {
321
+ return; // No ducking configured for this channel
322
+ }
323
+ const config = globalVolumeConfig;
306
324
  const transitionPromises = [];
325
+ const duration = (_a = config.duckTransitionDuration) !== null && _a !== void 0 ? _a : 250;
326
+ const easing = (_b = config.transitionEasing) !== null && _b !== void 0 ? _b : types_1.EasingType.EaseOut;
327
+ // Duck all channels except the priority channel
307
328
  info_1.audioChannels.forEach((channel, channelNumber) => {
308
- var _a, _b;
309
- if (channel === null || channel === void 0 ? void 0 : channel.volumeConfig) {
310
- const config = channel.volumeConfig;
311
- if (activeChannelNumber === config.priorityChannel) {
312
- const duration = (_a = config.duckTransitionDuration) !== null && _a !== void 0 ? _a : 250;
313
- const easing = (_b = config.transitionEasing) !== null && _b !== void 0 ? _b : types_1.EasingType.EaseOut;
314
- // Priority channel is active, duck other channels
315
- if (channelNumber === config.priorityChannel) {
316
- transitionPromises.push((0, exports.transitionVolume)(channelNumber, config.priorityVolume, duration, easing));
317
- }
318
- else {
319
- transitionPromises.push((0, exports.transitionVolume)(channelNumber, config.duckingVolume, duration, easing));
320
- }
321
- }
329
+ if (!channel || channel.queue.length === 0) {
330
+ return; // Skip channels without audio
331
+ }
332
+ if (channelNumber === activeChannelNumber) {
333
+ // This is the priority channel - set to priority volume
334
+ // Only change audio volume, preserve channel.volume as desired volume
335
+ const currentAudio = channel.queue[0];
336
+ transitionPromises.push(transitionAudioVolume(currentAudio, config.priorityVolume, duration, easing));
337
+ }
338
+ else {
339
+ // This is a background channel - duck it
340
+ // Only change audio volume, preserve channel.volume as desired volume
341
+ const currentAudio = channel.queue[0];
342
+ transitionPromises.push(transitionAudioVolume(currentAudio, config.duckingVolume, duration, easing));
322
343
  }
323
344
  });
324
345
  // Wait for all transitions to complete
@@ -343,25 +364,123 @@ const fadeVolume = (channelNumber_1, targetVolume_1, ...args_1) => __awaiter(voi
343
364
  });
344
365
  exports.fadeVolume = fadeVolume;
345
366
  /**
346
- * Restores normal volume levels when priority channel stops with smooth transitions
367
+ * Restores normal volume levels when priority channel queue becomes empty
347
368
  * @param stoppedChannelNumber - The channel that just stopped playing
348
369
  * @internal
349
370
  */
350
371
  const restoreVolumeLevels = (stoppedChannelNumber) => __awaiter(void 0, void 0, void 0, function* () {
372
+ // Check if ducking is configured and this channel is the priority channel
373
+ if (!globalVolumeConfig || globalVolumeConfig.priorityChannel !== stoppedChannelNumber) {
374
+ return; // No ducking configured for this channel
375
+ }
376
+ // Check if the priority channel queue is now empty
377
+ const priorityChannel = info_1.audioChannels[stoppedChannelNumber];
378
+ if (priorityChannel && priorityChannel.queue.length > 0) {
379
+ return; // Priority channel still has audio queued, don't restore yet
380
+ }
381
+ const config = globalVolumeConfig;
351
382
  const transitionPromises = [];
383
+ // Restore volume for all channels EXCEPT the priority channel
352
384
  info_1.audioChannels.forEach((channel, channelNumber) => {
353
385
  var _a, _b, _c;
354
- if (channel === null || channel === void 0 ? void 0 : channel.volumeConfig) {
355
- const config = channel.volumeConfig;
356
- if (stoppedChannelNumber === config.priorityChannel) {
357
- const duration = (_a = config.restoreTransitionDuration) !== null && _a !== void 0 ? _a : 500;
358
- const easing = (_b = config.transitionEasing) !== null && _b !== void 0 ? _b : types_1.EasingType.EaseOut;
359
- // Priority channel stopped, restore normal volumes
360
- transitionPromises.push((0, exports.transitionVolume)(channelNumber, (_c = channel.volume) !== null && _c !== void 0 ? _c : 1.0, duration, easing));
361
- }
386
+ // Skip the priority channel itself and channels without audio
387
+ if (channelNumber === stoppedChannelNumber || !channel || channel.queue.length === 0) {
388
+ return;
362
389
  }
390
+ // Restore this channel to its desired volume
391
+ const duration = (_a = config.restoreTransitionDuration) !== null && _a !== void 0 ? _a : 500;
392
+ const easing = (_b = config.transitionEasing) !== null && _b !== void 0 ? _b : types_1.EasingType.EaseOut;
393
+ const targetVolume = (_c = channel.volume) !== null && _c !== void 0 ? _c : 1.0;
394
+ // Only transition the audio element volume, keep channel.volume as the desired volume
395
+ const currentAudio = channel.queue[0];
396
+ transitionPromises.push(transitionAudioVolume(currentAudio, targetVolume, duration, easing));
363
397
  });
364
398
  // Wait for all transitions to complete
365
399
  yield Promise.all(transitionPromises);
366
400
  });
367
401
  exports.restoreVolumeLevels = restoreVolumeLevels;
402
+ /**
403
+ * Transitions only the audio element volume without affecting channel.volume
404
+ * This is used for ducking/restoration where channel.volume represents desired volume
405
+ * @param audio - The audio element to transition
406
+ * @param targetVolume - Target volume level (0-1)
407
+ * @param duration - Transition duration in milliseconds
408
+ * @param easing - Easing function type
409
+ * @returns Promise that resolves when transition completes
410
+ * @internal
411
+ */
412
+ 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) {
413
+ const startVolume = audio.volume;
414
+ const volumeDelta = targetVolume - startVolume;
415
+ // If no change needed, resolve immediately
416
+ if (Math.abs(volumeDelta) < 0.001) {
417
+ return Promise.resolve();
418
+ }
419
+ // Handle zero or negative duration - instant change
420
+ if (duration <= 0) {
421
+ audio.volume = Math.max(0, Math.min(1, targetVolume));
422
+ return Promise.resolve();
423
+ }
424
+ const startTime = performance.now();
425
+ const easingFn = easingFunctions[easing];
426
+ return new Promise((resolve) => {
427
+ const updateVolume = () => {
428
+ const elapsed = performance.now() - startTime;
429
+ const progress = Math.min(elapsed / duration, 1);
430
+ const easedProgress = easingFn(progress);
431
+ const currentVolume = startVolume + volumeDelta * easedProgress;
432
+ const clampedVolume = Math.max(0, Math.min(1, currentVolume));
433
+ // Only apply volume to audio element, not channel.volume
434
+ audio.volume = clampedVolume;
435
+ if (progress >= 1) {
436
+ resolve();
437
+ }
438
+ else {
439
+ // Use requestAnimationFrame in browser, setTimeout in tests
440
+ if (typeof requestAnimationFrame !== 'undefined') {
441
+ requestAnimationFrame(updateVolume);
442
+ }
443
+ else {
444
+ setTimeout(updateVolume, 1);
445
+ }
446
+ }
447
+ };
448
+ updateVolume();
449
+ });
450
+ });
451
+ /**
452
+ * Cancels any active volume transition for a specific channel
453
+ * @param channelNumber - The channel number to cancel transitions for
454
+ * @internal
455
+ */
456
+ const cancelVolumeTransition = (channelNumber) => {
457
+ if (activeTransitions.has(channelNumber)) {
458
+ const transitionId = activeTransitions.get(channelNumber);
459
+ const timerType = timerTypes.get(channelNumber);
460
+ if (transitionId) {
461
+ // Cancel based on the timer type that was actually used
462
+ if (timerType === types_1.TimerType.RequestAnimationFrame &&
463
+ typeof cancelAnimationFrame !== 'undefined') {
464
+ cancelAnimationFrame(transitionId);
465
+ }
466
+ else if (timerType === types_1.TimerType.Timeout) {
467
+ clearTimeout(transitionId);
468
+ }
469
+ }
470
+ activeTransitions.delete(channelNumber);
471
+ timerTypes.delete(channelNumber);
472
+ }
473
+ };
474
+ exports.cancelVolumeTransition = cancelVolumeTransition;
475
+ /**
476
+ * Cancels all active volume transitions across all channels
477
+ * @internal
478
+ */
479
+ const cancelAllVolumeTransitions = () => {
480
+ // Get all active channel numbers to avoid modifying Map while iterating
481
+ const activeChannels = Array.from(activeTransitions.keys());
482
+ activeChannels.forEach((channelNumber) => {
483
+ (0, exports.cancelVolumeTransition)(channelNumber);
484
+ });
485
+ };
486
+ exports.cancelAllVolumeTransitions = cancelAllVolumeTransitions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audio-channel-queue",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Allows you to queue audio files to different playback channels.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,7 +17,16 @@
17
17
  "lint": "npx eslint src/**/*.ts __tests__/**/*.ts",
18
18
  "lint:fix": "npx eslint src/**/*.ts __tests__/**/*.ts --fix",
19
19
  "format": "npx prettier --write src/**/*.ts __tests__/**/*.ts",
20
- "format:check": "npx prettier --check src/**/*.ts __tests__/**/*.ts"
20
+ "format:check": "npx prettier --check src/**/*.ts __tests__/**/*.ts",
21
+ "clean": "rimraf dist",
22
+ "prebuild": "npm run clean",
23
+ "prepack": "npm run validate && npm run build",
24
+ "validate": "npm run format:check && npm run lint && npm run test",
25
+ "publish:patch": "npm version patch && npm run publish:release",
26
+ "publish:minor": "npm version minor && npm run publish:release",
27
+ "publish:major": "npm version major && npm run publish:release",
28
+ "publish:release": "npm run validate && npm run build && npm publish",
29
+ "publish:dry": "npm run validate && npm run build && npm publish --dry-run"
21
30
  },
22
31
  "repository": {
23
32
  "type": "git",
@@ -42,6 +51,7 @@
42
51
  "@types/jest": "^29.5.13",
43
52
  "jest": "^29.7.0",
44
53
  "jest-environment-jsdom": "^29.7.0",
54
+ "rimraf": "^6.0.1",
45
55
  "ts-jest": "^29.2.5",
46
56
  "typescript": "^5.6.2",
47
57
  "typescript-eslint": "^8.34.1"