feat: sync waveform to live player position (mpv time-pos, position-window reads, smooth clock)

This commit is contained in:
2026-08-10 15:51:02 -04:00
parent e70469b1ec
commit 19eae4fd5a
5 changed files with 373 additions and 195 deletions

View File

@@ -88,6 +88,29 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
return true;
};
// ── Smooth position clock ──────────────────────────────────────────
//
// audio.position() updates at the useAudio poll rate (~150ms). Between
// polls, interpolate the position from wall time so the FFT window (and
// the played/future split) tracks the audio continuously instead of
// stepping. The 0.5s cap prevents extrapolating far beyond reality when
// the player stalls (e.g. network re-buffering).
let lastPolledPosition = 0;
let lastPolledAt = 0;
const smoothPosition = () => {
const pos = audio.position();
const now = performance.now();
if (pos !== lastPolledPosition) {
lastPolledPosition = pos;
lastPolledAt = now;
return pos;
}
if (lastPolledAt === 0) return pos;
const elapsed = Math.min((now - lastPolledAt) / 1000, 0.5);
return lastPolledPosition + elapsed * (audio.speed() ?? 1);
};
// ── Start/stop the visualization pipeline ──────────────────────────
const startVisualization = (url: string, position: number, speed: number) => {
@@ -139,7 +162,11 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
const renderFrame = () => {
if (!cava?.isReady || !reader?.running || !sampleBuffer) return;
const count = reader.read(sampleBuffer);
// Sample the FFT window at the player's position, not the decode
// head — the reader decodes independently and only the position clock
// ties the bars to what's actually playing.
const target = smoothPosition();
const count = reader.read(sampleBuffer, target);
if (count === 0) return;
const input =
@@ -212,7 +239,7 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
const playedRatio = () =>
audio.duration() <= 0
? 0
: Math.min(1, audio.position() / audio.duration());
: Math.min(1, smoothPosition() / audio.duration());
const renderLine = () => {
const bars = barData();