Merge pull request #316 from Psychotoxical/fix/seekbar-truewave-pseudowave-split

fix(seekbar): split waveform style into truewave + pseudowave
This commit is contained in:
Frank Stellmacher
2026-04-26 02:47:37 +02:00
committed by GitHub
11 changed files with 42 additions and 14 deletions
+15 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -927,7 +927,8 @@ export const ruTranslation = {
sidebarLyricsStyleAppleDesc: 'Активная строка фиксируется сверху с плавной анимацией.', sidebarLyricsStyleAppleDesc: 'Активная строка фиксируется сверху с плавной анимацией.',
seekbarStyle: 'Стиль прогресс-бара', seekbarStyle: 'Стиль прогресс-бара',
seekbarStyleDesc: 'Выбор внешнего вида полосы воспроизведения', seekbarStyleDesc: 'Выбор внешнего вида полосы воспроизведения',
seekbarWaveform: 'Форма волны', seekbarTruewave: 'Реальная форма волны',
seekbarPseudowave: 'Псевдо форма волны',
seekbarLinedot: 'Линия и точка', seekbarLinedot: 'Линия и точка',
seekbarBar: 'Полоса', seekbarBar: 'Полоса',
seekbarThick: 'Толстая полоса', seekbarThick: 'Толстая полоса',
+2 -1
View File
@@ -870,7 +870,8 @@ export const zhTranslation = {
sidebarLyricsStyleAppleDesc: '活动行固定在顶部,带有平滑弹簧动画。', sidebarLyricsStyleAppleDesc: '活动行固定在顶部,带有平滑弹簧动画。',
seekbarStyle: '进度条样式', seekbarStyle: '进度条样式',
seekbarStyleDesc: '选择播放进度条的外观', seekbarStyleDesc: '选择播放进度条的外观',
seekbarWaveform: '波形', seekbarTruewave: '真实波形',
seekbarPseudowave: '伪波形',
seekbarLinedot: '线条与点', seekbarLinedot: '线条与点',
seekbarBar: '条形', seekbarBar: '条形',
seekbarThick: '粗条形', seekbarThick: '粗条形',
+1 -1
View File
@@ -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
View File
@@ -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,
}); });
}, },
} }