feat(audio): hi-res transition blend rate for crossfade, AutoDJ, and gapless (#1171)

This commit is contained in:
cucadmuh
2026-06-24 21:04:42 +03:00
committed by GitHub
parent 0b7b5df30c
commit 09f24beb32
32 changed files with 737 additions and 60 deletions
+7
View File
@@ -14,6 +14,7 @@ import { NormalizationBlock } from './audio/NormalizationBlock';
import { PlaybackRateBlock } from './audio/PlaybackRateBlock';
import { TrackTransitionsBlock } from './audio/TrackTransitionsBlock';
import { TrackPreviewsSection } from './audio/TrackPreviewsSection';
import { HiResCrossfadeResampleBlock } from './audio/HiResCrossfadeResampleBlock';
export function AudioTab() {
const { t } = useTranslation();
@@ -87,6 +88,12 @@ export function AudioTab() {
checked={auth.enableHiRes}
onChange={auth.setEnableHiRes}
/>
<HiResCrossfadeResampleBlock
enabled={auth.enableHiRes}
resampleHz={auth.hiResCrossfadeResampleHz}
onResampleHzChange={auth.setHiResCrossfadeResampleHz}
t={t}
/>
</SettingsGroup>
</div>
</SettingsSubSection>
@@ -0,0 +1,56 @@
import {
HI_RES_CROSSFADE_RESAMPLE_OPTIONS,
type HiResCrossfadeResampleHz,
sanitizeHiResCrossfadeResampleHz,
} from '../../../utils/audio/hiResCrossfadeResample';
import type { TFunction } from 'i18next';
import { SettingsGroup } from '../SettingsGroup';
interface Props {
enabled: boolean;
resampleHz: HiResCrossfadeResampleHz;
onResampleHzChange: (hz: HiResCrossfadeResampleHz) => void;
t: TFunction;
}
function labelForHz(t: TFunction, hz: HiResCrossfadeResampleHz): string {
if (hz === 88_200) return t('settings.hiResCrossfadeResample88');
if (hz === 96_000) return t('settings.hiResCrossfadeResample96');
return t('settings.hiResCrossfadeResample44');
}
/** Hi-Res crossfade / AutoDJ / gapless blend-rate picker (visible when hi-res is on). */
export function HiResCrossfadeResampleBlock({
enabled,
resampleHz,
onResampleHzChange,
t,
}: Props) {
if (!enabled) return null;
return (
<SettingsGroup>
<p className="settings-row-label" style={{ marginBottom: '0.5rem' }}>
{t('settings.hiResCrossfadeResampleTitle')}
</p>
<p className="settings-row-desc" style={{ marginBottom: '0.75rem' }}>
{t('settings.hiResCrossfadeResampleDesc')}
</p>
<div className="settings-segmented" style={{ marginBottom: '0.75rem' }}>
{HI_RES_CROSSFADE_RESAMPLE_OPTIONS.map((hz) => (
<button
key={hz}
type="button"
className={`btn ${resampleHz === hz ? 'btn-primary' : 'btn-ghost'}`}
onClick={() => onResampleHzChange(sanitizeHiResCrossfadeResampleHz(hz))}
>
{labelForHz(t, hz)}
</button>
))}
</div>
<p className="settings-row-desc" role="note" style={{ marginBottom: 0, opacity: 0.85 }}>
{t('settings.hiResCrossfadeResampleWarning')}
</p>
</SettingsGroup>
);
}
+1 -1
View File
@@ -34,7 +34,7 @@ export type SearchIndexEntry = { tab: Tab; titleKey: string; keywords?: string;
export const SETTINGS_INDEX: SearchIndexEntry[] = [
{ tab: 'audio', titleKey: 'settings.audioOutputDevice', keywords: 'output device speakers headphones alsa wasapi coreaudio' },
{ tab: 'audio', titleKey: 'settings.hiResTitle', keywords: 'hi-res hires resampling bit depth sample rate dsd 24bit' },
{ tab: 'audio', titleKey: 'settings.hiResTitle', keywords: 'hi-res hires resampling bit depth sample rate dsd 24bit crossfade autodj blend 44 88 96' },
{ tab: 'audio', titleKey: 'settings.eqTitle', keywords: 'equalizer eq bass treble autoeq filter pre-gain' },
{ tab: 'audio', titleKey: 'settings.playbackRateTitle', keywords: 'speed playback rate tempo pitch varispeed preserve corrected time stretch' },
{ tab: 'audio', titleKey: 'settings.normalization', keywords: 'normalization normalisation loudness volume leveling level replaygain replay gain lufs pre-gain' },
+1
View File
@@ -174,6 +174,7 @@ const CONTRIBUTOR_ENTRIES = [
'All Albums browse: compilation and favorites filters in slice mode — skip redundant client comp filter, route favorites through getStarred2, pre-index page scan, offline isCompilation from tracks (PR #1151)',
'Custom HTTP headers for reverse-proxy gates — per-server header editor, TS/Rust registry sync, full playback/sync/cover path coverage, log redaction (PR #1156)',
'CI: ESLint strict workflow and path-aware ci-ok merge gate (PR #1170)',
'Hi-Res transition blend rate — configurable 44.1/88.2/96 kHz resampling for crossfade, AutoDJ, and gapless when adjacent tracks differ in sample rate (PR #1171)',
],
},
{
+6
View File
@@ -235,6 +235,12 @@ export const settings = {
hiResTitle: 'Native Hi-Res-Wiedergabe',
hiResEnabled: 'Native Hi-Res-Wiedergabe aktivieren',
hiResDesc: "Spielt jeden Titel mit seiner ursprünglichen Abtastrate ab, statt alles auf 44,1 kHz herunterzurechnen, und stellt das Ausgabegerät passend zur Datei um (88,2 kHz und höher). Nur aktivieren, wenn Hardware und Netzwerk hohe Abtastraten zuverlässig verarbeiten.",
hiResCrossfadeResampleTitle: 'Crossfade / AutoDJ / Gapless Mischrate',
hiResCrossfadeResampleDesc: 'Wenn benachbarte Titel unterschiedliche Abtastraten haben, werden beide Seiten für Crossfade-, AutoDJ- und Gapless-Übergänge auf diese Rate resampled.',
hiResCrossfadeResample44: '44,1 kHz',
hiResCrossfadeResample88: '88,2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: 'Resampling verbraucht zusätzliche CPU und Speicher — auf langsameren Geräten vorsichtig wählen.',
showArtistImages: 'Künstlerbilder anzeigen',
showArtistImagesDesc: 'Lädt und zeigt Künstlerbilder in der Künstlerübersicht. Standardmäßig deaktiviert, um Server-I/O und Netzwerklast bei großen Bibliotheken zu reduzieren.',
showOrbitTrigger: '„Orbit" im Header zeigen',
+6
View File
@@ -238,6 +238,12 @@ export const settings = {
hiResTitle: 'Native Hi-Res Playback',
hiResEnabled: 'Enable native hi-res playback',
hiResDesc: "Plays each track at its original sample rate instead of resampling everything to 44.1 kHz, switching the audio device to match the file (88.2 kHz and up). Enable only if your hardware and network reliably handle high sample rates.",
hiResCrossfadeResampleTitle: 'Crossfade / AutoDJ / Gapless blend rate',
hiResCrossfadeResampleDesc: 'When adjacent tracks have different sample rates, both sides are resampled to this rate during crossfade, AutoDJ, and gapless transitions.',
hiResCrossfadeResample44: '44.1 kHz',
hiResCrossfadeResample88: '88.2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: 'Resampling uses extra CPU and memory — choose carefully on slower devices.',
showArtistImages: 'Show Artist Images',
showArtistImagesDesc: 'Load and display artist images in the Artists overview. Disabled by default to reduce server disk I/O and network load on large libraries.',
showOrbitTrigger: 'Show "Orbit" in the header',
+6
View File
@@ -234,6 +234,12 @@ export const settings = {
hiResTitle: 'Reproducción Nativa Hi-Res',
hiResEnabled: 'Habilitar reproducción nativa hi-res',
hiResDesc: "Reproduce cada pista a su frecuencia de muestreo original en vez de remuestrear todo a 44.1 kHz, ajustando el dispositivo de salida para que coincida con el archivo (88.2 kHz o más). Habilítalo solo si tu hardware y tu red soportan de forma fiable altas tasas de muestreo.",
hiResCrossfadeResampleTitle: 'Frecuencia de mezcla Crossfade / AutoDJ / Gapless',
hiResCrossfadeResampleDesc: 'Cuando pistas adyacentes tienen distinta frecuencia de muestreo, ambos lados se remuestrean a esta frecuencia durante crossfade, AutoDJ y transiciones gapless.',
hiResCrossfadeResample44: '44,1 kHz',
hiResCrossfadeResample88: '88,2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: 'El remuestreo consume más CPU y memoria — elige con cuidado en dispositivos lentos.',
showArtistImages: 'Mostrar Imágenes de Artistas',
showArtistImagesDesc: 'Carga y muestra imágenes de artistas en el resumen de Artistas. Desactivado por defecto para reducir I/O de disco del servidor y carga de red en bibliotecas grandes.',
showOrbitTrigger: 'Mostrar "Orbit" en la cabecera',
+6
View File
@@ -234,6 +234,12 @@ export const settings = {
hiResTitle: 'Lecture haute résolution native',
hiResEnabled: 'Activer la lecture haute résolution native',
hiResDesc: "Lit chaque piste à sa fréquence d'échantillonnage d'origine au lieu de tout rééchantillonner à 44,1 kHz, en réglant le périphérique de sortie sur celle du fichier (88,2 kHz et plus). N'activer que si le matériel et le réseau gèrent ces hautes fréquences de façon fiable.",
hiResCrossfadeResampleTitle: 'Fréquence de mixage Crossfade / AutoDJ / Gapless',
hiResCrossfadeResampleDesc: 'Lorsque des pistes voisines ont des fréquences d\'échantillonnage différentes, les deux côtés sont rééchantillonnés à cette fréquence pendant le crossfade, AutoDJ et les transitions gapless.',
hiResCrossfadeResample44: '44,1 kHz',
hiResCrossfadeResample88: '88,2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: 'Le rééchantillonnage utilise davantage de CPU et de mémoire — choisissez avec prudence sur les appareils lents.',
showArtistImages: 'Afficher les images d\'artistes',
showArtistImagesDesc: 'Charge et affiche les images d\'artistes dans la vue d\'ensemble. Désactivé par défaut pour réduire les E/S disque serveur et la charge réseau sur les grandes bibliothèques.',
showOrbitTrigger: 'Afficher « Orbit » dans l\'en-tête',
+6
View File
@@ -238,6 +238,12 @@ export const settings = {
hiResTitle: 'Natív Hi-Res lejátszás',
hiResEnabled: 'Natív Hi-Res lejátszás engedélyezése',
hiResDesc: 'Minden számot az eredeti mintavételezési frekvenciáján játszik le, ahelyett, hogy mindent 44,1 kHz-re mintavételezne újra, és a hangeszközt a fájlhoz igazítja (88,2 kHz-től felfelé). Csak akkor engedélyezd, ha a hardvered és a hálózatod megbízhatóan kezeli a magas mintavételezési frekvenciákat.',
hiResCrossfadeResampleTitle: 'Crossfade / AutoDJ / Gapless keverési frekvencia',
hiResCrossfadeResampleDesc: 'Ha a szomszédos számok mintavételezési frekvenciája eltér, mindkét oldal erre a frekvenciára kerül átmintavételezésre crossfade, AutoDJ és gapless átmeneteknél.',
hiResCrossfadeResample44: '44,1 kHz',
hiResCrossfadeResample88: '88,2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: 'Az átmintavételezés extra CPU-t és memóriát igényel — lassabb eszközökön válassz körültekintően.',
showArtistImages: 'Előadói képek megjelenítése',
showArtistImagesDesc: 'Az előadói képek betöltése és megjelenítése az Előadók áttekintőben. Alapértelmezetten letiltva a szerver lemez-I/O és hálózati terhelésének csökkentése érdekében nagy könyvtáraknál.',
showOrbitTrigger: 'Az „Orbit" megjelenítése a fejlécben',
+6
View File
@@ -238,6 +238,12 @@ export const settings = {
hiResTitle: 'ネイティブ Hi-Res 再生',
hiResEnabled: 'ネイティブ Hi-Res 再生を有効化',
hiResDesc: 'すべてを 44.1 kHz にリサンプリングせず、各トラックを元のサンプルレートで再生し、ファイルに合わせて音声デバイスを切り替えます (88.2 kHz 以上)。高サンプルレートをハードウェアとネットワークが安定して扱える場合のみ有効にしてください。',
hiResCrossfadeResampleTitle: 'クロスフェード / AutoDJ / Gapless ブレンドレート',
hiResCrossfadeResampleDesc: '隣接トラックのサンプルレートが異なる場合、クロスフェード・AutoDJ・ギャップレス遷移中は両方をこのレートにリサンプリングします。',
hiResCrossfadeResample44: '44.1 kHz',
hiResCrossfadeResample88: '88.2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: 'リサンプリングは CPU とメモリを追加で消費します。低速デバイスでは慎重に選んでください。',
showArtistImages: 'アーティスト画像を表示',
showArtistImagesDesc: 'アーティスト一覧でアーティスト画像を読み込んで表示します。大規模ライブラリでサーバーディスク I/O とネットワーク負荷を減らすため、既定では無効です。',
showOrbitTrigger: 'ヘッダーに "Orbit" を表示',
+6
View File
@@ -235,6 +235,12 @@ export const settings = {
hiResTitle: 'Innebygd hi-res-avspilling',
hiResEnabled: 'Aktiver innebygd hi-res-avspilling',
hiResDesc: "Spiller hvert spor med sin opprinnelige samplingsrate i stedet for å nedsample alt til 44,1 kHz, og stiller utdataenheten etter filen (88,2 kHz og høyere). Aktiver kun hvis maskinvare og nettverk håndterer høye samplingsrater pålitelig.",
hiResCrossfadeResampleTitle: 'Crossfade / AutoDJ / Gapless blandingsrate',
hiResCrossfadeResampleDesc: 'Når nabospor har ulik samplingsrate, resamples begge sider til denne raten under crossfade-, AutoDJ- og gapless-overganger.',
hiResCrossfadeResample44: '44,1 kHz',
hiResCrossfadeResample88: '88,2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: 'Resampling bruker ekstra CPU og minne — velg forsiktig på tregere enheter.',
showArtistImages: 'Vis artistbilder',
showArtistImagesDesc: 'Last inn og vis artistbilder i artistoversikten. Denne er deaktivert som standard, for å redusere disk-I/O og nettverksbelastningen på store biblioteker.',
showOrbitTrigger: 'Vis «Orbit» i toppen',
+6
View File
@@ -234,6 +234,12 @@ export const settings = {
hiResTitle: 'Natieve hi-res-weergave',
hiResEnabled: 'Natieve hi-res-weergave inschakelen',
hiResDesc: "Speelt elke track af op zijn oorspronkelijke samplerate in plaats van alles te herbemonsteren naar 44,1 kHz, en stelt het uitvoerapparaat af op het bestand (88,2 kHz en hoger). Alleen inschakelen als hardware en netwerk hoge samplerates betrouwbaar aankunnen.",
hiResCrossfadeResampleTitle: 'Crossfade / AutoDJ / Gapless mengfrequentie',
hiResCrossfadeResampleDesc: 'Als aangrenzende tracks verschillende samplerates hebben, worden beide kanten naar deze frequentie geresampled tijdens crossfade-, AutoDJ- en gapless-overgangen.',
hiResCrossfadeResample44: '44,1 kHz',
hiResCrossfadeResample88: '88,2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: 'Resampling kost extra CPU en geheugen — kies voorzichtig op tragere apparaten.',
showArtistImages: 'Artiestafbeeldingen weergeven',
showArtistImagesDesc: 'Laadt en toont artiestafbeeldingen in het artiestenoverzicht. Standaard uitgeschakeld om server-I/O en netwerkbelasting bij grote bibliotheken te beperken.',
showOrbitTrigger: '"Orbit" in de kop tonen',
+6
View File
@@ -237,6 +237,12 @@ export const settings = {
hiResTitle: 'Playback Hi-Res nativ',
hiResEnabled: 'Pornește playback-ul hi-res nativ',
hiResDesc: "Redă fiecare piesă la rata sa de eșantionare originală în loc să reeșantioneze totul la 44.1 kHz, comutând dispozitivul de ieșire pentru a se potrivi cu fișierul (88.2 kHz și peste). Pornește doar dacă hardware-ul și rețeaua gestionează fiabil rate mari de eșantionare.",
hiResCrossfadeResampleTitle: 'Rată de mixaj Crossfade / AutoDJ / Gapless',
hiResCrossfadeResampleDesc: 'Când piesele adiacente au rate de eșantionare diferite, ambele părți sunt reeșantionate la această rată în timpul crossfade-ului, AutoDJ și tranzițiilor gapless.',
hiResCrossfadeResample44: '44,1 kHz',
hiResCrossfadeResample88: '88,2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: 'Reeșantionarea consumă CPU și memorie suplimentară — alege cu atenție pe dispozitive mai lente.',
showArtistImages: 'Afișează Imagini Artist',
showArtistImagesDesc: 'Încarcă și afișează imagini artist în Prezentarea generală a Artiștilor. Oprit implicit pentru a reduce I/O pe server și încărcarea rețelei pe librării mari.',
showOrbitTrigger: 'Afișează "Orbit" în antet',
+6
View File
@@ -238,6 +238,12 @@ export const settings = {
hiResTitle: 'Нативное воспроизведение Hi-Res',
hiResEnabled: 'Включить нативное Hi-Res воспроизведение',
hiResDesc: "Воспроизводит каждый трек с его исходной частотой дискретизации вместо передискретизации всего в 44,1 кГц, переключая устройство вывода под частоту файла (88,2 кГц и выше). Включайте только если оборудование и сеть надёжно справляются с высокими частотами.",
hiResCrossfadeResampleTitle: 'Частота сведения Crossfade / AutoDJ / Gapless',
hiResCrossfadeResampleDesc: 'Когда соседние треки имеют разную частоту дискретизации, обе стороны передискретизируются на эту частоту во время кроссфейда, AutoDJ и gapless-переходов.',
hiResCrossfadeResample44: '44,1 кГц',
hiResCrossfadeResample88: '88,2 кГц',
hiResCrossfadeResample96: '96 кГц',
hiResCrossfadeResampleWarning: 'Передискретизация увеличивает нагрузку на CPU и память — на слабых устройствах выбирайте с осторожностью.',
showArtistImages: 'Фото исполнителей',
showArtistImagesDesc:
'Показывать обложки в разделе «Исполнители». По умолчанию выключено — меньше нагрузки на диск и сеть.',
+6
View File
@@ -234,6 +234,12 @@ export const settings = {
hiResTitle: '原生高清晰度播放',
hiResEnabled: '启用原生高清晰度播放',
hiResDesc: "以每个曲目的原始采样率播放,而不是将所有内容重采样到 44.1 kHz,并将输出设备切换为与文件匹配(88.2 kHz 及以上)。仅在硬件和网络能可靠处理高采样率时启用。",
hiResCrossfadeResampleTitle: '交叉淡化 / AutoDJ / Gapless 混合采样率',
hiResCrossfadeResampleDesc: '当相邻曲目的采样率不同时,在交叉淡化、AutoDJ 和无缝过渡期间将两侧重采样到此采样率。',
hiResCrossfadeResample44: '44.1 kHz',
hiResCrossfadeResample88: '88.2 kHz',
hiResCrossfadeResample96: '96 kHz',
hiResCrossfadeResampleWarning: '重采样会额外消耗 CPU 和内存——在较慢设备上请谨慎选择。',
showArtistImages: '显示艺术家图片',
showArtistImagesDesc: '在艺术家概览中加载并显示艺术家图片。默认关闭以减少大型音乐库的服务器磁盘I/O和网络负载。',
showOrbitTrigger: '在顶栏显示"Orbit"',
+2 -1
View File
@@ -27,6 +27,7 @@ import {
} from '../utils/playback/playbackServer';
import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl';
import { resolveReplayGainDb } from '../utils/audio/resolveReplayGainDb';
import { audioPlayHiResBlendArgs } from '../utils/audio/hiResCrossfadeResample';
import { showToast } from '../utils/ui/toast';
import { useAuthStore } from './authStore';
import { getPlayGeneration, setIsAudioPaused } from './engineState';
@@ -444,7 +445,7 @@ export function handleAudioProgress(
loudnessGainDb: loudnessGainDbForEngineBind(nextTrack.id),
preGainDb: authState.replayGainPreGainDb,
fallbackDb: authState.replayGainFallbackDb,
hiResEnabled: authState.enableHiRes,
...audioPlayHiResBlendArgs(authState),
analysisTrackId: nextTrack.id,
serverId: analysisServerId || null,
}).catch(() => {});
+2
View File
@@ -31,6 +31,7 @@ export function createAudioSettingsActions(set: SetState): Pick<
| 'setAutodjSmoothSkip'
| 'setGaplessEnabled'
| 'setEnableHiRes'
| 'setHiResCrossfadeResampleHz'
| 'setAudioOutputDevice'
> {
return {
@@ -73,6 +74,7 @@ export function createAudioSettingsActions(set: SetState): Pick<
setAutodjSmoothSkip: (v) => set({ autodjSmoothSkip: v }),
setGaplessEnabled: (v) => set({ gaplessEnabled: v }),
setEnableHiRes: (v) => set({ enableHiRes: v }),
setHiResCrossfadeResampleHz: (v) => set({ hiResCrossfadeResampleHz: v }),
setAudioOutputDevice: (v) => set({ audioOutputDevice: v }),
};
}
+1
View File
@@ -69,6 +69,7 @@ describe('trivial pass-through setters', () => {
['setQueueNowPlayingCollapsed', 'queueNowPlayingCollapsed', true],
['setQueueDurationDisplayMode', 'queueDurationDisplayMode', 'eta'],
['setEnableHiRes', 'enableHiRes', true],
['setHiResCrossfadeResampleHz', 'hiResCrossfadeResampleHz', 88_200],
['setFavoritesOfflineEnabled', 'favoritesOfflineEnabled', true],
['setHotCacheEnabled', 'hotCacheEnabled', true],
['setMixMinRatingFilterEnabled', 'mixMinRatingFilterEnabled', true],
+1
View File
@@ -100,6 +100,7 @@ export const useAuthStore = create<AuthState>()(
queueDurationDisplayMode: 'total',
queueDisplayMode: 'queue',
enableHiRes: false,
hiResCrossfadeResampleHz: 44_100,
audioOutputDevice: null,
favoritesOfflineEnabled: false,
hotCacheEnabled: false,
+4
View File
@@ -1,4 +1,5 @@
import { IS_LINUX } from '../utils/platform';
import { sanitizeHiResCrossfadeResampleHz } from '../utils/audio/hiResCrossfadeResample';
import {
LOUDNESS_PRE_ANALYSIS_REF_TARGET_LUFS,
clampStoredLoudnessPreAnalysisAttenuationRefDb,
@@ -244,6 +245,9 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
loudnessTargetLufs: targetSan,
loudnessPreAnalysisAttenuationDb: preSan,
loudnessPreIsRefV1: true,
hiResCrossfadeResampleHz: sanitizeHiResCrossfadeResampleHz(
(state as { hiResCrossfadeResampleHz?: unknown }).hiResCrossfadeResampleHz,
),
...lyricsSourcesMigrated,
...youLyPlusMigrated,
...wheelSmoothOneTime,
+4
View File
@@ -1,3 +1,4 @@
import type { HiResCrossfadeResampleHz } from '../utils/audio/hiResCrossfadeResample';
import type { EntityRatingSupportLevel } from '../api/subsonicTypes';
import type {
AudiomusePluginProbeResult,
@@ -242,6 +243,8 @@ export interface AuthState {
/** Alpha: native hi-res sample rate output (disabled = safe 44.1 kHz mode) */
enableHiRes: boolean;
/** Hi-Res: common output rate for crossfade / AutoDJ when adjacent tracks differ (Hz). */
hiResCrossfadeResampleHz: HiResCrossfadeResampleHz;
/** Selected audio output device name. null = system default. */
audioOutputDevice: string | null;
@@ -416,6 +419,7 @@ export interface AuthState {
setQueueDurationDisplayMode: (v: DurationMode) => void;
setQueueDisplayMode: (v: QueueDisplayMode) => void;
setEnableHiRes: (v: boolean) => void;
setHiResCrossfadeResampleHz: (v: HiResCrossfadeResampleHz) => void;
setAudioOutputDevice: (v: string | null) => void;
setFavoritesOfflineEnabled: (v: boolean) => void;
setHotCacheEnabled: (v: boolean) => void;
+2 -1
View File
@@ -7,6 +7,7 @@ import {
} from '../utils/playback/playbackServer';
import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl';
import { resolveReplayGainDb } from '../utils/audio/resolveReplayGainDb';
import { audioPlayHiResBlendArgs } from '../utils/audio/hiResCrossfadeResample';
import { useAuthStore } from './authStore';
import { getPlayGeneration, setIsAudioPaused } from './engineState';
import { touchHotCacheOnPlayback } from './hotCacheTouch';
@@ -56,7 +57,7 @@ export function engineLoadTrackAtPosition(opts: {
preGainDb: authState.replayGainPreGainDb,
fallbackDb: authState.replayGainFallbackDb,
manual: false,
hiResEnabled: authState.enableHiRes,
...audioPlayHiResBlendArgs(authState),
analysisTrackId: track.id,
serverId: playbackIndexKey || null,
streamFormatSuffix: track.suffix ?? null,
+2 -1
View File
@@ -33,6 +33,7 @@ import {
} from '../utils/offline/offlineLibraryHelpers';
import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl';
import { resolveReplayGainDb } from '../utils/audio/resolveReplayGainDb';
import { audioPlayHiResBlendArgs } from '../utils/audio/hiResCrossfadeResample';
import { useAuthStore } from './authStore';
import { consumeCrossfadeDynamicOverlap, getCrossfadeTransition, peekArmedCrossfadeDynamicOverlap } from './crossfadeTrimCache';
import {
@@ -468,7 +469,7 @@ export function runPlayTrack(
preGainDb: authStateNow.replayGainPreGainDb,
fallbackDb: authStateNow.replayGainFallbackDb,
manual,
hiResEnabled: authStateNow.enableHiRes,
...audioPlayHiResBlendArgs(authStateNow),
analysisTrackId: scopedTrack.id,
serverId: getPlaybackIndexKey() || null,
streamFormatSuffix: scopedTrack.suffix ?? null,
+3 -2
View File
@@ -8,6 +8,7 @@ import {
} from '../utils/playback/playbackServer';
import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl';
import { resolveReplayGainDb } from '../utils/audio/resolveReplayGainDb';
import { audioPlayHiResBlendArgs } from '../utils/audio/hiResCrossfadeResample';
import { songToTrack } from '../utils/playback/songToTrack';
import { useAuthStore } from './authStore';
import {
@@ -184,7 +185,7 @@ export function runResume(set: SetState, get: GetState): void {
preGainDb: authStateCold.replayGainPreGainDb,
fallbackDb: authStateCold.replayGainFallbackDb,
manual: false,
hiResEnabled: useAuthStore.getState().enableHiRes,
...audioPlayHiResBlendArgs(useAuthStore.getState()),
analysisTrackId: trackToPlay.id,
serverId: coldServerId || null,
streamFormatSuffix: trackToPlay.suffix ?? null,
@@ -225,7 +226,7 @@ export function runResume(set: SetState, get: GetState): void {
preGainDb: authStateCold.replayGainPreGainDb,
fallbackDb: authStateCold.replayGainFallbackDb,
manual: false,
hiResEnabled: useAuthStore.getState().enableHiRes,
...audioPlayHiResBlendArgs(useAuthStore.getState()),
analysisTrackId: currentTrack.id,
serverId: coldServerId || null,
streamFormatSuffix: currentTrack.suffix ?? null,
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import {
DEFAULT_HI_RES_CROSSFADE_RESAMPLE_HZ,
audioPlayHiResBlendArgs,
sanitizeHiResCrossfadeResampleHz,
} from './hiResCrossfadeResample';
describe('hiResCrossfadeResample', () => {
it('defaults unknown values to 44.1 kHz', () => {
expect(sanitizeHiResCrossfadeResampleHz(undefined)).toBe(44_100);
expect(sanitizeHiResCrossfadeResampleHz(48_000)).toBe(44_100);
});
it('keeps allowed blend rates', () => {
expect(sanitizeHiResCrossfadeResampleHz(88_200)).toBe(88_200);
expect(sanitizeHiResCrossfadeResampleHz(96_000)).toBe(96_000);
});
it('omits blend rate when hi-res is off', () => {
expect(
audioPlayHiResBlendArgs({
enableHiRes: false,
hiResCrossfadeResampleHz: DEFAULT_HI_RES_CROSSFADE_RESAMPLE_HZ,
}),
).toEqual({ hiResEnabled: false, hiResCrossfadeResampleHz: null });
});
it('forwards blend rate when hi-res is on', () => {
expect(
audioPlayHiResBlendArgs({ enableHiRes: true, hiResCrossfadeResampleHz: 96_000 }),
).toEqual({ hiResEnabled: true, hiResCrossfadeResampleHz: 96_000 });
});
});
+25
View File
@@ -0,0 +1,25 @@
/** Hi-Res transition blend output rates (Hz) for crossfade, AutoDJ, and gapless. */
export const HI_RES_CROSSFADE_RESAMPLE_OPTIONS = [44_100, 88_200, 96_000] as const;
export type HiResCrossfadeResampleHz = (typeof HI_RES_CROSSFADE_RESAMPLE_OPTIONS)[number];
export const DEFAULT_HI_RES_CROSSFADE_RESAMPLE_HZ: HiResCrossfadeResampleHz = 44_100;
export function sanitizeHiResCrossfadeResampleHz(value: unknown): HiResCrossfadeResampleHz {
if (value === 88_200 || value === 96_000) return value;
return DEFAULT_HI_RES_CROSSFADE_RESAMPLE_HZ;
}
/** Args forwarded to `audio_play` for hi-res + crossfade blend rate. */
export function audioPlayHiResBlendArgs(auth: {
enableHiRes: boolean;
hiResCrossfadeResampleHz: HiResCrossfadeResampleHz;
}): {
hiResEnabled: boolean;
hiResCrossfadeResampleHz: number | null;
} {
return {
hiResEnabled: auth.enableHiRes,
hiResCrossfadeResampleHz: auth.enableHiRes ? auth.hiResCrossfadeResampleHz : null,
};
}