- 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)
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
/**
|
||
* Pure bar-scaling helpers for the terminal waveform.
|
||
*
|
||
* barChars maps a 0..16 level to the two characters of a 2-row bar built
|
||
* from Unicode lower block elements (U+2581..U+2588). The partial block
|
||
* sits in the TOP row (its glyph bottom edge = row bottom), so a full
|
||
* block below makes a visually continuous 2-cell column — the "double the
|
||
* default height" requirement (each bar = 2 terminal rows, 16 heights).
|
||
*
|
||
* createBarScaler is a stateful fast-attack / slow-release peak follower
|
||
* that replaces cava's autosens: a loud start cannot pin every bar at
|
||
* full height (the peak follower absorbs it) and quiet content gets
|
||
* normalized up.
|
||
*/
|
||
|
||
// ── Types ────────────────────────────────────────────────────────────
|
||
|
||
export interface BarScalerOptions {
|
||
/** Peak follower decay per frame (default: 0.985) */
|
||
release?: number;
|
||
/** Power curve applied after normalization (default: 0.7) */
|
||
curve?: number;
|
||
/** Silence threshold — below this the input is treated as silent (default: 1e-6) */
|
||
epsilon?: number;
|
||
}
|
||
|
||
// ── Constants ────────────────────────────────────────────────────────
|
||
|
||
/** Number of discrete bar heights (2 rows × 8 block levels). */
|
||
export const BAR_LEVELS = 16;
|
||
|
||
/** Lower block elements, index 0 = space (silence) through full block (max). */
|
||
const LOWER = [
|
||
" ",
|
||
"\u2581",
|
||
"\u2582",
|
||
"\u2583",
|
||
"\u2584",
|
||
"\u2585",
|
||
"\u2586",
|
||
"\u2587",
|
||
"\u2588",
|
||
];
|
||
|
||
// ── Bar mapping ──────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Map a bar level (0..16) to the two characters that render it as a
|
||
* 2-row column: top row + bottom row.
|
||
*
|
||
* level 0 → { top: " ", bottom: " " }
|
||
* level 1..8 → { top: " ", bottom: LOWER[level] }
|
||
* level 9..16 → { top: LOWER[level - 8], bottom: "\u2588" }
|
||
*/
|
||
export function barChars(level: number): { top: string; bottom: string } {
|
||
const raw = Math.floor(level);
|
||
const lvl = Number.isFinite(raw)
|
||
? Math.max(0, Math.min(BAR_LEVELS, raw))
|
||
: 0;
|
||
|
||
if (lvl === 0) return { top: " ", bottom: " " };
|
||
if (lvl <= 8) return { top: " ", bottom: LOWER[lvl] };
|
||
return { top: LOWER[lvl - 8], bottom: "\u2588" };
|
||
}
|
||
|
||
// ── Peak-follower scaler ─────────────────────────────────────────────
|
||
|
||
const clamp01 = (value: number): number => Math.max(0, Math.min(1, value));
|
||
|
||
/**
|
||
* Create a stateful bar scaler. Each call normalizes its input against a
|
||
* peak follower (instant attack, multiplicative release), then applies a
|
||
* power curve so low-energy content remains visible. Returns a new
|
||
* number[] per call.
|
||
*/
|
||
export function createBarScaler(
|
||
opts?: BarScalerOptions,
|
||
): (values: ArrayLike<number>) => number[] {
|
||
const release = opts?.release ?? 0.985;
|
||
const curve = opts?.curve ?? 0.7;
|
||
const epsilon = opts?.epsilon ?? 1e-6;
|
||
let peak = 0;
|
||
|
||
return (values: ArrayLike<number>): number[] => {
|
||
let frameMax = 0;
|
||
for (let i = 0; i < values.length; i++) {
|
||
const magnitude = Math.abs(values[i]);
|
||
if (magnitude > frameMax) frameMax = magnitude;
|
||
}
|
||
|
||
// Fast attack, slow release
|
||
peak = frameMax > peak ? frameMax : peak * release;
|
||
|
||
const gain = peak > epsilon ? 1 / peak : 0;
|
||
|
||
const output = new Array<number>(values.length);
|
||
for (let i = 0; i < values.length; i++) {
|
||
output[i] = Math.pow(clamp01(values[i] * gain), curve);
|
||
}
|
||
return output;
|
||
};
|
||
}
|