mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
fix(audio): stabilize loudness normalization and streaming startup behavior
Improve normalization consistency by separating cache refresh for non-playing tracks, hardening track-id cache lookups, and keeping UI gain state aligned with actual playback. Also reduce startup stalls by preferring non-seekable streaming when format hints are missing and by tuning ALSA/analysis paths to avoid underrun-prone churn.
This commit is contained in:
@@ -258,6 +258,9 @@ export default function QueuePanel() {
|
||||
const contextMenu = usePlayerStore(s => s.contextMenu);
|
||||
|
||||
const playbackSource = usePlayerStore(s => s.currentPlaybackSource);
|
||||
const normalizationNowDb = usePlayerStore(s => s.normalizationNowDb);
|
||||
const normalizationTargetLufs = usePlayerStore(s => s.normalizationTargetLufs);
|
||||
const normalizationEngineLive = usePlayerStore(s => s.normalizationEngineLive);
|
||||
|
||||
const crossfadeEnabled = useAuthStore(s => s.crossfadeEnabled);
|
||||
const crossfadeSecs = useAuthStore(s => s.crossfadeSecs);
|
||||
@@ -267,6 +270,8 @@ export default function QueuePanel() {
|
||||
const setCrossfadeSecs = useAuthStore(s => s.setCrossfadeSecs);
|
||||
const setGaplessEnabled = useAuthStore(s => s.setGaplessEnabled);
|
||||
const setInfiniteQueueEnabled = useAuthStore(s => s.setInfiniteQueueEnabled);
|
||||
const normalizationEngine = useAuthStore(s => s.normalizationEngine);
|
||||
const replayGainMode = useAuthStore(s => s.replayGainMode);
|
||||
|
||||
const activeTab = useLyricsStore(s => s.activeTab);
|
||||
const setTab = useLyricsStore(s => s.setTab);
|
||||
@@ -478,6 +483,14 @@ export default function QueuePanel() {
|
||||
const rgParts = formatQueueReplayGainParts(currentTrack, t);
|
||||
const baseLine = baseParts.join(' · ');
|
||||
const rgLine = rgParts.join(' · ');
|
||||
const liveGainLabel = normalizationNowDb != null
|
||||
? `${normalizationNowDb >= 0 ? '+' : ''}${normalizationNowDb.toFixed(2)} dB`
|
||||
: '—';
|
||||
const targetLabel = normalizationEngineLive === 'loudness'
|
||||
? (normalizationTargetLufs != null ? `${normalizationTargetLufs} LUFS` : '-16 LUFS')
|
||||
: normalizationEngineLive === 'replaygain'
|
||||
? `ReplayGain (${replayGainMode})`
|
||||
: t('queue.normOff', { defaultValue: 'Off' });
|
||||
if (!baseLine && !rgLine && !playbackSource) return null;
|
||||
const showRgLine = expandReplayGain && !!rgLine;
|
||||
return (
|
||||
@@ -522,6 +535,15 @@ export default function QueuePanel() {
|
||||
{' · '}{rgLine}
|
||||
</span>
|
||||
)}
|
||||
{(normalizationEngine === 'loudness' || normalizationEngineLive === 'loudness') && (
|
||||
<span className="queue-current-tech-rg">
|
||||
<span className="queue-current-tech-rg-label">{t('queue.normNow', { defaultValue: 'Now' })}</span>
|
||||
{' · '}{liveGainLabel}
|
||||
{' · '}
|
||||
<span className="queue-current-tech-rg-label">{t('queue.normTarget', { defaultValue: 'Target' })}</span>
|
||||
{' · '}{targetLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -824,6 +824,7 @@ export default function WaveformSeek({ trackId }: Props) {
|
||||
const SEEK_COMMIT_PROGRESS_EPS = 0.02;
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const heightsRef = useRef<Float32Array | null>(null);
|
||||
const lastHeightsTrackIdRef = useRef<string | undefined>(undefined);
|
||||
const progressRef = useRef(usePlayerStore.getState().progress);
|
||||
const bufferedRef = useRef(usePlayerStore.getState().buffered);
|
||||
const isDragging = useRef(false);
|
||||
@@ -832,6 +833,10 @@ export default function WaveformSeek({ trackId }: Props) {
|
||||
const [hoverPct, setHoverPct] = useState<number | null>(null);
|
||||
|
||||
const seek = usePlayerStore(s => s.seek);
|
||||
const waveformBins = usePlayerStore(s => s.waveformBins);
|
||||
const waveformIsPartial = usePlayerStore(s => s.waveformIsPartial);
|
||||
const waveformKnownUntilSec = usePlayerStore(s => s.waveformKnownUntilSec);
|
||||
const waveformDurationSec = usePlayerStore(s => s.waveformDurationSec);
|
||||
const duration = usePlayerStore(s => s.currentTrack?.duration ?? 0);
|
||||
const seekbarStyle = useAuthStore(s => s.seekbarStyle);
|
||||
|
||||
@@ -843,10 +848,44 @@ export default function WaveformSeek({ trackId }: Props) {
|
||||
useEffect(() => {
|
||||
if (!trackId) {
|
||||
heightsRef.current = null;
|
||||
lastHeightsTrackIdRef.current = undefined;
|
||||
return;
|
||||
}
|
||||
if (waveformBins && waveformBins.length > 0) {
|
||||
const src = waveformBins;
|
||||
const h = new Float32Array(BAR_COUNT);
|
||||
const effectiveDur = waveformDurationSec > 0 ? waveformDurationSec : duration;
|
||||
const knownFrac = waveformIsPartial && effectiveDur > 0
|
||||
? Math.max(0, Math.min(1, waveformKnownUntilSec / effectiveDur))
|
||||
: 1;
|
||||
const knownBars = Math.max(0, Math.min(BAR_COUNT, Math.floor(knownFrac * BAR_COUNT)));
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
if (i >= knownBars) {
|
||||
h[i] = 0.08; // unknown tail baseline while partial waveform is still loading
|
||||
continue;
|
||||
}
|
||||
const idx = Math.min(src.length - 1, Math.floor((i / BAR_COUNT) * src.length));
|
||||
const v = src[idx];
|
||||
h[i] = Math.max(0.08, Math.min(1, (Number(v) / 255)));
|
||||
}
|
||||
// Partial -> full handoff: blend with previous heights for a smoother
|
||||
// visual transition and avoid a hard "jump" in the waveform shape.
|
||||
if (lastHeightsTrackIdRef.current === trackId && heightsRef.current && heightsRef.current.length === BAR_COUNT) {
|
||||
const prev = heightsRef.current;
|
||||
const mixed = new Float32Array(BAR_COUNT);
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
mixed[i] = prev[i] * 0.45 + h[i] * 0.55;
|
||||
}
|
||||
heightsRef.current = mixed;
|
||||
} else {
|
||||
heightsRef.current = h;
|
||||
}
|
||||
lastHeightsTrackIdRef.current = trackId;
|
||||
return;
|
||||
}
|
||||
heightsRef.current = makeHeights(trackId);
|
||||
}, [trackId]);
|
||||
lastHeightsTrackIdRef.current = trackId;
|
||||
}, [trackId, waveformBins, waveformIsPartial, waveformKnownUntilSec, waveformDurationSec, duration]);
|
||||
|
||||
// Imperative subscription — no React re-renders from progress changes.
|
||||
// Static styles draw here; animated styles only update refs.
|
||||
|
||||
Reference in New Issue
Block a user