mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(autodj): smooth skip and interrupt blend transitions (#1128)
This commit is contained in:
@@ -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` /
|
||||
|
||||
Reference in New Issue
Block a user