reduce waveform mem consumption

This commit is contained in:
2026-09-22 07:37:53 -04:00
parent f5f4bc73b9
commit 0640172f05

View File

@@ -13,7 +13,7 @@
* and a dotted placeholder when idle. * 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 { useTerminalDimensions } from "@opentui/solid";
import { useVisualizer } from "@/stores/visualizer"; import { useVisualizer } from "@/stores/visualizer";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
@@ -45,46 +45,40 @@ export function RealtimeWaveform() {
createEffect(on(numBars, (n) => viz.setBarCount(n))); createEffect(on(numBars, (n) => viz.setBarCount(n)));
// ── Rendering ────────────────────────────────────────────────────── // ── 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 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 <LoadingIndicator />;
}
if (bars.length === 0) { if (bars.length === 0) {
const placeholder = ".".repeat(count); const placeholder = ".".repeat(numBars());
return ( return { top: placeholder, bottom: placeholder };
<box flexDirection="column" gap={0}>
<text fg={theme.primary}>{placeholder}</text>
<text fg={theme.primary}>{placeholder}</text>
</box>
);
} }
const pairs = bars.map((v) => barChars(Math.floor(v * BAR_LEVELS))); const pairs = bars.map((v) => barChars(Math.floor(v * BAR_LEVELS)));
const top = pairs.map((pair) => pair.top).join(""); return {
const bottom = pairs.map((pair) => pair.bottom).join(""); top: pairs.map((pair) => pair.top).join(""),
bottom: pairs.map((pair) => pair.bottom).join(""),
return ( };
<box flexDirection="column" gap={0}> });
<text fg={theme.primary}>{top}</text>
<text fg={theme.primary}>{bottom}</text>
</box>
);
};
return ( return (
<box border borderColor={theme.border} padding={1}> <box border borderColor={theme.border} padding={1}>
{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. */}
<Show
when={!viz.isLoading() && !viz.isStalled()}
fallback={<LoadingIndicator />}
>
<box flexDirection="column" gap={0}>
<text fg={theme.primary}>{lines().top}</text>
<text fg={theme.primary}>{lines().bottom}</text>
</box>
</Show>
</box> </box>
); );
} }