Files
STSheets/tests/pipeline/tempo.test.ts
T
2026-10-30 03:35:13 +01:00

61 lines
2.5 KiB
TypeScript

import { test, expect, beforeAll } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildSong, loadSongDir } from "../../src/bench/dataset.ts";
import { estimateGrid, gridFromTruth } from "../../src/pipeline/tempo.ts";
import type { TierId } from "../../src/gen/tiers.ts";
type Row = { id: string; trueBpm: number; estBpm: number; factor: number; beatErrMs: number; barErrMs: number };
const rows: Row[] = [];
let root = "";
function circ(x: number, period: number): number {
const m = ((x % period) + period) % period;
return Math.min(m, period - m);
}
beforeAll(async () => {
root = await mkdtemp(join(tmpdir(), "stsheets-tempo-"));
for (const tier of ["t1b", "t1c"] as TierId[]) {
for (let seed = 1; seed <= 5; seed++) {
const dir = await buildSong(tier, seed, root);
const { song, mix } = await loadSongDir(dir);
const g = estimateGrid(mix);
const beat = 60 / song.bpm;
const f = [0.5, 1, 2].find((c) => Math.abs(g.bpm / song.bpm - c) / c < 0.02) ?? 0;
// Compare against the estimated beat period so a 2x/0.5x grid is judged on its own beat.
const estBeat = 60 / g.bpm;
rows.push({
id: `${tier}-${seed}`, trueBpm: song.bpm, estBpm: g.bpm, factor: f,
beatErrMs: circ(g.firstDownbeat, estBeat) * 1000,
barErrMs: circ(g.firstDownbeat, estBeat * g.beatsPerBar) * 1000,
});
void beat;
}
}
console.log(rows.map((r) => `${r.id} true=${r.trueBpm} est=${r.estBpm.toFixed(2)} f=${r.factor} beatErr=${r.beatErrMs.toFixed(1)}ms barErr=${r.barErrMs.toFixed(1)}ms`).join("\n"));
}, 600_000);
test("gridFromTruth is the oracle grid", () => {
expect(gridFromTruth(80, 4)).toEqual({ bpm: 80, firstDownbeat: 0, beatsPerBar: 4 });
});
test("tempo within 2% (allowing 0.5x/1x/2x) in >= 8 of 10 songs", () => {
expect(rows.filter((r) => r.factor !== 0).length).toBeGreaterThanOrEqual(8);
});
test("beat phase within 50 ms of 0 in >= 8 of 10 songs", () => {
expect(rows.filter((r) => r.beatErrMs <= 50).length).toBeGreaterThanOrEqual(8);
});
test("report exact 1x and downbeat-phase hit counts", () => {
const exact = rows.filter((r) => r.factor === 1).length;
const bar = rows.filter((r) => r.factor === 1 && r.barErrMs <= 50).length;
console.log(`exact 1x: ${exact}/10, bar-phase right: ${bar}/10`);
expect(exact).toBeGreaterThanOrEqual(0);
});
import { afterAll } from "bun:test";
afterAll(async () => { if (root) await rm(root, { recursive: true, force: true }); });