- 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)
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
/**
|
|
* bar-mapping tests — the pure waveform bar-scaling helpers.
|
|
*
|
|
* Two contracts are pinned:
|
|
*
|
|
* • barChars: the 2-row / 16-level bar rendering. The partial block
|
|
* always sits in the TOP row (glyph bottom edge = row bottom) so a
|
|
* full block below renders a visually continuous 2-cell column —
|
|
* this is the "double the default height" requirement.
|
|
*
|
|
* • createBarScaler: the peak-follower normalization that replaces
|
|
* cava's autosens. The regression this guards: bars suddenly maxing
|
|
* out when audio starts. A loud first frame must normalize to a
|
|
* single full bar (the peak), not pin every bar at full height, and
|
|
* quiet content after a loud passage must still recover (slow
|
|
* release) instead of staying dead.
|
|
*/
|
|
|
|
import { describe, test, expect } from "bun:test";
|
|
import { barChars, createBarScaler, BAR_LEVELS } from "../src/utils/bar-mapping";
|
|
|
|
describe("barChars", () => {
|
|
test("level 0 is two spaces (silence)", () => {
|
|
expect(barChars(0)).toEqual({ top: " ", bottom: " " });
|
|
});
|
|
|
|
test("levels 1..8 fill the bottom row only, partial block on top row stays empty", () => {
|
|
expect(barChars(1)).toEqual({ top: " ", bottom: "\u2581" });
|
|
expect(barChars(4)).toEqual({ top: " ", bottom: "\u2584" });
|
|
expect(barChars(8)).toEqual({ top: " ", bottom: "\u2588" });
|
|
});
|
|
|
|
test("levels 9..16 fill the bottom row and put the partial in the top row", () => {
|
|
expect(barChars(9)).toEqual({ top: "\u2581", bottom: "\u2588" });
|
|
expect(barChars(12)).toEqual({ top: "\u2584", bottom: "\u2588" });
|
|
expect(barChars(16)).toEqual({ top: "\u2588", bottom: "\u2588" });
|
|
});
|
|
|
|
test("BAR_LEVELS is 16 (double the single-row 8 levels)", () => {
|
|
expect(BAR_LEVELS).toBe(16);
|
|
});
|
|
|
|
test("clamps out-of-range and NaN levels", () => {
|
|
expect(barChars(20)).toEqual(barChars(16));
|
|
expect(barChars(-3)).toEqual(barChars(0));
|
|
expect(barChars(Number.NaN)).toEqual(barChars(0));
|
|
});
|
|
});
|
|
|
|
describe("createBarScaler", () => {
|
|
test("a loud first frame normalizes to one full bar, not all bars", () => {
|
|
const scale = createBarScaler();
|
|
const out = scale([0.9, 0.5, 0.1]);
|
|
expect(out[0]).toBeCloseTo(1, 5); // the peak maps to full height
|
|
expect(out[1]).toBeCloseTo(Math.pow(0.5 / 0.9, 0.7), 5);
|
|
expect(out[2]).toBeCloseTo(Math.pow(0.1 / 0.9, 0.7), 5);
|
|
});
|
|
|
|
test("quiet frame after a loud passage recovers via slow release", () => {
|
|
const scale = createBarScaler();
|
|
scale([0.9]);
|
|
// peak decays multiplicatively (release 0.985), so 0.05 gets
|
|
// normalized up well past its raw value instead of rendering dead.
|
|
const out = scale([0.05]);
|
|
const expectedPeak = 0.9 * 0.985;
|
|
expect(out[0]).toBeCloseTo(Math.pow(0.05 / expectedPeak, 0.7), 5);
|
|
});
|
|
|
|
test("silence maps to zeros and never inflates the peak", () => {
|
|
const scale = createBarScaler();
|
|
scale([0.8, 0.4]);
|
|
const out = scale([0, 0, 0]);
|
|
expect(out).toEqual([0, 0, 0]);
|
|
// peak keeps decaying toward silence
|
|
expect(scale([0])[0]).toBe(0);
|
|
});
|
|
|
|
test("negative values clamp to zero (no negative bars)", () => {
|
|
const scale = createBarScaler();
|
|
const out = scale([-0.5]);
|
|
expect(out[0]).toBe(0);
|
|
});
|
|
|
|
test("empty input returns an empty array", () => {
|
|
const scale = createBarScaler();
|
|
expect(scale([])).toEqual([]);
|
|
});
|
|
|
|
test("returns a fresh array each call (no aliasing of cava's buffer)", () => {
|
|
const scale = createBarScaler();
|
|
const a = scale([0.5]);
|
|
const b = scale([0.5]);
|
|
expect(a).not.toBe(b);
|
|
a[0] = 0;
|
|
expect(b[0]).not.toBe(0);
|
|
});
|
|
});
|