forked from Keelrleek/SoulStream
player bar: the name sets the width, and the waveform stops flattering the clamp
The source chips are gone — the rail already tells that story — and with them the measured slot, its effects, and the bar's only container query. The now-playing cluster hugs the song name with no cap, giving ground only once the scrubber hits its 260px floor; the artist's 55% ceiling applies only when an album sits beside it, so a lone loading artist no longer renders as two letters in a twelve-pixel box. The waveform is rebuilt on honesty. WAVE_BUCKETS rises 400 → 2000 (~128ms per bucket — at 400, every display bar was ~2s of audio averaged twice), and one shared shaping in waveshape.js replaces both lanes' gain-into-clamp rule, which flat-topped 53.8% of the bars on a real decode and was propped up by per-bar noise in the rail — deleted; the lane shows the record, not a hash of the bar index. Bars fold with a mean+peak blend and stretch across the track's own p05–p97, so a loud master stops pinning to the ceiling and a quiet one stops vanishing. The playbar copies the rail's fade verbatim: whole bars blend accent→grey with cuts landing in the 4px gap, and the one bar under the playhead crossfades toward whatever its neighbours are — the wash while hovering, the muted grey otherwise. The redraw gate watches that blend instead of a 0.75px playhead move, so both lanes now step on every position event. Loading is drawn, not implied: the solid fallback bar is gone and the lane is always the bars, always seekable. Empty, it rests as the rail's dots; a swell crosses them while a waveform is computed (2s sweep, 0.5s rest); a streaming track's bars stand up left→right on a 40ms cadence, each rising over 2s. The rise frontier is the waveform's OWN coverage, reported by stream_waveform beside the peaks — the old contract zero-padded partial peaks to full length and let the UI fold them against a fresher byte count, which stretched the whole shape between refreshes. Where the bars end is how much of the track has actually loaded; cached and local tracks arrive at coverage 1 and pop in complete. The first partial is also requested eagerly (fresh refresh clock per track, 700ms retries until peaks exist), closing most of the audio-before-lane gap.
This commit is contained in:
1 parent
3aaec30dc6
commit
b33d403634
9 files changed
+740
-323
No files matched your search
+62
-8
@@ -3391,6 +3391,7 @@ export async function refreshSourcePrefs() {
|
||||
// settings unavailable (fresh install / backend restart) → defaults
|
||||
}
|
||||
}
|
||||
|
||||
void refreshSourcePrefs();
|
||||
|
||||
/** Whether a peer file satisfies the user's quality window for this mode.
|
||||
@@ -4597,6 +4598,16 @@ export const player = $state({
|
||||
repeat: "off", // off | all | one
|
||||
// waveform peaks (0..1) for the current local track, [] while loading
|
||||
waveform: [],
|
||||
// How much of the TRACK `waveform` actually describes, 0..1 — reported by
|
||||
// the backend with the peaks themselves, so the two can never disagree.
|
||||
// 1 for local files and complete streams; a mid-transfer decode covers
|
||||
// exactly this prefix and the lanes fold it across exactly this fraction.
|
||||
// Where the standing bars end IS how much of the track has loaded.
|
||||
waveformCoverage: 1,
|
||||
// True while a waveform request for the CURRENT track is still working —
|
||||
// what separates "computing, show the loading swell" from "there is no
|
||||
// waveform, rest as dots". Cleared on success and on final failure.
|
||||
wavePending: false,
|
||||
// Which catalog entity is playing (drives now-playing badges).
|
||||
catalogRef: { albumId: null, trackId: null },
|
||||
// True for a track restored from the last session whose AUDIO did not
|
||||
@@ -4999,7 +5010,12 @@ export function initEvents() {
|
||||
} else if (
|
||||
ui.windowActive
|
||||
&& cur?.stream
|
||||
&& performance.now() - waveRefreshAt > 3000
|
||||
// Until the FIRST peaks land, retry fast — the earliest decodes
|
||||
// race the transfer's opening seconds and often see too little
|
||||
// file to succeed, and audio audibly playing over a still-empty
|
||||
// lane was the gap being closed here. Once bars exist, the
|
||||
// geometric decode thresholds make 3s the right cadence.
|
||||
&& performance.now() - waveRefreshAt > (player.waveform.length ? 3000 : 700)
|
||||
) {
|
||||
// Still downloading: refresh the partial waveform so it grows
|
||||
// with the transfer.
|
||||
@@ -5207,6 +5223,10 @@ function playQueueIndex(i) {
|
||||
// Remote track streamed from a peer while it downloads to cache.
|
||||
player.streamReceived = 0;
|
||||
player.streamTotal = item.stream.size;
|
||||
// A fresh track must not inherit the previous one's refresh clock —
|
||||
// that throttled the FIRST partial waveform by up to three seconds of
|
||||
// audible-but-laneless playback.
|
||||
waveRefreshAt = 0;
|
||||
resetStreamRate();
|
||||
if ("trackId" in item) player.catalogRef.trackId = item.trackId;
|
||||
appendedNextPath = null;
|
||||
@@ -5453,21 +5473,35 @@ async function maybeExtendAlbumQueue() {
|
||||
|
||||
// ---- waveform ----
|
||||
|
||||
const WAVE_BUCKETS = 400;
|
||||
// 2000, up from 400: at 400 each bucket already averaged ~0.64s of audio, so
|
||||
// by the time the lanes folded buckets into display bars every bar was ~2s
|
||||
// averaged twice, and the transients the lane exists to show were gone before
|
||||
// shaping ever ran (measured in waveshape.js's header). 2000 is ~128ms per
|
||||
// bucket on a typical track — finer than any lane's bar pitch — and the
|
||||
// command's ceiling is 4096, so this is only a constant.
|
||||
const WAVE_BUCKETS = 2000;
|
||||
let waveReqPath = null;
|
||||
// Throttle for progressive stream-waveform recomputes (see soulseek:stream).
|
||||
let waveRefreshAt = 0;
|
||||
|
||||
async function loadWaveform(path) {
|
||||
player.waveform = [];
|
||||
player.wavePending = !!path;
|
||||
waveReqPath = path;
|
||||
if (!path) return;
|
||||
try {
|
||||
const peaks = await api.waveform(path, WAVE_BUCKETS);
|
||||
// Ignore if the track changed while we were computing.
|
||||
if (waveReqPath === path) player.waveform = peaks;
|
||||
if (waveReqPath === path) {
|
||||
player.waveform = peaks;
|
||||
player.waveformCoverage = 1;
|
||||
}
|
||||
} catch {
|
||||
if (waveReqPath === path) player.waveform = [];
|
||||
} finally {
|
||||
// Either way the request is over; a failed local decode rests as dots
|
||||
// rather than swelling forever at a waveform that will never come.
|
||||
if (waveReqPath === path) player.wavePending = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5480,7 +5514,13 @@ let queuedStreamWave = null;
|
||||
|
||||
async function loadStreamWaveform(item, final = false) {
|
||||
const key = `stream:${item.stream.username}|${item.path}`;
|
||||
if (waveReqPath !== key) player.waveform = [];
|
||||
if (waveReqPath !== key) {
|
||||
player.waveform = [];
|
||||
// Pending until the FIRST partial decode lands; after that the lane
|
||||
// has bars and the streaming rise owns the story. A mid-stream refresh
|
||||
// that fails just keeps the bars it already has.
|
||||
player.wavePending = true;
|
||||
}
|
||||
waveReqPath = key;
|
||||
// A late partial FLAC decode can take close to the three-second refresh
|
||||
// interval. Coalesce all overlap into the newest request, retaining a
|
||||
@@ -5498,7 +5538,12 @@ async function loadStreamWaveform(item, final = false) {
|
||||
next = null;
|
||||
const seq = ++waveSeq;
|
||||
try {
|
||||
const peaks = await api.streamWaveform(
|
||||
// { coverage, peaks }: the backend reports how much of the
|
||||
// track the peaks describe, measured from the SAME file read
|
||||
// that produced them — the lanes' rise frontier is anchored to
|
||||
// this, never to the live byte counter, so the drawn shape
|
||||
// cannot stretch between refreshes.
|
||||
const slice = await api.streamWaveform(
|
||||
request.item.stream.username,
|
||||
request.item.path,
|
||||
request.item.stream.size,
|
||||
@@ -5507,12 +5552,19 @@ async function loadStreamWaveform(item, final = false) {
|
||||
if (
|
||||
waveReqPath === request.key
|
||||
&& seq === waveSeq
|
||||
&& peaks.length > 0
|
||||
&& slice.peaks.length > 0
|
||||
) {
|
||||
player.waveform = peaks;
|
||||
player.waveform = slice.peaks;
|
||||
player.waveformCoverage = Math.max(0, Math.min(1, slice.coverage));
|
||||
player.wavePending = false;
|
||||
}
|
||||
} catch {
|
||||
// stays empty — the buffer/progress bar renders instead
|
||||
// Peaks stay as they were. A failed FINAL decode is the end of
|
||||
// the line though — stop the loading swell so the lane rests
|
||||
// as dots instead of promising a waveform that will not come.
|
||||
if (request.final && waveReqPath === request.key) {
|
||||
player.wavePending = false;
|
||||
}
|
||||
}
|
||||
const queued = queuedStreamWave;
|
||||
queuedStreamWave = null;
|
||||
@@ -5598,6 +5650,8 @@ function stopPlayback() {
|
||||
player.queue = [];
|
||||
player.queueIndex = -1;
|
||||
player.waveform = [];
|
||||
player.waveformCoverage = 1;
|
||||
player.wavePending = false;
|
||||
player.catalogRef = { albumId: null, trackId: null };
|
||||
latestPlayerPosition = 0;
|
||||
latestPlayerDuration = null;
|
||||
|
||||
Reference in new issue
Block a user