mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
fix(seekbar): split waveform style into truewave (analyzed) + pseudowave (deterministic)
The waveform-loudness-cache merge replaced the existing deterministic per-track-ID waveform with a bins-based one driven by the analysis cache. The bins-based variant is the better default but the old deterministic look is still valuable when no analysis is available (brand-new track, cache-miss, etc.) and several users prefer it. Split into two explicit options in the seekbar style picker: - 'truewave' (default, replaces old 'waveform') — bins from the analysis cache, with morph-on-arrival animation and flat-line fallback while empty. - 'pseudowave' — pseudo-random heights derived deterministically from the track ID. No analysis dependency, no morph, instant render. Existing persisted seekbarStyle: 'waveform' is migrated to 'truewave' in onRehydrateStorage so users keep the visual they have today. The useEffect that builds heightsRef now lists seekbarStyle in its deps so switching between the two is live. i18n labels added in all 8 locales. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -750,7 +750,8 @@ export function drawSeekbar(
|
|||||||
) {
|
) {
|
||||||
const anim = animState ?? makeAnimState();
|
const anim = animState ?? makeAnimState();
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case 'waveform': drawWaveform(canvas, heights, progress, buffered); break;
|
case 'truewave': drawWaveform(canvas, heights, progress, buffered); break;
|
||||||
|
case 'pseudowave': drawWaveform(canvas, heights, progress, buffered); break;
|
||||||
case 'linedot': drawLineDot(canvas, progress, buffered); break;
|
case 'linedot': drawLineDot(canvas, progress, buffered); break;
|
||||||
case 'bar': drawBar(canvas, progress, buffered); break;
|
case 'bar': drawBar(canvas, progress, buffered); break;
|
||||||
case 'thick': drawThick(canvas, progress, buffered); break;
|
case 'thick': drawThick(canvas, progress, buffered); break;
|
||||||
@@ -782,7 +783,7 @@ export function SeekbarPreview({
|
|||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
let heights: Float32Array | null = null;
|
let heights: Float32Array | null = null;
|
||||||
if (style === 'waveform') {
|
if (style === 'truewave' || style === 'pseudowave') {
|
||||||
heights = makeHeights('seekbar-preview-demo');
|
heights = makeHeights('seekbar-preview-demo');
|
||||||
}
|
}
|
||||||
const animState = makeAnimState();
|
const animState = makeAnimState();
|
||||||
@@ -896,6 +897,17 @@ export default function WaveformSeek({ trackId }: Props) {
|
|||||||
heightsRef.current = null;
|
heightsRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Pseudowave is the deterministic per-track-ID variant — no analysis needed,
|
||||||
|
// no morph animation, no flat-fallback. It just sits there looking like a
|
||||||
|
// waveform.
|
||||||
|
if (seekbarStyle === 'pseudowave') {
|
||||||
|
heightsRef.current = makeHeights(trackId);
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (canvas && !ANIMATED_STYLES.has(seekbarStyle)) {
|
||||||
|
drawSeekbar(canvas, seekbarStyle, heightsRef.current, progressRef.current, bufferedRef.current, animStateRef.current);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (waveformBins && waveformBins.length > 0) {
|
if (waveformBins && waveformBins.length > 0) {
|
||||||
const h = binsToHeights(waveformBins);
|
const h = binsToHeights(waveformBins);
|
||||||
const prev = heightsRef.current;
|
const prev = heightsRef.current;
|
||||||
@@ -959,7 +971,7 @@ export default function WaveformSeek({ trackId }: Props) {
|
|||||||
}
|
}
|
||||||
// No analysis bins yet: render 500 flat bars immediately.
|
// No analysis bins yet: render 500 flat bars immediately.
|
||||||
heightsRef.current = makeFlatWaveHeights();
|
heightsRef.current = makeFlatWaveHeights();
|
||||||
}, [trackId, waveformBins]);
|
}, [trackId, waveformBins, seekbarStyle]);
|
||||||
|
|
||||||
// Imperative subscription — no React re-renders from progress changes.
|
// Imperative subscription — no React re-renders from progress changes.
|
||||||
// Static styles draw here; animated styles only update refs.
|
// Static styles draw here; animated styles only update refs.
|
||||||
|
|||||||
+2
-1
@@ -888,7 +888,8 @@ export const deTranslation = {
|
|||||||
sidebarLyricsStyleAppleDesc: 'Aktive Zeile oben mit sanfter Animation.',
|
sidebarLyricsStyleAppleDesc: 'Aktive Zeile oben mit sanfter Animation.',
|
||||||
seekbarStyle: 'Seekbar-Stil',
|
seekbarStyle: 'Seekbar-Stil',
|
||||||
seekbarStyleDesc: 'Aussehen der Wiedergabe-Seekbar auswählen',
|
seekbarStyleDesc: 'Aussehen der Wiedergabe-Seekbar auswählen',
|
||||||
seekbarWaveform: 'Wellenform',
|
seekbarTruewave: 'Echte Wellenform',
|
||||||
|
seekbarPseudowave: 'Pseudo-Wellenform',
|
||||||
seekbarLinedot: 'Linie & Punkt',
|
seekbarLinedot: 'Linie & Punkt',
|
||||||
seekbarBar: 'Balken',
|
seekbarBar: 'Balken',
|
||||||
seekbarThick: 'Dicker Balken',
|
seekbarThick: 'Dicker Balken',
|
||||||
|
|||||||
+2
-1
@@ -894,7 +894,8 @@ export const enTranslation = {
|
|||||||
fsPortraitDim: 'Photo dimming',
|
fsPortraitDim: 'Photo dimming',
|
||||||
seekbarStyle: 'Seekbar Style',
|
seekbarStyle: 'Seekbar Style',
|
||||||
seekbarStyleDesc: 'Choose the look of the player seek bar',
|
seekbarStyleDesc: 'Choose the look of the player seek bar',
|
||||||
seekbarWaveform: 'Waveform',
|
seekbarTruewave: 'Truewave',
|
||||||
|
seekbarPseudowave: 'Pseudowave',
|
||||||
seekbarLinedot: 'Line & Dot',
|
seekbarLinedot: 'Line & Dot',
|
||||||
seekbarBar: 'Bar',
|
seekbarBar: 'Bar',
|
||||||
seekbarThick: 'Thick Bar',
|
seekbarThick: 'Thick Bar',
|
||||||
|
|||||||
+2
-1
@@ -881,7 +881,8 @@ export const esTranslation = {
|
|||||||
sidebarLyricsStyleAppleDesc: 'La línea activa se ancla arriba con animación suave.',
|
sidebarLyricsStyleAppleDesc: 'La línea activa se ancla arriba con animación suave.',
|
||||||
seekbarStyle: 'Estilo de Barra de Progreso',
|
seekbarStyle: 'Estilo de Barra de Progreso',
|
||||||
seekbarStyleDesc: 'Elige la apariencia de la barra de progreso del reproductor',
|
seekbarStyleDesc: 'Elige la apariencia de la barra de progreso del reproductor',
|
||||||
seekbarWaveform: 'Forma de Onda',
|
seekbarTruewave: 'Forma de Onda Real',
|
||||||
|
seekbarPseudowave: 'Forma de Onda Pseudo',
|
||||||
seekbarLinedot: 'Línea y Punto',
|
seekbarLinedot: 'Línea y Punto',
|
||||||
seekbarBar: 'Barra',
|
seekbarBar: 'Barra',
|
||||||
seekbarThick: 'Barra Gruesa',
|
seekbarThick: 'Barra Gruesa',
|
||||||
|
|||||||
+2
-1
@@ -876,7 +876,8 @@ export const frTranslation = {
|
|||||||
sidebarLyricsStyleAppleDesc: "La ligne active reste en haut avec une animation fluide.",
|
sidebarLyricsStyleAppleDesc: "La ligne active reste en haut avec une animation fluide.",
|
||||||
seekbarStyle: 'Style de la barre de lecture',
|
seekbarStyle: 'Style de la barre de lecture',
|
||||||
seekbarStyleDesc: 'Choisir l\'apparence de la barre de progression',
|
seekbarStyleDesc: 'Choisir l\'apparence de la barre de progression',
|
||||||
seekbarWaveform: 'Forme d\'onde',
|
seekbarTruewave: 'Forme d\'onde réelle',
|
||||||
|
seekbarPseudowave: 'Forme d\'onde pseudo',
|
||||||
seekbarLinedot: 'Ligne & point',
|
seekbarLinedot: 'Ligne & point',
|
||||||
seekbarBar: 'Barre',
|
seekbarBar: 'Barre',
|
||||||
seekbarThick: 'Barre épaisse',
|
seekbarThick: 'Barre épaisse',
|
||||||
|
|||||||
+2
-1
@@ -875,7 +875,8 @@ export const nbTranslation = {
|
|||||||
sidebarLyricsStyleAppleDesc: 'Aktiv linje forankres øverst med jevn animasjon.',
|
sidebarLyricsStyleAppleDesc: 'Aktiv linje forankres øverst med jevn animasjon.',
|
||||||
seekbarStyle: 'Søkefelt-stil',
|
seekbarStyle: 'Søkefelt-stil',
|
||||||
seekbarStyleDesc: 'Velg utseendet på avspillingssøkefeltet',
|
seekbarStyleDesc: 'Velg utseendet på avspillingssøkefeltet',
|
||||||
seekbarWaveform: 'Bølgeform',
|
seekbarTruewave: 'Ekte bølgeform',
|
||||||
|
seekbarPseudowave: 'Pseudo-bølgeform',
|
||||||
seekbarLinedot: 'Linje & punkt',
|
seekbarLinedot: 'Linje & punkt',
|
||||||
seekbarBar: 'Linje',
|
seekbarBar: 'Linje',
|
||||||
seekbarThick: 'Tykk linje',
|
seekbarThick: 'Tykk linje',
|
||||||
|
|||||||
+2
-1
@@ -875,7 +875,8 @@ export const nlTranslation = {
|
|||||||
sidebarLyricsStyleAppleDesc: 'Actieve regel verankerd bovenaan met vloeiende animatie.',
|
sidebarLyricsStyleAppleDesc: 'Actieve regel verankerd bovenaan met vloeiende animatie.',
|
||||||
seekbarStyle: 'Zoekbalkstijl',
|
seekbarStyle: 'Zoekbalkstijl',
|
||||||
seekbarStyleDesc: 'Kies het uiterlijk van de afspeelbalk',
|
seekbarStyleDesc: 'Kies het uiterlijk van de afspeelbalk',
|
||||||
seekbarWaveform: 'Golfvorm',
|
seekbarTruewave: 'Echte golfvorm',
|
||||||
|
seekbarPseudowave: 'Pseudo-golfvorm',
|
||||||
seekbarLinedot: 'Lijn & punt',
|
seekbarLinedot: 'Lijn & punt',
|
||||||
seekbarBar: 'Balk',
|
seekbarBar: 'Balk',
|
||||||
seekbarThick: 'Dikke balk',
|
seekbarThick: 'Dikke balk',
|
||||||
|
|||||||
+2
-1
@@ -927,7 +927,8 @@ export const ruTranslation = {
|
|||||||
sidebarLyricsStyleAppleDesc: 'Активная строка фиксируется сверху с плавной анимацией.',
|
sidebarLyricsStyleAppleDesc: 'Активная строка фиксируется сверху с плавной анимацией.',
|
||||||
seekbarStyle: 'Стиль прогресс-бара',
|
seekbarStyle: 'Стиль прогресс-бара',
|
||||||
seekbarStyleDesc: 'Выбор внешнего вида полосы воспроизведения',
|
seekbarStyleDesc: 'Выбор внешнего вида полосы воспроизведения',
|
||||||
seekbarWaveform: 'Форма волны',
|
seekbarTruewave: 'Реальная форма волны',
|
||||||
|
seekbarPseudowave: 'Псевдо форма волны',
|
||||||
seekbarLinedot: 'Линия и точка',
|
seekbarLinedot: 'Линия и точка',
|
||||||
seekbarBar: 'Полоса',
|
seekbarBar: 'Полоса',
|
||||||
seekbarThick: 'Толстая полоса',
|
seekbarThick: 'Толстая полоса',
|
||||||
|
|||||||
+2
-1
@@ -870,7 +870,8 @@ export const zhTranslation = {
|
|||||||
sidebarLyricsStyleAppleDesc: '活动行固定在顶部,带有平滑弹簧动画。',
|
sidebarLyricsStyleAppleDesc: '活动行固定在顶部,带有平滑弹簧动画。',
|
||||||
seekbarStyle: '进度条样式',
|
seekbarStyle: '进度条样式',
|
||||||
seekbarStyleDesc: '选择播放进度条的外观',
|
seekbarStyleDesc: '选择播放进度条的外观',
|
||||||
seekbarWaveform: '波形',
|
seekbarTruewave: '真实波形',
|
||||||
|
seekbarPseudowave: '伪波形',
|
||||||
seekbarLinedot: '线条与点',
|
seekbarLinedot: '线条与点',
|
||||||
seekbarBar: '条形',
|
seekbarBar: '条形',
|
||||||
seekbarThick: '粗条形',
|
seekbarThick: '粗条形',
|
||||||
|
|||||||
@@ -3676,7 +3676,7 @@ export default function Settings() {
|
|||||||
{t('settings.seekbarStyleDesc')}
|
{t('settings.seekbarStyleDesc')}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
|
||||||
{(['waveform', 'linedot', 'bar', 'thick', 'segmented', 'neon', 'pulsewave', 'particletrail', 'liquidfill', 'retrotape'] as SeekbarStyle[]).map(style => (
|
{(['truewave', 'pseudowave', 'linedot', 'bar', 'thick', 'segmented', 'neon', 'pulsewave', 'particletrail', 'liquidfill', 'retrotape'] as SeekbarStyle[]).map(style => (
|
||||||
<SeekbarPreview
|
<SeekbarPreview
|
||||||
key={style}
|
key={style}
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
+10
-2
@@ -18,7 +18,7 @@ export interface ServerProfile {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SeekbarStyle = 'waveform' | 'linedot' | 'bar' | 'thick' | 'segmented' | 'neon' | 'pulsewave' | 'particletrail' | 'liquidfill' | 'retrotape';
|
export type SeekbarStyle = 'truewave' | 'pseudowave' | 'linedot' | 'bar' | 'thick' | 'segmented' | 'neon' | 'pulsewave' | 'particletrail' | 'liquidfill' | 'retrotape';
|
||||||
export type LoggingMode = 'off' | 'normal' | 'debug';
|
export type LoggingMode = 'off' | 'normal' | 'debug';
|
||||||
export type NormalizationEngine = 'off' | 'replaygain' | 'loudness';
|
export type NormalizationEngine = 'off' | 'replaygain' | 'loudness';
|
||||||
|
|
||||||
@@ -388,7 +388,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
fsPortraitDim: 28,
|
fsPortraitDim: 28,
|
||||||
showChangelogOnUpdate: true,
|
showChangelogOnUpdate: true,
|
||||||
lastSeenChangelogVersion: '',
|
lastSeenChangelogVersion: '',
|
||||||
seekbarStyle: 'waveform',
|
seekbarStyle: 'truewave',
|
||||||
enableHiRes: false,
|
enableHiRes: false,
|
||||||
audioOutputDevice: null,
|
audioOutputDevice: null,
|
||||||
hotCacheEnabled: false,
|
hotCacheEnabled: false,
|
||||||
@@ -729,6 +729,13 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One-time: 'waveform' style was renamed to 'truewave' (with 'pseudowave'
|
||||||
|
// added as the deterministic legacy variant). Existing persisted value
|
||||||
|
// 'waveform' should land on the new bins-based default.
|
||||||
|
const seekbarStyleMigrated = (state.seekbarStyle as string) === 'waveform'
|
||||||
|
? { seekbarStyle: 'truewave' as SeekbarStyle }
|
||||||
|
: {};
|
||||||
|
|
||||||
const st = state as { loudnessTargetLufs?: unknown; loudnessPreAnalysisAttenuationDb?: unknown };
|
const st = state as { loudnessTargetLufs?: unknown; loudnessPreAnalysisAttenuationDb?: unknown };
|
||||||
const targetSan = sanitizeLoudnessLufsPreset(st.loudnessTargetLufs, -12);
|
const targetSan = sanitizeLoudnessLufsPreset(st.loudnessTargetLufs, -12);
|
||||||
const preSan = clampLoudnessPreAnalysisAttenuationDb(st.loudnessPreAnalysisAttenuationDb);
|
const preSan = clampLoudnessPreAnalysisAttenuationDb(st.loudnessPreAnalysisAttenuationDb);
|
||||||
@@ -745,6 +752,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
...conflictingLegacyState,
|
...conflictingLegacyState,
|
||||||
...lyricsSourcesMigrated,
|
...lyricsSourcesMigrated,
|
||||||
...wheelSmoothOneTime,
|
...wheelSmoothOneTime,
|
||||||
|
...seekbarStyleMigrated,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user