mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
44287a7ceb
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>
256 lines
8.9 KiB
TypeScript
256 lines
8.9 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { usePlayerStore } from './playerStore';
|
|
|
|
export interface ServerProfile {
|
|
id: string;
|
|
name: string;
|
|
url: string;
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
interface AuthState {
|
|
// Multi-server
|
|
servers: ServerProfile[];
|
|
activeServerId: string | null;
|
|
|
|
// Last.fm (global)
|
|
lastfmApiKey: string;
|
|
lastfmApiSecret: string;
|
|
lastfmSessionKey: string;
|
|
lastfmUsername: string;
|
|
|
|
// Settings (global)
|
|
scrobblingEnabled: boolean;
|
|
maxCacheMb: number;
|
|
downloadFolder: string;
|
|
offlineDownloadDir: string;
|
|
excludeAudiobooks: boolean;
|
|
customGenreBlacklist: string[];
|
|
replayGainEnabled: boolean;
|
|
replayGainMode: 'track' | 'album';
|
|
crossfadeEnabled: boolean;
|
|
crossfadeSecs: number;
|
|
gaplessEnabled: boolean;
|
|
preloadMode: 'balanced' | 'early' | 'custom';
|
|
preloadCustomSeconds: number;
|
|
infiniteQueueEnabled: boolean;
|
|
showArtistImages: boolean;
|
|
showTrayIcon: boolean;
|
|
minimizeToTray: boolean;
|
|
discordRichPresence: boolean;
|
|
nowPlayingEnabled: boolean;
|
|
lyricsServerFirst: boolean;
|
|
showFullscreenLyrics: boolean;
|
|
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;
|
|
hotCacheDebounceSec: number;
|
|
/** Parent directory; actual cache is `<dir>/psysonic-hot-cache/`. Empty = app data. */
|
|
hotCacheDownloadDir: string;
|
|
|
|
// Status
|
|
isLoggedIn: boolean;
|
|
isConnecting: boolean;
|
|
connectionError: string | null;
|
|
lastfmSessionError: boolean;
|
|
|
|
// Actions
|
|
addServer: (profile: Omit<ServerProfile, 'id'>) => string;
|
|
updateServer: (id: string, data: Partial<Omit<ServerProfile, 'id'>>) => void;
|
|
removeServer: (id: string) => void;
|
|
setActiveServer: (id: string) => void;
|
|
setLoggedIn: (v: boolean) => void;
|
|
setConnecting: (v: boolean) => void;
|
|
setConnectionError: (e: string | null) => void;
|
|
setLastfm: (apiKey: string, apiSecret: string, sessionKey: string, username: string) => void;
|
|
connectLastfm: (sessionKey: string, username: string) => void;
|
|
disconnectLastfm: () => void;
|
|
setLastfmSessionError: (v: boolean) => void;
|
|
setScrobblingEnabled: (v: boolean) => void;
|
|
setMaxCacheMb: (v: number) => void;
|
|
setDownloadFolder: (v: string) => void;
|
|
setOfflineDownloadDir: (v: string) => void;
|
|
setExcludeAudiobooks: (v: boolean) => void;
|
|
setCustomGenreBlacklist: (v: string[]) => void;
|
|
setReplayGainEnabled: (v: boolean) => void;
|
|
setReplayGainMode: (v: 'track' | 'album') => void;
|
|
setCrossfadeEnabled: (v: boolean) => void;
|
|
setCrossfadeSecs: (v: number) => void;
|
|
setGaplessEnabled: (v: boolean) => void;
|
|
setPreloadMode: (v: 'balanced' | 'early' | 'custom') => void;
|
|
setPreloadCustomSeconds: (v: number) => void;
|
|
setInfiniteQueueEnabled: (v: boolean) => void;
|
|
setShowArtistImages: (v: boolean) => void;
|
|
setShowTrayIcon: (v: boolean) => void;
|
|
setMinimizeToTray: (v: boolean) => void;
|
|
setDiscordRichPresence: (v: boolean) => void;
|
|
setNowPlayingEnabled: (v: boolean) => void;
|
|
setLyricsServerFirst: (v: boolean) => void;
|
|
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;
|
|
setHotCacheDownloadDir: (v: string) => void;
|
|
logout: () => void;
|
|
|
|
// Derived
|
|
getBaseUrl: () => string;
|
|
getActiveServer: () => ServerProfile | undefined;
|
|
}
|
|
|
|
function generateId(): string {
|
|
return Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
servers: [],
|
|
activeServerId: null,
|
|
lastfmApiKey: '',
|
|
lastfmApiSecret: '',
|
|
lastfmSessionKey: '',
|
|
lastfmUsername: '',
|
|
scrobblingEnabled: true,
|
|
maxCacheMb: 500,
|
|
downloadFolder: '',
|
|
offlineDownloadDir: '',
|
|
excludeAudiobooks: false,
|
|
customGenreBlacklist: [],
|
|
replayGainEnabled: false,
|
|
replayGainMode: 'track',
|
|
crossfadeEnabled: false,
|
|
crossfadeSecs: 3,
|
|
gaplessEnabled: false,
|
|
preloadMode: 'balanced',
|
|
preloadCustomSeconds: 30,
|
|
infiniteQueueEnabled: false,
|
|
showArtistImages: false,
|
|
showTrayIcon: true,
|
|
minimizeToTray: false,
|
|
discordRichPresence: false,
|
|
nowPlayingEnabled: false,
|
|
lyricsServerFirst: true,
|
|
showFullscreenLyrics: true,
|
|
showChangelogOnUpdate: true,
|
|
lastSeenChangelogVersion: '',
|
|
enableHiRes: false,
|
|
hotCacheEnabled: false,
|
|
hotCacheMaxMb: 256,
|
|
hotCacheDebounceSec: 30,
|
|
hotCacheDownloadDir: '',
|
|
isLoggedIn: false,
|
|
isConnecting: false,
|
|
connectionError: null,
|
|
lastfmSessionError: false,
|
|
|
|
addServer: (profile) => {
|
|
const id = generateId();
|
|
set(s => ({ servers: [...s.servers, { ...profile, id }] }));
|
|
return id;
|
|
},
|
|
|
|
updateServer: (id, data) => {
|
|
set(s => ({
|
|
servers: s.servers.map(srv => srv.id === id ? { ...srv, ...data } : srv),
|
|
}));
|
|
},
|
|
|
|
removeServer: (id) => {
|
|
set(s => {
|
|
const newServers = s.servers.filter(srv => srv.id !== id);
|
|
const switchedAway = s.activeServerId === id;
|
|
return {
|
|
servers: newServers,
|
|
activeServerId: switchedAway ? (newServers[0]?.id ?? null) : s.activeServerId,
|
|
isLoggedIn: switchedAway ? false : s.isLoggedIn,
|
|
};
|
|
});
|
|
},
|
|
|
|
setActiveServer: (id) => set({ activeServerId: id }),
|
|
|
|
setLoggedIn: (v) => set({ isLoggedIn: v }),
|
|
setConnecting: (v) => set({ isConnecting: v }),
|
|
setConnectionError: (e) => set({ connectionError: e }),
|
|
|
|
setLastfm: (apiKey, apiSecret, sessionKey, username) =>
|
|
set({ lastfmApiKey: apiKey, lastfmApiSecret: apiSecret, lastfmSessionKey: sessionKey, lastfmUsername: username }),
|
|
|
|
connectLastfm: (sessionKey, username) =>
|
|
set({ lastfmSessionKey: sessionKey, lastfmUsername: username }),
|
|
|
|
disconnectLastfm: () =>
|
|
set({ lastfmSessionKey: '', lastfmUsername: '', lastfmSessionError: false }),
|
|
|
|
setLastfmSessionError: (v) => set({ lastfmSessionError: v }),
|
|
|
|
setScrobblingEnabled: (v) => set({ scrobblingEnabled: v }),
|
|
setMaxCacheMb: (v) => set({ maxCacheMb: v }),
|
|
setDownloadFolder: (v) => set({ downloadFolder: v }),
|
|
setOfflineDownloadDir: (v) => set({ offlineDownloadDir: v }),
|
|
setExcludeAudiobooks: (v) => set({ excludeAudiobooks: v }),
|
|
setCustomGenreBlacklist: (v) => set({ customGenreBlacklist: v }),
|
|
setReplayGainEnabled: (v) => {
|
|
set({ replayGainEnabled: v });
|
|
usePlayerStore.getState().updateReplayGainForCurrentTrack();
|
|
},
|
|
setReplayGainMode: (v) => {
|
|
set({ replayGainMode: v });
|
|
usePlayerStore.getState().updateReplayGainForCurrentTrack();
|
|
},
|
|
setCrossfadeEnabled: (v) => set({ crossfadeEnabled: v }),
|
|
setCrossfadeSecs: (v) => set({ crossfadeSecs: v }),
|
|
setGaplessEnabled: (v) => set({ gaplessEnabled: v }),
|
|
setPreloadMode: (v: 'balanced' | 'early' | 'custom') => set({ preloadMode: v }),
|
|
setPreloadCustomSeconds: (v: number) => set({ preloadCustomSeconds: v }),
|
|
setInfiniteQueueEnabled: (v) => set({ infiniteQueueEnabled: v }),
|
|
setShowArtistImages: (v) => set({ showArtistImages: v }),
|
|
setShowTrayIcon: (v) => set({ showTrayIcon: v }),
|
|
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
|
|
setDiscordRichPresence: (v) => set({ discordRichPresence: v }),
|
|
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
|
|
setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }),
|
|
setShowFullscreenLyrics: (v: boolean) => set({ showFullscreenLyrics: v }),
|
|
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 }),
|
|
setHotCacheDownloadDir: (v) => set({ hotCacheDownloadDir: v }),
|
|
|
|
logout: () => set({ isLoggedIn: false }),
|
|
|
|
getBaseUrl: () => {
|
|
const s = get();
|
|
const server = s.servers.find(srv => srv.id === s.activeServerId);
|
|
if (!server?.url) return '';
|
|
return server.url.startsWith('http') ? server.url : `http://${server.url}`;
|
|
},
|
|
|
|
getActiveServer: () => {
|
|
const s = get();
|
|
return s.servers.find(srv => srv.id === s.activeServerId);
|
|
},
|
|
}),
|
|
{
|
|
name: 'psysonic-auth',
|
|
storage: createJSONStorage(() => localStorage),
|
|
}
|
|
)
|
|
);
|