Two separate costs, both measured on musicdolphin (Pi 4, 1920x1080 kiosk), where the app was burning ~70% of a core with nothing happening on screen. Per-frame work. The ambient canvas repaints a full-screen gradient plus a particle field every frame, and `usePlaybackClock` pushes a React setState per animation frame into both PlayView and PlayerBar for the whole length of a track. `?pi=1` (lib/ lowPower.ts) makes those cheaper rather than switching them off: the canvas paints a quarter of the pixels at 30fps with a bubble cap, and the clock renders ten times a second - a progress bar advances one pixel every few hundred ms and its label has one-second resolution, so nothing on screen can tell. Only the effects with no cheap version actually go: the backdrop-filter glass blur and the decorative CSS loops. Also drops a redundant full-canvas clearRect that the opaque gradient always covered. Search. normalize() runs a Unicode NFD decomposition, and albumMatches/songMatches called it on every album title and every track title on every keystroke - 4969 of them for a track search, whose answer cannot change until the library does. buildSearchIndex does it once per library payload; a keystroke is now String.includes over strings that already exist. On the real library that is 33ms -> 3.4ms for an eight-letter track query on a laptop, and this runs on a Pi. The same index partitions albums by shelf and pre-sorts each shelf's categories, which App and BrowseView were deriving separately from the same data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
44 lines
1.7 KiB
TypeScript
44 lines
1.7 KiB
TypeScript
/** A smooth playback position, from a position that only arrives twice a second.
|
|
|
|
Driving a progress bar straight off the websocket visibly steps. This interpolates
|
|
between frames on every animation frame and re-seeds whenever a real one lands, which
|
|
also gives beat-synced animation the frame-accurate clock it will need later.
|
|
*/
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
import { PLAYBACK_CLOCK_FPS } from "../lib/lowPower";
|
|
|
|
/** A render budget, not a timer - rAF still drives the loop, this only decides which
|
|
* frames are allowed to push state. The 1ms slack keeps a budget from landing just
|
|
* inside a vsync interval and silently halving the rate it asked for. */
|
|
const MIN_RENDER_MS = PLAYBACK_CLOCK_FPS > 0 ? 1000 / PLAYBACK_CLOCK_FPS - 1 : 0;
|
|
|
|
export function usePlaybackClock(position: number, playing: boolean): number {
|
|
const [interpolated, setInterpolated] = useState(position);
|
|
const anchor = useRef({ position, at: performance.now() });
|
|
|
|
// A new server frame is the truth; restart the interpolation from it.
|
|
useEffect(() => {
|
|
anchor.current = { position, at: performance.now() };
|
|
setInterpolated(position);
|
|
}, [position]);
|
|
|
|
useEffect(() => {
|
|
if (!playing) return;
|
|
let frame = 0;
|
|
let lastRender = 0;
|
|
const tick = (now: number) => {
|
|
frame = requestAnimationFrame(tick);
|
|
if (now - lastRender < MIN_RENDER_MS) return;
|
|
lastRender = now;
|
|
const { position: base, at } = anchor.current;
|
|
setInterpolated(base + (performance.now() - at) / 1000);
|
|
};
|
|
frame = requestAnimationFrame(tick);
|
|
return () => cancelAnimationFrame(frame);
|
|
}, [playing]);
|
|
|
|
return playing ? interpolated : position;
|
|
}
|