The waveform's ffmpeg decode + cavacore FFT pipeline lived inside
RealtimeWaveform, so switching away from the Player tab unmounted it and
killed the pipeline instantly — respawning ffmpeg on every return.
Move the pipeline into a module-level store (stores/visualizer.ts) that
outlives the page:
- losing Player-tab focus keeps the pipeline warm for
VISUALIZER_UNLOAD_DELAY_MS (30s), then tears it down (kills ffmpeg,
destroys the cava plan); regaining focus within the delay resumes the
warm pipeline with no restart churn; after an unload it restarts from
the current playback position.
- playback signals move to utils/audio-signals.ts (module-level, no
useAudio() owner needed) so the store reacts to play/pause/seek/speed
while no Player page is mounted; useAudio re-imports them.
- render a braille spinner as the loading state for the visualizer
(first play / after unload); stale bars stay on screen during warmup
restarts so the waveform never blanks out for the network-bound cold
start.
- seed the smooth position clock at pipeline start: with the position
still frozen at 0 while mpv opens the stream, the reader sampled a
1-sample window that could never fill, starving the bars until mpv's
first poll.
Pins the store contract in tests/visualizer-store.test.ts: loading→bars,
warm resume without restart, 30s unload, and bars while position is
frozen at 0.
The babel-preset-solid JSX transform HTML-escapes static string
children (< > → < >), which opentui renders verbatim — so the
"< > seek" hint displayed its entities. Pass the string as the
content prop instead, which bypasses the transform.
- ProgressBar: full-width played/remaining bar in the player pane, click-to-seek;
waveform no longer handles seeking or the played/future color split (pure visual)
- bars: 2 terminal rows per bar (16 levels) via barChars, partial block in the
top row so the column renders continuously
- fix bars maxing at audio start: disable cava autosens (silence gain-ramp),
pre-warm the malloc'd FFT window with zeros, skip partial FFT windows
- createBarScaler peak follower + power curve replaces cava autosens for
level-to-height mapping (src/utils/bar-mapping.ts, unit-tested)
- Current pane gets muted left/right borders only (no full box, no accent
ring); border colors are passed only when a border is requested, since
opentui flips borderless boxes to bordered when borderColor is supplied.
- Remove the Up / <current tab> / Detail titles above the panes; the current
pane's title now renders once, top-left in the parent column's header slot.
- Drop the parentLabel/previewLabel props from PaneRow and all callers.
- Remove the tab/depth indicator from the bottom-left of the status bar.
- Tests measure column widths from the border glyphs and assert the
left/right edges render muted regardless of focus.