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();
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 'bar': drawBar(canvas, progress, buffered); break;
case 'thick': drawThick(canvas, progress, buffered); break;
@@ -782,7 +783,7 @@ export function SeekbarPreview({
const canvas = canvasRef.current;
if (!canvas) return;
let heights: Float32Array | null = null;
if (style === 'waveform') {
if (style === 'truewave' || style === 'pseudowave') {
heights = makeHeights('seekbar-preview-demo');
}
const animState = makeAnimState();
@@ -896,6 +897,17 @@ export default function WaveformSeek({ trackId }: Props) {
heightsRef.current = null;
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) {
const h = binsToHeights(waveformBins);
const prev = heightsRef.current;
@@ -959,7 +971,7 @@ export default function WaveformSeek({ trackId }: Props) {
}
// No analysis bins yet: render 500 flat bars immediately.
heightsRef.current = makeFlatWaveHeights();
}, [trackId, waveformBins]);
}, [trackId, waveformBins, seekbarStyle]);
// Imperative subscription — no React re-renders from progress changes.
// 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.',
seekbarStyle: 'Seekbar-Stil',
seekbarStyleDesc: 'Aussehen der Wiedergabe-Seekbar auswählen',
seekbarWaveform: 'Wellenform',
seekbarTruewave: 'Echte Wellenform',
seekbarPseudowave: 'Pseudo-Wellenform',
seekbarLinedot: 'Linie & Punkt',
seekbarBar: 'Balken',
seekbarThick: 'Dicker Balken',
+2 -1
View File
@@ -894,7 +894,8 @@ export const enTranslation = {
fsPortraitDim: 'Photo dimming',
seekbarStyle: 'Seekbar Style',
seekbarStyleDesc: 'Choose the look of the player seek bar',
seekbarWaveform: 'Waveform',
seekbarTruewave: 'Truewave',
seekbarPseudowave: 'Pseudowave',
seekbarLinedot: 'Line & Dot',
seekbarBar: '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.',
seekbarStyle: 'Estilo de Barra de Progreso',
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',
seekbarBar: 'Barra',
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.",
seekbarStyle: 'Style de la barre de lecture',
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',
seekbarBar: 'Barre',
seekbarThick: 'Barre épaisse',
+2 -1
View File
@@ -875,7 +875,8 @@ export const nbTranslation = {
sidebarLyricsStyleAppleDesc: 'Aktiv linje forankres øverst med jevn animasjon.',
seekbarStyle: 'Søkefelt-stil',
seekbarStyleDesc: 'Velg utseendet på avspillingssøkefeltet',
seekbarWaveform: 'Bølgeform',
seekbarTruewave: 'Ekte bølgeform',
seekbarPseudowave: 'Pseudo-bølgeform',
seekbarLinedot: 'Linje & punkt',
seekbarBar: 'Linje',
seekbarThick: 'Tykk linje',
+2 -1
View File
@@ -875,7 +875,8 @@ export const nlTranslation = {
sidebarLyricsStyleAppleDesc: 'Actieve regel verankerd bovenaan met vloeiende animatie.',
seekbarStyle: 'Zoekbalkstijl',
seekbarStyleDesc: 'Kies het uiterlijk van de afspeelbalk',
seekbarWaveform: 'Golfvorm',
seekbarTruewave: 'Echte golfvorm',
seekbarPseudowave: 'Pseudo-golfvorm',
seekbarLinedot: 'Lijn & punt',
seekbarBar: 'Balk',
seekbarThick: 'Dikke balk',
+2 -1
View File
@@ -927,7 +927,8 @@ export const ruTranslation = {
sidebarLyricsStyleAppleDesc: 'Активная строка фиксируется сверху с плавной анимацией.',
seekbarStyle: 'Стиль прогресс-бара',
seekbarStyleDesc: 'Выбор внешнего вида полосы воспроизведения',
seekbarWaveform: 'Форма волны',
seekbarTruewave: 'Реальная форма волны',
seekbarPseudowave: 'Псевдо форма волны',
seekbarLinedot: 'Линия и точка',
seekbarBar: 'Полоса',
seekbarThick: 'Толстая полоса',
+2 -1
View File
@@ -870,7 +870,8 @@ export const zhTranslation = {
sidebarLyricsStyleAppleDesc: '活动行固定在顶部,带有平滑弹簧动画。',
seekbarStyle: '进度条样式',
seekbarStyleDesc: '选择播放进度条的外观',
seekbarWaveform: '波形',
seekbarTruewave: '真实波形',
seekbarPseudowave: '伪波形',
seekbarLinedot: '线条与点',
seekbarBar: '条形',
seekbarThick: '粗条形',
+1 -1
View File
@@ -3676,7 +3676,7 @@ export default function Settings() {
{t('settings.seekbarStyleDesc')}
</div>
<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
key={style}
style={style}
+10 -2
View File
@@ -18,7 +18,7 @@ export interface ServerProfile {
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 NormalizationEngine = 'off' | 'replaygain' | 'loudness';
@@ -388,7 +388,7 @@ export const useAuthStore = create<AuthState>()(
fsPortraitDim: 28,
showChangelogOnUpdate: true,
lastSeenChangelogVersion: '',
seekbarStyle: 'waveform',
seekbarStyle: 'truewave',
enableHiRes: false,
audioOutputDevice: null,
hotCacheEnabled: false,
@@ -729,6 +729,13 @@ export const useAuthStore = create<AuthState>()(
} 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 targetSan = sanitizeLoudnessLufsPreset(st.loudnessTargetLufs, -12);
const preSan = clampLoudnessPreAnalysisAttenuationDb(st.loudnessPreAnalysisAttenuationDb);
@@ -745,6 +752,7 @@ export const useAuthStore = create<AuthState>()(
...conflictingLegacyState,
...lyricsSourcesMigrated,
...wheelSmoothOneTime,
...seekbarStyleMigrated,
});
},
}