@kattowo/distube 6.0.2 → 6.2.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/index.mjs CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/constant.ts
5
- var version = "6.0.2";
5
+ var version = "6.2.0";
6
6
  var AUDIO_SAMPLE_RATE = 48e3;
7
7
  var AUDIO_CHANNELS = 2;
8
8
  var DEFAULT_VOLUME = 50;
@@ -29,6 +29,12 @@ var defaultFilters = {
29
29
  surround: "surround",
30
30
  earwax: "earwax"
31
31
  };
32
+ var DEFAULT_PRELOAD_THRESHOLD_SECS = 15;
33
+ var DEFAULT_CROSSFADE_DURATION_SECS = 0;
34
+ var MAX_CROSSFADE_DURATION_SECS = 12;
35
+ var DEFAULT_CROSSFADE_CURVE = "qsin";
36
+ var DEFAULT_CROSSFADE_WARMUP_SECS = 5;
37
+ var CROSSFADE_DRIFT_TOLERANCE_SECS = 0.75;
32
38
  var defaultOptions = {
33
39
  plugins: [],
34
40
  emitNewSongOnly: false,
@@ -38,7 +44,13 @@ var defaultOptions = {
38
44
  emitAddListWhenCreatingQueue: true,
39
45
  joinNewVoiceChannel: true,
40
46
  unhandledError: "log",
41
- stuckThreshold: STUCK_THRESHOLD_MS
47
+ stuckThreshold: STUCK_THRESHOLD_MS,
48
+ preload: false,
49
+ crossfade: {
50
+ duration: DEFAULT_CROSSFADE_DURATION_SECS,
51
+ curve: DEFAULT_CROSSFADE_CURVE,
52
+ warmup: DEFAULT_CROSSFADE_WARMUP_SECS
53
+ }
42
54
  };
43
55
 
44
56
  // src/core/DisTubeBase.ts
@@ -615,7 +627,14 @@ var DisTubeVoice = class extends TypedEmitter {
615
627
  if (newState.reason === VoiceConnectionDisconnectReason.Manual) {
616
628
  this.leave();
617
629
  } else if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
618
- entersState(this.connection, VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).catch(() => {
630
+ entersState(this.connection, VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).then(() => entersState(this.connection, VoiceConnectionStatus.Ready, RECONNECT_TIMEOUT_MS)).then(() => {
631
+ const oldChannel = this.#channel;
632
+ const currentChannel = this.channel;
633
+ if (oldChannel && currentChannel && oldChannel.id !== currentChannel.id) {
634
+ this.#channel = currentChannel;
635
+ this.emit("channelChange", oldChannel, currentChannel);
636
+ }
637
+ }).catch(() => {
619
638
  if (![VoiceConnectionStatus.Ready, VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
620
639
  this.leave();
621
640
  }
@@ -676,8 +695,12 @@ var DisTubeVoice = class extends TypedEmitter {
676
695
  if (channel.full) throw new DisTubeError("VOICE_FULL");
677
696
  else throw new DisTubeError("VOICE_MISSING_PERMS");
678
697
  }
698
+ const oldChannel = this.#channel;
679
699
  this.connection = this.#join(channel);
680
700
  this.#channel = channel;
701
+ if (oldChannel && oldChannel.id !== channel.id) {
702
+ this.emit("channelChange", oldChannel, channel);
703
+ }
681
704
  }
682
705
  #join(channel) {
683
706
  return joinVoiceChannel({
@@ -757,7 +780,7 @@ var DisTubeVoice = class extends TypedEmitter {
757
780
  if (this.audioPlayer.state.status !== AudioPlayerStatus.Paused) {
758
781
  this.audioPlayer.play(dtStream.audioResource);
759
782
  this.stream?.kill();
760
- dtStream.spawn();
783
+ if (!dtStream.process) dtStream.spawn();
761
784
  } else if (!this.pausingStream) {
762
785
  this.pausingStream = this.stream;
763
786
  }
@@ -965,7 +988,13 @@ var FilterManager = class extends BaseManager {
965
988
  }
966
989
  throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
967
990
  }
968
- #apply() {
991
+ #apply(oldFilters) {
992
+ this.queue._clearPreload?.();
993
+ const newFilters = this.names;
994
+ const changed = oldFilters.length !== newFilters.length || !oldFilters.every((f) => newFilters.includes(f));
995
+ if (changed) {
996
+ this.emit("changeFilter" /* CHANGE_FILTER */, this.queue, newFilters, oldFilters);
997
+ }
969
998
  void this.queue._restartStream().catch((e) => {
970
999
  const errorMessage = e instanceof Error ? e.message : String(e);
971
1000
  this.queue.debug(`[${this.queue.id}] Failed to apply the filters: ${errorMessage}`);
@@ -983,6 +1012,7 @@ var FilterManager = class extends BaseManager {
983
1012
  * ```
984
1013
  */
985
1014
  add(filterOrFilters, override = false) {
1015
+ const oldFilters = this.names;
986
1016
  if (Array.isArray(filterOrFilters)) {
987
1017
  for (const filter of filterOrFilters) {
988
1018
  const ft = this.#resolve(filter);
@@ -992,7 +1022,7 @@ var FilterManager = class extends BaseManager {
992
1022
  const ft = this.#resolve(filterOrFilters);
993
1023
  if (override || !this.has(ft)) this.collection.set(ft.name, ft);
994
1024
  }
995
- this.#apply();
1025
+ this.#apply(oldFilters);
996
1026
  return this;
997
1027
  }
998
1028
  /**
@@ -1011,12 +1041,13 @@ var FilterManager = class extends BaseManager {
1011
1041
  */
1012
1042
  set(filters) {
1013
1043
  if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
1044
+ const oldFilters = this.names;
1014
1045
  this.collection.clear();
1015
1046
  for (const f of filters) {
1016
1047
  const filter = this.#resolve(f);
1017
1048
  this.collection.set(filter.name, filter);
1018
1049
  }
1019
- this.#apply();
1050
+ this.#apply(oldFilters);
1020
1051
  return this;
1021
1052
  }
1022
1053
  #removeFn(f) {
@@ -1029,9 +1060,10 @@ var FilterManager = class extends BaseManager {
1029
1060
  * @throws {@link DisTubeError} When a filter is not a valid {@link FilterResolvable}
1030
1061
  */
1031
1062
  remove(filterOrFilters) {
1063
+ const oldFilters = this.names;
1032
1064
  if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
1033
1065
  else this.#removeFn(filterOrFilters);
1034
- this.#apply();
1066
+ this.#apply(oldFilters);
1035
1067
  return this;
1036
1068
  }
1037
1069
  /**
@@ -1087,6 +1119,17 @@ var Events = /* @__PURE__ */ ((Events2) => {
1087
1119
  Events2["DELETE_QUEUE"] = "deleteQueue";
1088
1120
  Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
1089
1121
  Events2["DEBUG"] = "debug";
1122
+ Events2["PAUSE_SONG"] = "pauseSong";
1123
+ Events2["RESUME_SONG"] = "resumeSong";
1124
+ Events2["SEEK_SONG"] = "seekSong";
1125
+ Events2["SET_VOLUME"] = "setVolume";
1126
+ Events2["SHUFFLE_QUEUE"] = "shuffleQueue";
1127
+ Events2["SKIP_SONG"] = "skipSong";
1128
+ Events2["CHANGE_FILTER"] = "changeFilter";
1129
+ Events2["VOICE_CHANNEL_CHANGE"] = "voiceChannelChange";
1130
+ Events2["PRELOAD_SONG"] = "preloadSong";
1131
+ Events2["PRELOADED_SONG"] = "preloadedSong";
1132
+ Events2["PRELOAD_ERROR"] = "preloadError";
1090
1133
  return Events2;
1091
1134
  })(Events || {});
1092
1135
  var SearchResultType = /* @__PURE__ */ ((SearchResultType2) => {
@@ -1220,6 +1263,7 @@ var Queue = class extends DisTubeBase {
1220
1263
  */
1221
1264
  _beginTime;
1222
1265
  #filters;
1266
+ #initialized = false;
1223
1267
  /**
1224
1268
  * Whether or not the queue is being updated manually (skip, jump, previous)
1225
1269
  * @internal
@@ -1235,6 +1279,75 @@ var Queue = class extends DisTubeBase {
1235
1279
  * @internal
1236
1280
  */
1237
1281
  _listeners;
1282
+ /**
1283
+ * Preload timer
1284
+ * @internal
1285
+ */
1286
+ _preloadTimeout;
1287
+ /**
1288
+ * Preloaded song
1289
+ * @internal
1290
+ */
1291
+ _preloadedSong;
1292
+ /**
1293
+ * Preloaded stream
1294
+ * @internal
1295
+ */
1296
+ _preloadedStream;
1297
+ /**
1298
+ * In-flight preload task
1299
+ * @internal
1300
+ */
1301
+ _preloading;
1302
+ /**
1303
+ * Bumped whenever the preloaded state is invalidated, so an in-flight
1304
+ * preload task can detect that its result is no longer needed.
1305
+ * @internal
1306
+ */
1307
+ _preloadGeneration = 0;
1308
+ #crossfade;
1309
+ /**
1310
+ * Crossfade boundary timer
1311
+ * @internal
1312
+ */
1313
+ _crossfadeTimeout;
1314
+ /**
1315
+ * The merged transition stream that is warming up before the fade window
1316
+ * starts
1317
+ * @internal
1318
+ */
1319
+ _crossfadeWarmup;
1320
+ /**
1321
+ * Clear any preloaded song and cancel pending preload timer.
1322
+ * @internal
1323
+ */
1324
+ _clearPreload() {
1325
+ this._crossfadeCancel();
1326
+ this._preloadGeneration++;
1327
+ if (this._preloadTimeout) {
1328
+ clearTimeout(this._preloadTimeout);
1329
+ this._preloadTimeout = void 0;
1330
+ }
1331
+ if (this._preloadedStream) {
1332
+ this._preloadedStream.kill();
1333
+ this._preloadedStream = void 0;
1334
+ }
1335
+ this._preloadedSong = void 0;
1336
+ }
1337
+ /**
1338
+ * Cancel a pending crossfade and kill its warming-up transition stream.
1339
+ * @internal
1340
+ */
1341
+ _crossfadeCancel() {
1342
+ if (this._crossfadeTimeout) {
1343
+ clearTimeout(this._crossfadeTimeout);
1344
+ this._crossfadeTimeout = void 0;
1345
+ }
1346
+ if (this._crossfadeWarmup) {
1347
+ this._crossfadeWarmup.kill();
1348
+ this._crossfadeWarmup = void 0;
1349
+ }
1350
+ }
1238
1351
  /**
1239
1352
  * Create a queue for the guild
1240
1353
  * @param distube - DisTube
@@ -1263,6 +1376,8 @@ var Queue = class extends DisTubeBase {
1263
1376
  input: { ...this.options.ffmpeg.args.input },
1264
1377
  output: { ...this.options.ffmpeg.args.output }
1265
1378
  };
1379
+ this.#crossfade = Number(this.options.crossfade?.duration) || 0;
1380
+ this.#initialized = true;
1266
1381
  }
1267
1382
  #addToPreviousSongs(songs) {
1268
1383
  if (Array.isArray(songs)) {
@@ -1320,6 +1435,28 @@ var Queue = class extends DisTubeBase {
1320
1435
  get currentTime() {
1321
1436
  return this.voice.playbackTime;
1322
1437
  }
1438
+ /**
1439
+ * The crossfade duration (in seconds) between this song and the next one.
1440
+ *
1441
+ * `0` (the default) disables the crossfade. Changing it takes effect for the
1442
+ * next transition; call {@link Queue#scheduleCrossfade} to re-arm the
1443
+ * transition for the currently playing song immediately.
1444
+ * @throws {@link DisTubeError} When the value is not a number between `0`
1445
+ * and the maximum crossfade duration
1446
+ */
1447
+ get crossfade() {
1448
+ return this.#crossfade;
1449
+ }
1450
+ set crossfade(value) {
1451
+ if (typeof value !== "number" || Number.isNaN(value)) {
1452
+ throw new DisTubeError("INVALID_TYPE", "number", value, "Queue.crossfade");
1453
+ }
1454
+ if (value < 0) throw new DisTubeError("NUMBER_COMPARE", "Crossfade", "bigger or equal to", 0);
1455
+ if (value > MAX_CROSSFADE_DURATION_SECS) {
1456
+ throw new DisTubeError("NUMBER_COMPARE", "Crossfade", "smaller or equal to", MAX_CROSSFADE_DURATION_SECS);
1457
+ }
1458
+ this.#crossfade = value;
1459
+ }
1323
1460
  /**
1324
1461
  * Formatted {@link Queue#currentTime} string.
1325
1462
  */
@@ -1345,7 +1482,11 @@ var Queue = class extends DisTubeBase {
1345
1482
  * negative
1346
1483
  */
1347
1484
  set volume(value) {
1485
+ const oldVolume = this.voice.volume;
1348
1486
  this.voice.volume = value;
1487
+ if (this.#initialized && oldVolume !== value) {
1488
+ this.emit("setVolume" /* SET_VOLUME */, this, value, oldVolume);
1489
+ }
1349
1490
  }
1350
1491
  /**
1351
1492
  * Add a song or a list of songs to the queue.
@@ -1374,6 +1515,7 @@ var Queue = class extends DisTubeBase {
1374
1515
  } else {
1375
1516
  this.songs.splice(position, 0, song);
1376
1517
  }
1518
+ this.queues?.schedulePreload?.(this);
1377
1519
  return this;
1378
1520
  }
1379
1521
  /**
@@ -1391,7 +1533,9 @@ var Queue = class extends DisTubeBase {
1391
1533
  try {
1392
1534
  if (this.paused) throw new DisTubeError("PAUSED");
1393
1535
  this.paused = true;
1536
+ this._clearPreload();
1394
1537
  this.voice.pause();
1538
+ this.emit("pauseSong" /* PAUSE_SONG */, this, this.songs[0]);
1395
1539
  return this;
1396
1540
  } finally {
1397
1541
  this._taskQueue.resolve();
@@ -1412,6 +1556,9 @@ var Queue = class extends DisTubeBase {
1412
1556
  if (!this.paused) throw new DisTubeError("RESUMED");
1413
1557
  this.paused = false;
1414
1558
  this.voice.unpause();
1559
+ this.emit("resumeSong" /* RESUME_SONG */, this, this.songs[0]);
1560
+ this.queues?.schedulePreload?.(this);
1561
+ this.queues?.scheduleCrossfade?.(this);
1415
1562
  return this;
1416
1563
  } finally {
1417
1564
  this._taskQueue.resolve();
@@ -1476,11 +1623,14 @@ var Queue = class extends DisTubeBase {
1476
1623
  try {
1477
1624
  const playing = this.songs.shift();
1478
1625
  if (playing === void 0) return this;
1626
+ this._clearPreload();
1479
1627
  for (let i = this.songs.length - 1; i > 0; i--) {
1480
1628
  const j = Math.floor(Math.random() * (i + 1));
1481
1629
  [this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
1482
1630
  }
1483
1631
  this.songs.unshift(playing);
1632
+ this.emit("shuffleQueue" /* SHUFFLE_QUEUE */, this, this.songs);
1633
+ this.queues?.schedulePreload?.(this);
1484
1634
  return this;
1485
1635
  } finally {
1486
1636
  this._taskQueue.resolve();
@@ -1518,12 +1668,14 @@ var Queue = class extends DisTubeBase {
1518
1668
  } else {
1519
1669
  this.#addToPreviousSongs(skipped);
1520
1670
  }
1671
+ this.emit("skipSong" /* SKIP_SONG */, this, skipped[0]);
1521
1672
  } else if (!this.options.savePreviousSongs) {
1522
1673
  throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
1523
1674
  } else {
1524
1675
  const skipped = this.previousSongs.splice(position);
1525
1676
  this.songs.unshift(...skipped);
1526
1677
  }
1678
+ this._clearPreload();
1527
1679
  this.#stop();
1528
1680
  return this.songs[0];
1529
1681
  } finally {
@@ -1562,8 +1714,10 @@ var Queue = class extends DisTubeBase {
1562
1714
  try {
1563
1715
  if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
1564
1716
  if (Number.isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
1717
+ this._clearPreload();
1565
1718
  this._beginTime = time;
1566
1719
  await this.play(false);
1720
+ this.emit("seekSong" /* SEEK_SONG */, this, this.songs[0], time);
1567
1721
  return this;
1568
1722
  } finally {
1569
1723
  this._taskQueue.resolve();
@@ -1634,6 +1788,7 @@ var Queue = class extends DisTubeBase {
1634
1788
  * @returns Nothing
1635
1789
  */
1636
1790
  remove() {
1791
+ this._clearPreload();
1637
1792
  this.paused = true;
1638
1793
  this.stopped = true;
1639
1794
  this.songs = [];
@@ -1660,6 +1815,17 @@ var Queue = class extends DisTubeBase {
1660
1815
  if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
1661
1816
  return this.queues.playSong(this, emitPlaySong);
1662
1817
  }
1818
+ /**
1819
+ * Re-arm the crossfade into the next song for the currently playing one.
1820
+ *
1821
+ * Useful after changing {@link Queue#crossfade} while a song is playing: the
1822
+ * fade may still fit into the remaining time. This is called automatically
1823
+ * after every song switch.
1824
+ * @returns Nothing
1825
+ */
1826
+ scheduleCrossfade() {
1827
+ this.queues.scheduleCrossfade(this);
1828
+ }
1663
1829
  /**
1664
1830
  * Restart the current stream with the current FFmpeg arguments, keeping the
1665
1831
  * playback position.
@@ -2307,6 +2473,16 @@ var Options = class {
2307
2473
  * @default 10000
2308
2474
  */
2309
2475
  stuckThreshold;
2476
+ /**
2477
+ * The preload options of the DisTube instance.
2478
+ * @default { enabled: false, threshold: 15 }
2479
+ */
2480
+ preload;
2481
+ /**
2482
+ * The crossfade options of the DisTube instance.
2483
+ * @default { duration: 0, curve: "qsin", warmup: 5 }
2484
+ */
2485
+ crossfade;
2310
2486
  /**
2311
2487
  * The FFmpeg options of the DisTube instance.
2312
2488
  */
@@ -2332,10 +2508,76 @@ var Options = class {
2332
2508
  this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
2333
2509
  this.unhandledError = opts.unhandledError;
2334
2510
  this.stuckThreshold = opts.stuckThreshold;
2511
+ this.preload = this.#preloadOption(opts.preload);
2512
+ this.crossfade = this.#crossfadeOption(opts.crossfade);
2335
2513
  this.ffmpeg = this.#ffmpegOption(options);
2336
2514
  checkInvalidKey(opts, this, "DisTubeOptions");
2337
2515
  this.#validateOptions();
2338
2516
  }
2517
+ #preloadOption(value) {
2518
+ if (value === void 0 || value === false) {
2519
+ return { enabled: false, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
2520
+ }
2521
+ if (value === true) {
2522
+ return { enabled: true, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
2523
+ }
2524
+ if (typeof value !== "object" || Array.isArray(value)) {
2525
+ throw new DisTubeError("INVALID_TYPE", ["boolean", "object"], value, "DisTubeOptions.preload");
2526
+ }
2527
+ checkInvalidKey(value, ["enabled", "threshold"], "DisTubeOptions.preload");
2528
+ const { enabled, threshold } = value;
2529
+ if (enabled !== void 0 && typeof enabled !== "boolean") {
2530
+ throw new DisTubeError("INVALID_TYPE", "boolean", enabled, "DisTubeOptions.preload.enabled");
2531
+ }
2532
+ if (threshold !== void 0 && (typeof threshold !== "number" || Number.isNaN(threshold) || threshold < 0)) {
2533
+ throw new DisTubeError("INVALID_TYPE", "number", threshold, "DisTubeOptions.preload.threshold");
2534
+ }
2535
+ return {
2536
+ enabled: enabled ?? false,
2537
+ threshold: threshold ?? DEFAULT_PRELOAD_THRESHOLD_SECS
2538
+ };
2539
+ }
2540
+ #crossfadeOption(value) {
2541
+ if (value === void 0) {
2542
+ return {
2543
+ duration: DEFAULT_CROSSFADE_DURATION_SECS,
2544
+ curve: DEFAULT_CROSSFADE_CURVE,
2545
+ warmup: DEFAULT_CROSSFADE_WARMUP_SECS
2546
+ };
2547
+ }
2548
+ if (typeof value !== "object" || Array.isArray(value)) {
2549
+ throw new DisTubeError("INVALID_TYPE", "object", value, "DisTubeOptions.crossfade");
2550
+ }
2551
+ checkInvalidKey(value, ["duration", "curve", "warmup"], "DisTubeOptions.crossfade");
2552
+ const { duration, curve, warmup } = value;
2553
+ if (duration !== void 0 && (typeof duration !== "number" || Number.isNaN(duration) || duration < 0)) {
2554
+ throw new DisTubeError(
2555
+ "INVALID_TYPE",
2556
+ `number >= 0 (max ${MAX_CROSSFADE_DURATION_SECS})`,
2557
+ duration,
2558
+ "DisTubeOptions.crossfade.duration"
2559
+ );
2560
+ }
2561
+ if (duration !== void 0 && duration > MAX_CROSSFADE_DURATION_SECS) {
2562
+ throw new DisTubeError(
2563
+ "NUMBER_COMPARE",
2564
+ "Crossfade duration",
2565
+ "smaller or equal to",
2566
+ MAX_CROSSFADE_DURATION_SECS
2567
+ );
2568
+ }
2569
+ if (curve !== void 0 && (typeof curve !== "string" || !curve)) {
2570
+ throw new DisTubeError("INVALID_TYPE", "string", curve, "DisTubeOptions.crossfade.curve");
2571
+ }
2572
+ if (warmup !== void 0 && (typeof warmup !== "number" || Number.isNaN(warmup) || warmup < 0)) {
2573
+ throw new DisTubeError("INVALID_TYPE", "number >= 0", warmup, "DisTubeOptions.crossfade.warmup");
2574
+ }
2575
+ return {
2576
+ duration: duration ?? DEFAULT_CROSSFADE_DURATION_SECS,
2577
+ curve: curve ?? DEFAULT_CROSSFADE_CURVE,
2578
+ warmup: warmup ?? DEFAULT_CROSSFADE_WARMUP_SECS
2579
+ };
2580
+ }
2339
2581
  #validateOptions(options = this) {
2340
2582
  const booleanOptions = /* @__PURE__ */ new Set([
2341
2583
  "emitNewSongOnly",
@@ -2347,7 +2589,7 @@ var Options = class {
2347
2589
  ]);
2348
2590
  const numberOptions = /* @__PURE__ */ new Set(["stuckThreshold"]);
2349
2591
  const stringOptions = /* @__PURE__ */ new Set(["unhandledError"]);
2350
- const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
2592
+ const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg", "preload", "crossfade"]);
2351
2593
  const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
2352
2594
  for (const [key, value] of Object.entries(options)) {
2353
2595
  if (value === void 0 && optionalOptions.has(key)) continue;
@@ -2465,8 +2707,40 @@ var DisTubeStream = class extends TypedEmitter2 {
2465
2707
  */
2466
2708
  constructor(url, options) {
2467
2709
  super();
2468
- const { ffmpeg, seek } = options;
2710
+ const { ffmpeg, seek, crossfade } = options;
2469
2711
  this.seekTime = typeof seek === "number" && seek > 0 ? seek : 0;
2712
+ this.#ffmpegPath = ffmpeg.path;
2713
+ this.#opts = crossfade ? this.#crossfadeArgs(url, ffmpeg, crossfade) : this.#singleInputArgs(url, ffmpeg, seek);
2714
+ this.stream = new VolumeTransformer();
2715
+ this.stream.on("close", () => this.kill()).on("error", (err) => {
2716
+ this.debug(`[stream] error: ${err.message}`);
2717
+ this.emit("error", err);
2718
+ }).on("finish", () => this.debug("[stream] log: stream finished"));
2719
+ this.audioResource = createAudioResource(this.stream, {
2720
+ inputType: StreamType.Raw,
2721
+ inlineVolume: false
2722
+ });
2723
+ }
2724
+ #flattenArgs(args) {
2725
+ return Object.entries(args).flatMap(
2726
+ ([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
2727
+ ).flat();
2728
+ }
2729
+ /**
2730
+ * Translate a `file:` URL into a local filesystem path that FFmpeg accepts.
2731
+ *
2732
+ * `file:///C:/x` loses its drive-letter leading slash on Windows (`C:/x`),
2733
+ * UNC shares keep their `//host` prefix, POSIX paths stay untouched, and
2734
+ * other protocols return `undefined`.
2735
+ */
2736
+ #localPath(url) {
2737
+ const fileUrl = new URL(url);
2738
+ if (fileUrl.protocol !== "file:") return void 0;
2739
+ const pathname = decodeURIComponent(fileUrl.pathname);
2740
+ if (fileUrl.hostname) return `//${fileUrl.hostname}${pathname}`;
2741
+ return /^\/[A-Za-z]:\//.test(pathname) ? pathname.slice(1) : pathname;
2742
+ }
2743
+ #singleInputArgs(url, ffmpeg, seek) {
2470
2744
  const opts = {
2471
2745
  reconnect: 1,
2472
2746
  reconnect_streamed: 1,
@@ -2482,29 +2756,91 @@ var DisTubeStream = class extends TypedEmitter2 {
2482
2756
  f: "s16le"
2483
2757
  };
2484
2758
  if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
2485
- const fileUrl = new URL(url);
2486
- if (fileUrl.protocol === "file:") {
2759
+ const localPath = this.#localPath(url);
2760
+ if (localPath !== void 0) {
2487
2761
  opts.reconnect = null;
2488
2762
  opts.reconnect_streamed = null;
2489
2763
  opts.reconnect_delay_max = null;
2490
- opts.i = fileUrl.hostname + fileUrl.pathname;
2764
+ opts.i = localPath;
2491
2765
  }
2492
- this.#ffmpegPath = ffmpeg.path;
2493
- this.#opts = [
2494
- ...Object.entries(opts).flatMap(
2495
- ([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
2496
- ).flat(),
2766
+ return [...this.#flattenArgs(opts), "pipe:1"];
2767
+ }
2768
+ /**
2769
+ * Build the arguments of a crossfade transition stream: a single FFmpeg
2770
+ * process with two inputs (the fading out song seeked to its final
2771
+ * `duration` seconds and the fading in song from its beginning) mixed by an
2772
+ * `acrossfade` filter graph.
2773
+ *
2774
+ * Both inputs are normalized (48kHz stereo s16) before the fade, and the
2775
+ * queue's `af` filter chain, when present, is folded into the filter complex
2776
+ * after the fade (FFmpeg cannot combine `-af` with `-filter_complex`).
2777
+ */
2778
+ #crossfadeArgs(url, ffmpeg, crossfade) {
2779
+ const {
2780
+ duration,
2781
+ primarySeek,
2782
+ secondaryUrl,
2783
+ outCurve = DEFAULT_CROSSFADE_CURVE,
2784
+ inCurve = DEFAULT_CROSSFADE_CURVE
2785
+ } = crossfade;
2786
+ const outputArgs = { ...ffmpeg.args.output };
2787
+ const extraFilters = typeof outputArgs.af === "string" ? outputArgs.af : Array.isArray(outputArgs.af) ? outputArgs.af.filter(Boolean).join(",") : void 0;
2788
+ delete outputArgs.af;
2789
+ const primaryOpts = {
2790
+ reconnect: 1,
2791
+ reconnect_streamed: 1,
2792
+ reconnect_delay_max: 5,
2793
+ analyzeduration: 0,
2794
+ hide_banner: true,
2795
+ ...ffmpeg.args.global,
2796
+ ...ffmpeg.args.input,
2797
+ ss: typeof primarySeek === "number" && primarySeek > 0 ? primarySeek.toString() : null,
2798
+ i: url
2799
+ };
2800
+ const secondaryOpts = {
2801
+ reconnect: 1,
2802
+ reconnect_streamed: 1,
2803
+ reconnect_delay_max: 5,
2804
+ analyzeduration: 0,
2805
+ ...ffmpeg.args.input,
2806
+ i: secondaryUrl
2807
+ };
2808
+ const primaryLocal = this.#localPath(url);
2809
+ if (primaryLocal !== void 0) {
2810
+ primaryOpts.reconnect = null;
2811
+ primaryOpts.reconnect_streamed = null;
2812
+ primaryOpts.reconnect_delay_max = null;
2813
+ primaryOpts.i = primaryLocal;
2814
+ }
2815
+ const secondaryLocal = this.#localPath(secondaryUrl);
2816
+ if (secondaryLocal !== void 0) {
2817
+ secondaryOpts.reconnect = null;
2818
+ secondaryOpts.reconnect_streamed = null;
2819
+ secondaryOpts.reconnect_delay_max = null;
2820
+ secondaryOpts.i = secondaryLocal;
2821
+ }
2822
+ const normalize = `aresample=${AUDIO_SAMPLE_RATE},aformat=sample_fmts=s16:channel_layouts=stereo`;
2823
+ const fade = `[a0][a1]acrossfade=d=${duration}:c1=${outCurve}:c2=${inCurve}[xf]`;
2824
+ const graph = [
2825
+ `[0:a]${normalize}[a0]`,
2826
+ `[1:a]${normalize}[a1]`,
2827
+ fade,
2828
+ ...extraFilters ? [`[xf]${extraFilters}[aout]`] : []
2829
+ ].join(";");
2830
+ const outputOpts = {
2831
+ ar: AUDIO_SAMPLE_RATE,
2832
+ ac: AUDIO_CHANNELS,
2833
+ ...outputArgs,
2834
+ filter_complex: graph,
2835
+ map: extraFilters ? "[aout]" : "[xf]",
2836
+ f: "s16le"
2837
+ };
2838
+ return [
2839
+ ...this.#flattenArgs(primaryOpts),
2840
+ ...this.#flattenArgs(secondaryOpts),
2841
+ ...this.#flattenArgs(outputOpts),
2497
2842
  "pipe:1"
2498
2843
  ];
2499
- this.stream = new VolumeTransformer();
2500
- this.stream.on("close", () => this.kill()).on("error", (err) => {
2501
- this.debug(`[stream] error: ${err.message}`);
2502
- this.emit("error", err);
2503
- }).on("finish", () => this.debug("[stream] log: stream finished"));
2504
- this.audioResource = createAudioResource(this.stream, {
2505
- inputType: StreamType.Raw,
2506
- inlineVolume: false
2507
- });
2508
2844
  }
2509
2845
  /**
2510
2846
  * Spawn the FFmpeg process that decodes the audio.
@@ -2735,7 +3071,10 @@ var QueueManager = class extends GuildIdManager {
2735
3071
  if (error) this.emitError(error, queue, queue.songs?.[0]);
2736
3072
  }, "disconnect"),
2737
3073
  error: /* @__PURE__ */ __name((error) => void this.#handlePlayingError(queue, error), "error"),
2738
- finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish")
3074
+ finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish"),
3075
+ channelChange: /* @__PURE__ */ __name((oldChannel, newChannel) => {
3076
+ this.emit("voiceChannelChange" /* VOICE_CHANNEL_CHANGE */, queue, oldChannel, newChannel);
3077
+ }, "channelChange")
2739
3078
  };
2740
3079
  for (const event of objectKeys(queue._listeners)) {
2741
3080
  queue.voice.on(event, queue._listeners[event]);
@@ -2833,12 +3172,110 @@ var QueueManager = class extends GuildIdManager {
2833
3172
  }
2834
3173
  try {
2835
3174
  const song = queue.songs[0];
2836
- this.debug(`[${queue.id}] Getting stream from: ${song}`);
2837
- await this.handler.attachStreamInfo(song);
2838
- const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
3175
+ let dtStream;
3176
+ if (queue._preloadedSong === song && queue._preloadedStream) {
3177
+ this.debug(`[${queue.id}] Using preloaded stream for: ${song}`);
3178
+ dtStream = queue._preloadedStream;
3179
+ queue._preloadedSong = void 0;
3180
+ queue._preloadedStream = void 0;
3181
+ } else {
3182
+ queue._clearPreload();
3183
+ this.debug(`[${queue.id}] Getting stream from: ${song}`);
3184
+ await this.handler.attachStreamInfo(song);
3185
+ const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
3186
+ const stream = willPlaySong?.stream;
3187
+ if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
3188
+ this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
3189
+ const streamOptions = {
3190
+ ffmpeg: {
3191
+ path: this.options.ffmpeg.path,
3192
+ args: {
3193
+ global: { ...queue.ffmpegArgs.global },
3194
+ input: { ...queue.ffmpegArgs.input },
3195
+ output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
3196
+ }
3197
+ },
3198
+ seek: willPlaySong.duration ? queue._beginTime : void 0
3199
+ };
3200
+ queue._beginTime = 0;
3201
+ dtStream = new DisTubeStream(stream.url, streamOptions);
3202
+ dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
3203
+ }
3204
+ queue._beginTime = 0;
3205
+ this.debug(`[${queue.id}] Started playing: ${song}`);
3206
+ await queue.voice.play(dtStream);
3207
+ if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
3208
+ this.schedulePreload(queue);
3209
+ this.scheduleCrossfade(queue);
3210
+ } catch (e) {
3211
+ const error = e instanceof Error ? e : new Error(String(e));
3212
+ if (options?.keepSongOnError) {
3213
+ this.debug(`[${queue.id}] Error while restarting the stream: ${error.stack || error.message}`);
3214
+ this.emitError(error, queue, queue.songs[0]);
3215
+ return;
3216
+ }
3217
+ await this.#handlePlayingError(queue, error);
3218
+ }
3219
+ }
3220
+ /**
3221
+ * Schedule preloading the next song in the queue before the current song finishes.
3222
+ * @param queue - The guild queue
3223
+ */
3224
+ schedulePreload(queue) {
3225
+ if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
3226
+ if (queue._preloadTimeout) {
3227
+ clearTimeout(queue._preloadTimeout);
3228
+ queue._preloadTimeout = void 0;
3229
+ }
3230
+ const currentSong = queue.songs[0];
3231
+ const nextSong = queue.songs[1];
3232
+ if (!currentSong || !nextSong) return;
3233
+ if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
3234
+ const threshold = this.options.preload.threshold;
3235
+ const duration = currentSong.duration ?? 0;
3236
+ const currentTime = queue.currentTime;
3237
+ const remaining = duration > 0 && Number.isFinite(currentTime) ? duration - currentTime : 0;
3238
+ if (duration === 0 || remaining <= threshold) {
3239
+ void this.preloadNextSong(queue);
3240
+ } else {
3241
+ const delayMs = Math.max(0, (remaining - threshold) * 1e3);
3242
+ queue._preloadTimeout = setTimeout(() => {
3243
+ queue._preloadTimeout = void 0;
3244
+ void this.preloadNextSong(queue);
3245
+ }, delayMs);
3246
+ queue._preloadTimeout.unref?.();
3247
+ }
3248
+ }
3249
+ /**
3250
+ * Preload the next song in the queue.
3251
+ * @param queue - The guild queue
3252
+ */
3253
+ async preloadNextSong(queue) {
3254
+ if (queue._preloading) return queue._preloading;
3255
+ const task = this.#preloadTask(queue);
3256
+ queue._preloading = task;
3257
+ try {
3258
+ await task;
3259
+ } finally {
3260
+ if (queue._preloading === task) queue._preloading = void 0;
3261
+ }
3262
+ }
3263
+ async #preloadTask(queue) {
3264
+ if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
3265
+ const nextSong = queue.songs[1];
3266
+ if (!nextSong) return;
3267
+ if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
3268
+ const generation = queue._preloadGeneration;
3269
+ this.debug(`[${queue.id}] Preloading next song: ${nextSong}`);
3270
+ this.emit("preloadSong" /* PRELOAD_SONG */, queue, nextSong);
3271
+ try {
3272
+ await this.handler.attachStreamInfo(nextSong);
3273
+ if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) return;
3274
+ const willPlaySong = nextSong.stream.playFromSource ? nextSong : nextSong.stream.song;
2839
3275
  const stream = willPlaySong?.stream;
2840
- if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
2841
- this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
3276
+ if (!willPlaySong || !stream?.playFromSource || !stream.url) {
3277
+ throw new DisTubeError("NO_STREAM_URL", `${nextSong}`);
3278
+ }
2842
3279
  const streamOptions = {
2843
3280
  ffmpeg: {
2844
3281
  path: this.options.ffmpeg.path,
@@ -2848,22 +3285,185 @@ var QueueManager = class extends GuildIdManager {
2848
3285
  output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
2849
3286
  }
2850
3287
  },
2851
- seek: willPlaySong.duration ? queue._beginTime : void 0
3288
+ seek: void 0
2852
3289
  };
2853
- queue._beginTime = 0;
2854
3290
  const dtStream = new DisTubeStream(stream.url, streamOptions);
2855
- dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
2856
- this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
2857
- await queue.voice.play(dtStream);
2858
- if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
3291
+ dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] [preload] ${data}`));
3292
+ if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) {
3293
+ dtStream.kill();
3294
+ return;
3295
+ }
3296
+ queue._preloadedSong = nextSong;
3297
+ queue._preloadedStream = dtStream;
3298
+ this.debug(`[${queue.id}] Successfully preloaded next song: ${nextSong}`);
3299
+ this.emit("preloadedSong" /* PRELOADED_SONG */, queue, nextSong);
2859
3300
  } catch (e) {
3301
+ if (queue._preloadedSong === nextSong) {
3302
+ queue._preloadedSong = void 0;
3303
+ queue._preloadedStream?.kill();
3304
+ queue._preloadedStream = void 0;
3305
+ }
2860
3306
  const error = e instanceof Error ? e : new Error(String(e));
2861
- if (options?.keepSongOnError) {
2862
- this.debug(`[${queue.id}] Error while restarting the stream: ${error.stack || error.message}`);
2863
- this.emitError(error, queue, queue.songs[0]);
3307
+ this.debug(`[${queue.id}] Preload failed for ${nextSong}: ${error.message}`);
3308
+ this.emit("preloadError" /* PRELOAD_ERROR */, queue, nextSong, error);
3309
+ }
3310
+ }
3311
+ /**
3312
+ * Schedule the crossfade into the next song: a single FFmpeg process mixes
3313
+ * the current song (seeked to its final fade window) with the next song, and
3314
+ * replaces the playback stream exactly when the fade window begins.
3315
+ *
3316
+ * Canceled automatically whenever the preloaded state is invalidated
3317
+ * (pause, seek, stop...), as both schedulers share the same generation.
3318
+ * @param queue - The guild queue
3319
+ */
3320
+ scheduleCrossfade(queue) {
3321
+ queue._crossfadeCancel();
3322
+ if (queue.stopped || queue.paused) return;
3323
+ const currentSong = queue.songs[0];
3324
+ if (!currentSong) return;
3325
+ const songDuration = currentSong.duration ?? 0;
3326
+ if (!songDuration || !Number.isFinite(songDuration)) return;
3327
+ const fade = Math.min(Number(queue.crossfade) || 0, songDuration);
3328
+ if (fade <= 0) return;
3329
+ const nextSong = queue.repeatMode === 1 /* SONG */ ? currentSong : queue.songs[1];
3330
+ if (!nextSong) return;
3331
+ const currentTime = queue.currentTime;
3332
+ if (!Number.isFinite(currentTime)) return;
3333
+ const remaining = songDuration - currentTime;
3334
+ if (remaining <= fade) return;
3335
+ const delayMs = Math.max(0, (remaining - fade - this.options.crossfade.warmup) * 1e3);
3336
+ this.debug(
3337
+ `[${queue.id}] Scheduling crossfade (${fade}s fade) in ${(delayMs / 1e3).toFixed(1)}s: ${currentSong} -> ${nextSong}`
3338
+ );
3339
+ queue._crossfadeTimeout = setTimeout(() => {
3340
+ queue._crossfadeTimeout = void 0;
3341
+ void this.#crossfadeWarmupTask(queue);
3342
+ }, delayMs);
3343
+ queue._crossfadeTimeout.unref?.();
3344
+ }
3345
+ /**
3346
+ * Spawn the merged transition stream ahead of the fade window so both of
3347
+ * its inputs are buffered when the fade must start, then arm the boundary
3348
+ * timer that swaps the playback stream.
3349
+ */
3350
+ async #crossfadeWarmupTask(queue) {
3351
+ let transitionStream;
3352
+ try {
3353
+ if (queue.stopped || queue.paused) return;
3354
+ const song = queue.songs[0];
3355
+ const generation = queue._preloadGeneration;
3356
+ const repeatSong = queue.repeatMode === 1 /* SONG */;
3357
+ const nextSong = repeatSong ? song : queue.songs[1];
3358
+ if (!song || !nextSong) return;
3359
+ const songDuration = song.duration ?? 0;
3360
+ const fade = Math.min(Number(queue.crossfade) || 0, songDuration);
3361
+ if (fade <= 0) return;
3362
+ const remaining = songDuration - queue.currentTime;
3363
+ if (remaining <= fade) return;
3364
+ if (!repeatSong) {
3365
+ if (this.options.preload.enabled) await this.preloadNextSong(queue);
3366
+ else await this.handler.attachStreamInfo(nextSong);
3367
+ }
3368
+ if (queue.stopped || queue.paused || queue.songs[0] !== song || generation !== queue._preloadGeneration) return;
3369
+ if (repeatSong ? queue.songs[0] !== song : queue.songs[1] !== nextSong) return;
3370
+ const primary = song.stream.playFromSource ? song : song.stream.song;
3371
+ const secondary = repeatSong ? primary : nextSong.stream.playFromSource ? nextSong : nextSong.stream.song;
3372
+ if (!primary?.stream.playFromSource || !primary.stream.url) return;
3373
+ if (!secondary?.stream.playFromSource || !secondary.stream.url) return;
3374
+ this.debug(`[${queue.id}] Warming up crossfade stream: ${song} -> ${nextSong}`);
3375
+ transitionStream = new DisTubeStream(primary.stream.url, {
3376
+ ffmpeg: {
3377
+ path: this.options.ffmpeg.path,
3378
+ args: {
3379
+ global: { ...queue.ffmpegArgs.global },
3380
+ input: { ...queue.ffmpegArgs.input },
3381
+ output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
3382
+ }
3383
+ },
3384
+ crossfade: {
3385
+ duration: fade,
3386
+ primarySeek: Math.max(0, songDuration - fade),
3387
+ secondaryUrl: secondary.stream.url,
3388
+ inCurve: this.options.crossfade.curve,
3389
+ outCurve: this.options.crossfade.curve
3390
+ }
3391
+ });
3392
+ transitionStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] [crossfade] ${data}`));
3393
+ if (queue.stopped || queue.paused || queue.songs[0] !== song || generation !== queue._preloadGeneration) {
3394
+ transitionStream.kill();
2864
3395
  return;
2865
3396
  }
2866
- await this.#handlePlayingError(queue, error);
3397
+ queue._crossfadeWarmup = transitionStream;
3398
+ transitionStream.spawn();
3399
+ const boundaryMs = Math.max(0, (remaining - fade) * 1e3);
3400
+ queue._crossfadeTimeout = setTimeout(() => {
3401
+ queue._crossfadeTimeout = void 0;
3402
+ void this.#crossfadeSwapTask(queue, transitionStream);
3403
+ }, boundaryMs);
3404
+ queue._crossfadeTimeout.unref?.();
3405
+ } catch (e) {
3406
+ const errorMessage = e instanceof Error ? e.message : String(e);
3407
+ this.debug(`[${queue.id}] Crossfade warmup failed, using the regular transition: ${errorMessage}`);
3408
+ if (transitionStream) transitionStream.kill();
3409
+ }
3410
+ }
3411
+ /**
3412
+ * Swap the playing stream for the merged transition stream and update the
3413
+ * queue as if the current song had just finished.
3414
+ */
3415
+ async #crossfadeSwapTask(queue, transitionStream) {
3416
+ const abort = /* @__PURE__ */ __name(() => {
3417
+ if (queue._crossfadeWarmup === transitionStream) queue._crossfadeWarmup = void 0;
3418
+ transitionStream.kill();
3419
+ }, "abort");
3420
+ try {
3421
+ if (queue.stopped || queue.paused || queue._crossfadeWarmup !== transitionStream) return abort();
3422
+ const song = queue.songs[0];
3423
+ if (!song) return abort();
3424
+ const songDuration = song.duration ?? 0;
3425
+ const fade = Math.min(Number(queue.crossfade) || 0, songDuration);
3426
+ if (fade <= 0) return abort();
3427
+ const drift = queue.currentTime - (songDuration - fade);
3428
+ if (Math.abs(drift) > CROSSFADE_DRIFT_TOLERANCE_SECS) {
3429
+ this.debug(`[${queue.id}] Crossfade aborted: playback drifted ${drift.toFixed(2)}s from the fade boundary`);
3430
+ return abort();
3431
+ }
3432
+ await queue._taskQueue.queuing();
3433
+ try {
3434
+ if (queue.stopped || queue.paused || queue.songs[0] !== song || queue._crossfadeWarmup !== transitionStream) {
3435
+ return abort();
3436
+ }
3437
+ this.debug(`[${queue.id}] Starting crossfade: ${song} -> ${queue.songs.length > 1 ? queue.songs[1] : song}`);
3438
+ await queue.voice.play(transitionStream);
3439
+ queue._crossfadeWarmup = void 0;
3440
+ this.emit("finishSong" /* FINISH_SONG */, queue, song);
3441
+ if (queue.repeatMode === 2 /* QUEUE */) queue.songs.push(song);
3442
+ if (queue.repeatMode !== 1 /* SONG */) {
3443
+ const prev = queue.songs.shift();
3444
+ if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
3445
+ else queue.previousSongs.push({ id: prev.id });
3446
+ } else {
3447
+ }
3448
+ if (song !== queue.songs[0]) {
3449
+ const playedSong = song.stream.playFromSource ? song : song.stream.song;
3450
+ if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
3451
+ }
3452
+ if (queue._preloadedStream) {
3453
+ queue._preloadedStream.kill();
3454
+ queue._preloadedStream = void 0;
3455
+ queue._preloadedSong = void 0;
3456
+ }
3457
+ this.emit("playSong" /* PLAY_SONG */, queue, queue.songs[0]);
3458
+ this.schedulePreload(queue);
3459
+ this.scheduleCrossfade(queue);
3460
+ } finally {
3461
+ queue._taskQueue.resolve();
3462
+ }
3463
+ } catch (e) {
3464
+ abort();
3465
+ const errorMessage = e instanceof Error ? e.message : String(e);
3466
+ this.debug(`[${queue.id}] Crossfade swap failed, using the regular transition: ${errorMessage}`);
2867
3467
  }
2868
3468
  }
2869
3469
  };
@@ -3879,6 +4479,11 @@ export {
3879
4479
  AUDIO_CHANNELS,
3880
4480
  AUDIO_SAMPLE_RATE,
3881
4481
  BaseManager,
4482
+ CROSSFADE_DRIFT_TOLERANCE_SECS,
4483
+ DEFAULT_CROSSFADE_CURVE,
4484
+ DEFAULT_CROSSFADE_DURATION_SECS,
4485
+ DEFAULT_CROSSFADE_WARMUP_SECS,
4486
+ DEFAULT_PRELOAD_THRESHOLD_SECS,
3882
4487
  DEFAULT_VOLUME,
3883
4488
  DisTube,
3884
4489
  DisTubeBase,
@@ -3894,6 +4499,7 @@ export {
3894
4499
  HTTP_REDIRECT_CODES,
3895
4500
  InfoExtractorPlugin,
3896
4501
  JOIN_TIMEOUT_MS,
4502
+ MAX_CROSSFADE_DURATION_SECS,
3897
4503
  MAX_REDIRECT_DEPTH,
3898
4504
  Options,
3899
4505
  PlayableExtractorPlugin,