feat: tempo grid estimation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
1239e5e0ac
commit
4ab92d4a86
@@ -0,0 +1,60 @@
|
||||
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 }); });
|
||||
Reference in New Issue
Block a user