/**
* RealtimeWaveform — renders the shared visualizer pipeline state.
*
* The pipeline (ffmpeg decode + cavacore FFT) lives in the module-level
* visualizer store (`@/stores/visualizer`), not in this component, so it
* survives PlayerPage unmounts: leaving the Player tab keeps the waveform
* warm for VISUALIZER_UNLOAD_DELAY_MS, then the store tears it down.
*
* This component only subscribes to store state, reports the width-derived
* bar count (terminal resize re-inits the running pipeline), and renders:
* a braille spinner while the pipeline is loading its first frames, the
* frequency bars once frames arrive, and a dotted placeholder when idle.
*/
import { createEffect, on } from "solid-js";
import { useTerminalDimensions } from "@opentui/solid";
import { useVisualizer } from "@/stores/visualizer";
import { useTheme } from "@/context/ThemeContext";
import { LoadingIndicator } from "@/components/LoadingIndicator";
import { BAR_LEVELS, barChars } from "@/utils/bar-mapping";
import { PANE_RATIO } from "@/utils/navigation";
// ── Component ────────────────────────────────────────────────────────
export function RealtimeWaveform() {
const { theme } = useTheme();
const viz = useVisualizer();
// Bar count scales with terminal width so the waveform fills its pane.
// The player is a 2-pane row: current column = (current+preview) of
// (parent+current+preview) of the terminal width. Subtract ~8 chars of
// chrome (scrollbox border + box padding + waveform border + padding).
// Falls back to 64 before the renderer reports a real size.
const dimensions = useTerminalDimensions();
const numBars = () => {
const total = PANE_RATIO.parent + PANE_RATIO.current + PANE_RATIO.preview;
const current = PANE_RATIO.current + PANE_RATIO.preview; // 2-pane grows current
const width = dimensions().width;
if (!width) return 64;
return Math.max(
8,
Math.min(256, Math.floor((width * current) / total) - 8),
);
};
// Keep the store's bar count in sync with the terminal width; the store
// re-inits the running pipeline when it changes (terminal resize).
createEffect(on(numBars, (n) => viz.setBarCount(n)));
// ── Rendering ──────────────────────────────────────────────────────
const renderLine = () => {
const bars = viz.barData();
const count = numBars();
// Loading state: the braille spinner shows while the pipeline warms
// up — but only when there are no bars to render yet (first play /
// after an unload). On resume/seek the last bars stay on screen
// until fresh frames arrive, so the waveform never blanks out for
// the (multi-second, network-bound) cold start.
if (bars.length === 0 && viz.isLoading()) {
return ;
}
if (bars.length === 0) {
const placeholder = ".".repeat(count);
return (
{placeholder}
{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 (
{renderLine()}
);
}