mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
feat(audio): bit-perfect hi-res playback + underrun hardening (opt-in alpha)
Safe mode (default): all audio outputs at 44.1 kHz, rodio resamples internally. Maximum stability out of the box. Hi-Res mode (alpha toggle): stream opens at the file's native sample rate (e.g. 88.2/96/192 kHz) for bit-perfect output. Rust (audio.rs): - open_stream_for_rate(): CPAL queries device supported configs, finds exact rate match or falls back to highest available / device default. - create_engine() thread: ThreadPriority::Max (silently ignored without CAP_SYS_NICE), loop handles rate-switch requests, PIPEWIRE_LATENCY scales with rate (8192 frames for >48 kHz ≈ 93 ms quantum), keeps PULSE_LATENCY_MSEC in sync. - audio_play(): hi_res_enabled param gates effective_rate (44100 vs native); stream re-opens only when needed; 150 ms PipeWire settle + 500 ms sink pre-fill (pause→append→sleep→play) for hi-res tracks. - audio_chain_preload(): hi_res_enabled param; rate-mismatch bail skipped in safe mode so gapless chains always work at 44.1 kHz. - SizedDecoder MSS buffer: 4 MB (was 512 KB) to reduce Symphonia read overhead on high-bitrate hi-res files. - thread-priority crate added to Cargo.toml. Frontend: - authStore: enableHiRes (bool, default false) + setEnableHiRes. - playerStore: hiResEnabled passed to all audio_play / audio_chain_preload invoke calls. - Settings → Audio tab: "Native Hi-Res Playback" toggle with Alpha badge and stability warning, i18n for all 7 languages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,9 @@ interface AuthState {
|
||||
showChangelogOnUpdate: boolean;
|
||||
lastSeenChangelogVersion: string;
|
||||
|
||||
/** Alpha: native hi-res sample rate output (disabled = safe 44.1 kHz mode) */
|
||||
enableHiRes: boolean;
|
||||
|
||||
/** Alpha: ephemeral queue prefetch cache on disk */
|
||||
hotCacheEnabled: boolean;
|
||||
hotCacheMaxMb: number;
|
||||
@@ -95,6 +98,7 @@ interface AuthState {
|
||||
setShowFullscreenLyrics: (v: boolean) => void;
|
||||
setShowChangelogOnUpdate: (v: boolean) => void;
|
||||
setLastSeenChangelogVersion: (v: string) => void;
|
||||
setEnableHiRes: (v: boolean) => void;
|
||||
setHotCacheEnabled: (v: boolean) => void;
|
||||
setHotCacheMaxMb: (v: number) => void;
|
||||
setHotCacheDebounceSec: (v: number) => void;
|
||||
@@ -142,6 +146,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
showFullscreenLyrics: true,
|
||||
showChangelogOnUpdate: true,
|
||||
lastSeenChangelogVersion: '',
|
||||
enableHiRes: false,
|
||||
hotCacheEnabled: false,
|
||||
hotCacheMaxMb: 256,
|
||||
hotCacheDebounceSec: 30,
|
||||
@@ -222,6 +227,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
setShowChangelogOnUpdate: (v) => set({ showChangelogOnUpdate: v }),
|
||||
setLastSeenChangelogVersion: (v) => set({ lastSeenChangelogVersion: v }),
|
||||
|
||||
setEnableHiRes: (v) => set({ enableHiRes: v }),
|
||||
setHotCacheEnabled: (v) => set({ hotCacheEnabled: v }),
|
||||
setHotCacheMaxMb: (v) => set({ hotCacheMaxMb: v }),
|
||||
setHotCacheDebounceSec: (v) => set({ hotCacheDebounceSec: v }),
|
||||
|
||||
@@ -310,6 +310,7 @@ function handleAudioProgress(current_time: number, duration: number) {
|
||||
durationHint: nextTrack.duration,
|
||||
replayGainDb,
|
||||
replayGainPeak,
|
||||
hiResEnabled: useAuthStore.getState().enableHiRes,
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
// Gapless OFF: just pre-download bytes so audio_play finds them cached.
|
||||
@@ -744,6 +745,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
replayGainDb,
|
||||
replayGainPeak,
|
||||
manual,
|
||||
hiResEnabled: authState.enableHiRes,
|
||||
}).catch((err: unknown) => {
|
||||
if (playGeneration !== gen) return;
|
||||
setDeferHotCachePrefetch(false);
|
||||
@@ -825,6 +827,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
replayGainDb: replayGainDbCold,
|
||||
manual: false,
|
||||
replayGainPeak: replayGainPeakCold,
|
||||
hiResEnabled: useAuthStore.getState().enableHiRes,
|
||||
}).then(() => {
|
||||
if (playGeneration === gen && currentTime > 1) {
|
||||
invoke('audio_seek', { seconds: currentTime }).catch(console.error);
|
||||
@@ -855,6 +858,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
replayGainDb: replayGainDbCold,
|
||||
replayGainPeak: replayGainPeakCold,
|
||||
manual: false,
|
||||
hiResEnabled: useAuthStore.getState().enableHiRes,
|
||||
}).catch((err: unknown) => {
|
||||
if (playGeneration !== gen) return;
|
||||
setDeferHotCachePrefetch(false);
|
||||
|
||||
Reference in New Issue
Block a user