diff --git a/src/pages/Player/RealtimeWaveform.tsx b/src/pages/Player/RealtimeWaveform.tsx index d9e8e3a..53c5153 100644 --- a/src/pages/Player/RealtimeWaveform.tsx +++ b/src/pages/Player/RealtimeWaveform.tsx @@ -13,7 +13,7 @@ * and a dotted placeholder when idle. */ -import { createEffect, on } from "solid-js"; +import { createEffect, createMemo, on, Show } from "solid-js"; import { useTerminalDimensions } from "@opentui/solid"; import { useVisualizer } from "@/stores/visualizer"; import { useTheme } from "@/context/ThemeContext"; @@ -45,46 +45,40 @@ export function RealtimeWaveform() { createEffect(on(numBars, (n) => viz.setBarCount(n))); // ── Rendering ────────────────────────────────────────────────────── - - const renderLine = () => { + // + // The two bar rows are CONTENT of fixed text nodes — the element tree is + // created once and only the strings change. Rebuilding the box/texts per + // bar update (30/s) tears down a renderable per frame, and the renderer + // leaks ~44KB of native memory per teardown: gigabytes over one long + // playback session. + const lines = createMemo(() => { const bars = viz.barData(); - const count = numBars(); - - // Loading state: the braille spinner shows while the pipeline is - // warming up — cold start (first play / after an unload), resume - // into undecoded audio, or a stalled position clock (mpv - // re-buffering after a long pause on a network stream). The store - // clears it the moment the first fresh frame renders, so stale - // bars never masquerade as live data while the pipeline re-arms. - if (viz.isLoading() || viz.isStalled()) { - return ; - } - if (bars.length === 0) { - const placeholder = ".".repeat(count); - return ( - - {placeholder} - {placeholder} - - ); + const placeholder = ".".repeat(numBars()); + return { top: placeholder, bottom: placeholder }; } - const pairs = bars.map((v) => barChars(Math.floor(v * BAR_LEVELS))); - const top = pairs.map((pair) => pair.top).join(""); - const bottom = pairs.map((pair) => pair.bottom).join(""); - - return ( - - {top} - {bottom} - - ); - }; + return { + top: pairs.map((pair) => pair.top).join(""), + bottom: pairs.map((pair) => pair.bottom).join(""), + }; + }); return ( - {renderLine()} + {/* Loading: braille spinner while the pipeline warms up — cold + start, resume into undecoded audio, or a stalled position + clock. The store clears it the moment the first fresh frame + renders, so stale bars never masquerade as live data. */} + } + > + + {lines().top} + {lines().bottom} + + ); }