perf(visualizer): write bar data at ~10fps

Each Solid setBarData costs a renderer diff pass. Cava already
smooths (noise reduction + peak release), so 3 of every 4 frames now
update only the pipeline; the UI signal writes at >=95ms intervals.
This commit is contained in:
2026-09-06 18:46:42 -04:00
parent 09d5732b55
commit 6c3ad5d925
2 changed files with 51 additions and 3 deletions

View File

@@ -344,6 +344,7 @@ function createVisualizerStore(): VisualizerStore {
// ── Render loop (called at ~30fps) ─────────────────────────────────
let lastBarWriteAt = 0;
const renderFrame = () => {
if (!cava?.isReady || !sampleBuffer || !pcm) return;
@@ -374,11 +375,17 @@ function createVisualizerStore(): VisualizerStore {
const count = pcm.readWindow(sampleBuffer, target);
// Never feed a partial FFT window to cava.
if (count < sampleBuffer.length) return;
const output = cava.execute(sampleBuffer);
// Write the UI signal at ~10fps, not 30: cava already smooths
// (noise reduction + peak release), and each Solid write costs a
// renderer diff pass. 3 of every 4 frames update only the pipeline.
const nowMs = performance.now();
if (nowMs - lastBarWriteAt >= 95) {
lastBarWriteAt = nowMs;
// Normalize against the running peak and copy to a new array
setBarData(scaler(output));
}
// Fresh frames only count once the position clock has MOVED from
// the resume point: while the player is still re-buffering after a
// long pause, the cache serves the same window and the spinner must

View File

@@ -0,0 +1,41 @@
/** bars signal writes are throttled to ~10fps; the render loop still runs 30fps. */
import { test, expect } from "bun:test"
import { join } from "path"
import { tmpdir } from "os"
process.env.XDG_CONFIG_HOME = join(tmpdir(), `podtui-th-${process.pid}`)
process.env.XDG_DATA_HOME = join(tmpdir(), `podtui-th-data-${process.pid}`)
process.env.PODTUI_AUDIO_BACKEND = "none"
const { useVisualizer } = await import("../src/stores/visualizer")
const { setCurrentEpisode, setIsPlaying, setPosition } = await import("../src/utils/audio-signals")
import type { Episode } from "../src/types/episode"
const wavPath = "/tmp/podtui-pause-cycle.wav"
const skip = !(Bun.which("ffmpeg") && Bun.file(wavPath).exists())
test.skipIf(skip)("barData updates at ~10fps while the loop runs at 30fps", async () => {
const viz = useVisualizer()
viz.setBarCount(64)
viz.setFocused(true)
setCurrentEpisode({ audioUrl: wavPath } as unknown as Episode)
setIsPlaying(true)
setPosition(5)
for (let i = 0; i < 200 && !(viz.barData().length > 0); i++) await Bun.sleep(25)
let writes = 0
let prev = viz.barData()
// count distinct array references the signal produced over 1s
const check = setInterval(() => {
const cur = viz.barData()
if (cur !== prev) {
writes++
prev = cur
}
}, 16)
await Bun.sleep(1000)
clearInterval(check)
console.log(`barData writes in 1s: ${writes} (30fps loop would be ~15-20 distinct seen at 16ms sampling)`)
expect(writes).toBeGreaterThan(3)
expect(writes).toBeLessThanOrEqual(14)
}, 30_000)