player bar: the track-change morph stops being a coin flip
Two independent fragilities each collapsed the shape→shape morph back to a grow-from-dots, which is why it only sometimes worked. The hold was 600ms against an arrival whose timing is DECODE time: the same track costs ~5ms once the session holds its waveform in memory but hundreds of ms to over a second on the first cold decode. 2000ms covers the cold case with room; fresh-stream searches take well past that, so the loading story still gets the lane. And the hold bailed on a bar-count mismatch — but the lane's width follows the song TITLE now, so switching between a long and a short name resizes the lane mid-gap and the guard threw the held shape away. Held and morphed heights are refitted to the new count instead (nearest neighbour — it is a moving start point, on screen for under a second). Four consecutive cached switches measured after the fix: minimum lane height 15.1 on every one (dots are ~2, the old dip hit ~5). Gapless playback is untouched — the hold and morph are pure painting, and the sink-append path never waits on the lane.
This commit is contained in:
1 parent
0bbdb1bb0e
commit
e2cfed081c
2 files changed
+39
-8
No files matched your search
@@ -246,6 +246,23 @@
|
|||||||
return pending;
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resample a heights array onto a different bar count. The lane's WIDTH
|
||||||
|
* follows the song title now (the cluster hugs the name), so a track
|
||||||
|
* change routinely changes the bar count mid-hold — and bailing on the
|
||||||
|
* mismatch threw the held shape away and turned the morph into a coin
|
||||||
|
* flip. Nearest-neighbour is plenty: the result is a moving start point,
|
||||||
|
* on screen for under a second. */
|
||||||
|
function fitHeights(src, count) {
|
||||||
|
if (!src?.length) return null;
|
||||||
|
if (src.length === count) return src;
|
||||||
|
const out = new Array(count);
|
||||||
|
const top = src.length - 1;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
out[i] = src[Math.min(top, Math.round((i / Math.max(1, count - 1)) * top))] ?? 2;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/** The fade, one bar at a time — shared by the rise and the arrival
|
/** The fade, one bar at a time — shared by the rise and the arrival
|
||||||
* morph, whose heights differ every frame and cannot use the grouped
|
* morph, whose heights differ every frame and cannot use the grouped
|
||||||
* clip-fills of the settled painter. */
|
* clip-fills of the settled painter. */
|
||||||
@@ -492,11 +509,17 @@
|
|||||||
holdUntil = now + ARRIVE_HOLD_MS;
|
holdUntil = now + ARRIVE_HOLD_MS;
|
||||||
hadPeaks = false;
|
hadPeaks = false;
|
||||||
}
|
}
|
||||||
if (player.wavePending && lastDrawnHeights?.length === count && now < holdUntil) {
|
const held = player.wavePending && now < holdUntil
|
||||||
|
? fitHeights(lastDrawnHeights, count)
|
||||||
|
: null;
|
||||||
|
if (held) {
|
||||||
ctx.clearRect(0, 0, W, H);
|
ctx.clearRect(0, 0, W, H);
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
paintBar(ctx, i * BAR_PITCH, mid, lastDrawnHeights[i], UNPLAYED);
|
paintBar(ctx, i * BAR_PITCH, mid, held[i], UNPLAYED);
|
||||||
}
|
}
|
||||||
|
// Write the fitted shape back so a mid-hold resize composes
|
||||||
|
// and the arrival morph starts from exactly what is showing.
|
||||||
|
lastDrawnHeights = held;
|
||||||
lastPlayedBars = Number.NEGATIVE_INFINITY;
|
lastPlayedBars = Number.NEGATIVE_INFINITY;
|
||||||
scheduleAnim(true);
|
scheduleAnim(true);
|
||||||
return;
|
return;
|
||||||
@@ -600,10 +623,12 @@
|
|||||||
// A NEW complete waveform arrives on the morph — from the dots,
|
// A NEW complete waveform arrives on the morph — from the dots,
|
||||||
// from the swell mid-sweep, from the previous track's shape, or
|
// from the swell mid-sweep, from the previous track's shape, or
|
||||||
// from the progressive peaks the final stream decode replaces.
|
// from the progressive peaks the final stream decode replaces.
|
||||||
// Only on a peaks swap at a matching bar count: a pure resize
|
// Only on a peaks swap: a pure resize re-lays the same shape and
|
||||||
// re-lays the same shape and stays instant.
|
// stays instant. A count mismatch is refitted, not rejected — the
|
||||||
if (peaksSwapped && lastDrawnHeights?.length === count) {
|
// lane's width follows the title, so track changes change counts.
|
||||||
morphFrom = [...lastDrawnHeights];
|
if (peaksSwapped && lastDrawnHeights?.length) {
|
||||||
|
const from = fitHeights(lastDrawnHeights, count);
|
||||||
|
morphFrom = from === lastDrawnHeights ? [...from] : from;
|
||||||
morphStart = now;
|
morphStart = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,14 @@ export const ARRIVE_MS = 1000;
|
|||||||
* arrival — a local file, a cached stream — lands inside this window and
|
* arrival — a local file, a cached stream — lands inside this window and
|
||||||
* morphs shape→shape, one waveform transforming into the other; a slow load
|
* morphs shape→shape, one waveform transforming into the other; a slow load
|
||||||
* falls to the dots-and-swell story when the hold expires. Without the hold,
|
* falls to the dots-and-swell story when the hold expires. Without the hold,
|
||||||
* the cleared lane always morphed up from the dots. */
|
* the cleared lane always morphed up from the dots.
|
||||||
export const ARRIVE_HOLD_MS = 600;
|
*
|
||||||
|
* 2000, up from 600: an arrival's timing is decode time, and the same track
|
||||||
|
* costs ~5ms once the session has its waveform in memory but hundreds of ms
|
||||||
|
* to over a second on the first, cold decode — a 600ms hold made the morph
|
||||||
|
* a coin flip on exactly the tracks it was built for. Fresh-stream searches
|
||||||
|
* take well past 2s, so the loading story still gets the lane. */
|
||||||
|
export const ARRIVE_HOLD_MS = 2000;
|
||||||
/** Resting/queued bars render the muted grey at this opacity — the rail's
|
/** Resting/queued bars render the muted grey at this opacity — the rail's
|
||||||
* pending dim, shared so the two lanes' dots match. */
|
* pending dim, shared so the two lanes' dots match. */
|
||||||
export const PENDING_ALPHA = 0.45;
|
export const PENDING_ALPHA = 0.45;
|
||||||
|
|||||||
Reference in new issue
Block a user