mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(player): E.25 — extract audio-orchestration cluster (#589)
Two thematic cuts in one PR:
- `src/store/queueUndoAudioRestore.ts` — `queueUndoRestoreAudioEngine`
(~70 LOC). Reload the Rust audio engine to match a queue-undo
snapshot: audio_play with snapshot track params, optional
audio_seek to the snapshot position, audio_pause if the snapshot
captured a paused state. Generation-guard bails on concurrent
playTrack. Drives recordEnginePlayUrl, setDeferHotCachePrefetch,
and touchHotCacheOnPlayback.
- `src/store/loudnessPrefetch.ts` — `prefetchLoudnessForEnqueuedTracks`.
Warms the loudness cache for the current track + next-N window
after a bulk enqueue, no-op when normalization isn't loudness.
Both file-private; no caller-side changes outside playerStore's own
imports. Removes the unused `collectLoudnessBackfillWindowTrackIds`
import that was only feeding the moved prefetch helper.
11 tests across the two modules pin the orchestration: audio_play
payload + seek-when-near-zero + wantPlaying=false → audio_pause +
generation-mismatch bail + .finally clears the hot-cache gate even
on errors; engine guard + window forwarding + sync-flag for prefetch.
playerStore 2865 → 2779 LOC.
This commit is contained in:
committed by
GitHub
parent
14bdcde33f
commit
a5aadeea67
@@ -0,0 +1,95 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { setDeferHotCachePrefetch } from '../utils/hotCacheGate';
|
||||
import { resolvePlaybackUrl } from '../utils/resolvePlaybackUrl';
|
||||
import { resolveReplayGainDb } from '../utils/resolveReplayGainDb';
|
||||
import { useAuthStore } from './authStore';
|
||||
import { getPlayGeneration, setIsAudioPaused } from './engineState';
|
||||
import { touchHotCacheOnPlayback } from './hotCacheTouch';
|
||||
import { isReplayGainActive, loudnessGainDbForEngineBind } from './loudnessGainCache';
|
||||
import { playbackSourceHintForResolvedUrl, recordEnginePlayUrl } from './playbackUrlRouting';
|
||||
import { usePlayerStore, type Track } from './playerStore';
|
||||
|
||||
/**
|
||||
* Reload the Rust audio engine to match a queue-undo snapshot. Zustand
|
||||
* alone can rewrite the queue + currentTrack, but the engine is still
|
||||
* playing whatever cold-started before the undo — so we need a full
|
||||
* `audio_play` (+ optional `audio_seek` to the snapshot position) to
|
||||
* line the audible playback back up with the restored UI state.
|
||||
*
|
||||
* Captures the play-generation at start so a later concurrent `playTrack`
|
||||
* (e.g. user clicks another track) invalidates the seek/pause follow-up
|
||||
* without clobbering the new engine state.
|
||||
*/
|
||||
export function queueUndoRestoreAudioEngine(opts: {
|
||||
generation: number;
|
||||
track: Track;
|
||||
queue: Track[];
|
||||
queueIndex: number;
|
||||
atSeconds: number;
|
||||
wantPlaying: boolean;
|
||||
}): void {
|
||||
const { generation, track, queue, queueIndex, atSeconds, wantPlaying } = opts;
|
||||
const authState = useAuthStore.getState();
|
||||
const vol = usePlayerStore.getState().volume;
|
||||
const coldPrev = queueIndex > 0 ? queue[queueIndex - 1] : null;
|
||||
const coldNext = queueIndex + 1 < queue.length ? queue[queueIndex + 1] : null;
|
||||
const replayGainDb = resolveReplayGainDb(
|
||||
track, coldPrev, coldNext,
|
||||
isReplayGainActive(), authState.replayGainMode,
|
||||
);
|
||||
const replayGainPeak = isReplayGainActive() ? (track.replayGainPeak ?? null) : null;
|
||||
const url = resolvePlaybackUrl(track.id, authState.activeServerId ?? '');
|
||||
recordEnginePlayUrl(track.id, url);
|
||||
usePlayerStore.setState({
|
||||
currentPlaybackSource: playbackSourceHintForResolvedUrl(track.id, authState.activeServerId ?? '', url),
|
||||
});
|
||||
const keepPreloadHint = usePlayerStore.getState().enginePreloadedTrackId === track.id;
|
||||
setDeferHotCachePrefetch(true);
|
||||
invoke('audio_play', {
|
||||
url,
|
||||
volume: vol,
|
||||
durationHint: track.duration,
|
||||
replayGainDb,
|
||||
replayGainPeak,
|
||||
loudnessGainDb: loudnessGainDbForEngineBind(track.id),
|
||||
preGainDb: authState.replayGainPreGainDb,
|
||||
fallbackDb: authState.replayGainFallbackDb,
|
||||
manual: false,
|
||||
hiResEnabled: authState.enableHiRes,
|
||||
analysisTrackId: track.id,
|
||||
streamFormatSuffix: track.suffix ?? null,
|
||||
})
|
||||
.then(() => {
|
||||
if (getPlayGeneration() !== generation) return;
|
||||
if (keepPreloadHint) {
|
||||
usePlayerStore.setState({ enginePreloadedTrackId: null });
|
||||
}
|
||||
const dur = track.duration && track.duration > 0 ? track.duration : null;
|
||||
const seekTo = Math.max(0, atSeconds);
|
||||
const canSeek = seekTo > 0.05 && (dur == null || seekTo < dur - 0.05);
|
||||
const afterSeek = () => {
|
||||
if (getPlayGeneration() !== generation) return;
|
||||
if (!wantPlaying) {
|
||||
invoke('audio_pause').catch(console.error);
|
||||
setIsAudioPaused(true);
|
||||
usePlayerStore.setState({ isPlaying: false });
|
||||
} else {
|
||||
setIsAudioPaused(false);
|
||||
}
|
||||
};
|
||||
if (canSeek) {
|
||||
void invoke('audio_seek', { seconds: seekTo }).then(afterSeek).catch(afterSeek);
|
||||
} else {
|
||||
afterSeek();
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (getPlayGeneration() !== generation) return;
|
||||
console.error('[psysonic] queue-undo audio_play failed:', err);
|
||||
usePlayerStore.setState({ isPlaying: false });
|
||||
})
|
||||
.finally(() => {
|
||||
setDeferHotCachePrefetch(false);
|
||||
});
|
||||
touchHotCacheOnPlayback(track.id, authState.activeServerId ?? '');
|
||||
}
|
||||
Reference in New Issue
Block a user