feat(autodj): smooth skip and interrupt blend transitions (#1128)

This commit is contained in:
cucadmuh
2026-06-19 04:52:15 +03:00
committed by GitHub
parent d50c9c444d
commit 4225146a16
50 changed files with 1118 additions and 208 deletions
+56
View File
@@ -4,6 +4,7 @@ import { findLocalPlaybackUrl } from '../utils/offline/offlineLibraryHelpers';
import { playbackCacheKeyForRef } from '../utils/playback/playbackServer';
import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl';
import { resolveQueueTrack } from '../utils/library/queueTrackView';
import type { Track } from './playerStoreTypes';
import { useAuthStore } from './authStore';
import {
hasPlannedCrossfade,
@@ -51,6 +52,61 @@ export function isCrossfadeNextReady(
return false;
}
/** Outgoing fade + preload window before an interrupt handoff (library pick, etc.). */
export const INTERRUPT_BLEND_PREP_FADE_SEC = 1.0;
/** @deprecated Use {@link INTERRUPT_BLEND_PREP_FADE_SEC} — prep and wait are aligned. */
export const INTERRUPT_BLEND_PRELOAD_WAIT_MS = Math.round(INTERRUPT_BLEND_PREP_FADE_SEC * 1000);
/**
* Start an eager RAM preload for a track the user just picked (no queue lead time).
* No-op when already ready or a preload for this id is in flight.
*/
export function kickEagerCrossfadePreload(
track: Track,
profileId: string | null,
cacheKey: string | null,
): void {
if (isCrossfadeNextReady(track.id, profileId, cacheKey)) return;
if (track.id === getBytePreloadingId()) return;
const serverId = cacheKey || profileId;
const url = resolvePlaybackUrl(track.id, serverId ?? undefined);
setBytePreloadingId(track.id);
void refreshLoudnessForTrack(track.id, { syncPlayingEngine: false });
invoke('audio_preload', {
url,
durationHint: track.duration,
analysisTrackId: track.id,
serverId: serverId || null,
eager: true,
}).catch(() => {});
}
function sleepMs(ms: number): Promise<void> {
return new Promise(resolve => { window.setTimeout(resolve, ms); });
}
/**
* Poll until B is playable for a stable crossfade, or `maxWaitMs` elapses.
* Returns false when `isStale()` reports a superseding play generation.
*/
export async function waitForCrossfadeNextReady(
trackId: string,
profileId: string | null,
cacheKey: string | null,
maxWaitMs: number,
isStale: () => boolean,
): Promise<boolean> {
if (isCrossfadeNextReady(trackId, profileId, cacheKey)) return true;
const deadline = Date.now() + maxWaitMs;
while (Date.now() < deadline) {
if (isStale()) return false;
await sleepMs(50);
if (isCrossfadeNextReady(trackId, profileId, cacheKey)) return true;
}
return isCrossfadeNextReady(trackId, profileId, cacheKey);
}
/**
* Crossfade-only byte pre-download for the next track + (when trim is on) its
* leading-silence probe. Self-gating and idempotent (`bytePreloadingId` /