Files

76 lines
4.4 KiB
TypeScript

import { expect, test } from "bun:test";
import { scoreSong, type PipelineOutput } from "../../src/eval/score.ts";
import { secondsToTicks } from "../../src/core/time.ts";
import type { TimedNote, ScoreNote, TruthTrack } from "../../src/core/types.ts";
const flags = { oracleStems: false, oracleTempo: false, oracleNotes: false };
const bpm = 120; // 0.5 s per beat
const n = (onset: number, pitch: number, string?: number): TimedNote => ({ onset, offset: onset + 0.5, pitch, string });
const sc = (t: TimedNote[]): ScoreNote[] => t.map((x) => ({
tick: Math.round(secondsToTicks(x.onset, bpm)), dur: Math.round(secondsToTicks(x.offset - x.onset, bpm)), pitch: x.pitch, string: x.string,
}));
const grid = { bpm, firstDownbeat: 0, beatsPerBar: 4 };
test("two ref guitars, one merged out track", () => {
const g1 = [n(0, 64, 1), n(0.5, 62, 2)];
const g2 = [n(0, 40, 6), n(0.5, 45, 5)];
const truth = { bpm, timeSig: [4, 4] as [number, number], tracks: [
{ name: "g1", role: "guitar", notes: g1 }, { name: "g2", role: "guitar", notes: g2 } ] as TruthTrack[] };
const merged = [...g1, ...g2];
const out: PipelineOutput = { grid, tracks: [{ name: "o", role: "guitar", notes: merged, scoreNotes: sc(merged) }] };
const s = scoreSong(truth, out, flags);
expect(s.guitarMerged.f1).toBe(1);
expect(s.guitarMerged.pass).toBe(true);
expect(s.partSplit).not.toBeNull();
expect(s.partSplit!.outTracks).toBe(1);
expect(s.partSplit!.refTracks).toBe(2);
expect(s.partSplit!.f1).toBeCloseTo(0.5, 6);
expect(s.rhythm).toEqual({ factor: 1, beatOffset: 0, onBeat: 1, durationOk: 1 });
expect(s.stringAgreement).toBe(1);
expect(s.spuriousRate).toBe(0);
expect(s.perRole.vocals!.f1).toBeNull();
expect(s.drumsPerClass).toBeNull();
});
test("partSplit null with one ref guitar; rhythm null without guitar", () => {
const truth = { bpm, timeSig: [4, 4] as [number, number], tracks: [{ name: "g", role: "guitar", notes: [n(0, 60)] }] as TruthTrack[] };
const s = scoreSong(truth, { grid, tracks: [] }, flags);
expect(s.partSplit).toBeNull();
expect(s.guitarMerged.recall).toBe(0);
expect(s.guitarMerged.pass).toBe(false);
expect(s.rhythm).toBeNull();
expect(s.spuriousRate).toBeNull();
});
test("drums per class", () => {
const truth = { bpm, timeSig: [4, 4] as [number, number], tracks: [{ name: "d", role: "drums", notes: [n(0, 36), n(0.5, 38)] }] as TruthTrack[] };
const est = [n(0, 35), n(0.5, 42)];
const s = scoreSong(truth, { grid, tracks: [{ name: "d", role: "drums", notes: est, scoreNotes: [] }] }, flags);
expect(s.drumsPerClass!.kick.f1).toBe(1);
expect(s.drumsPerClass!.snare.recall).toBe(0);
expect(s.drumsPerClass!.hihat.precision).toBe(0);
expect(s.drumsPerClass!.tom.f1).toBeNull();
});
test("gpPitch and barAgreement come from the output GP file (offset by grid.firstDownbeat)", async () => {
const { songToGp, truthFromGp } = await import("../../src/score/gp.ts");
const { generateSong } = await import("../../src/gen/generate.ts");
const song = generateSong("t1a", 1);
const gp = songToGp(song);
const truthGp = truthFromGp(gp);
const outNotes = truthGp.tracks.filter((t) => t.role === "guitar").flatMap((t) => t.notes);
const mkOut = (firstDownbeat: number): PipelineOutput =>
({ grid: { bpm: truthGp.bpm, firstDownbeat, beatsPerBar: 4 }, tracks: [{ name: "o", role: "guitar", notes: outNotes, scoreNotes: [] }] });
const s = scoreSong(truthGp, mkOut(0), flags, gp);
expect(s.gpPitch!.f1).toBe(1);
expect(s.barAgreement).toEqual({ refBars: truthGp.bars, outBars: truthGp.bars, timeSigMatch: true });
// GP time zero is the grid's first downbeat: a 0.25 s downbeat shifts the GP notes beyond the 50 ms tolerance
expect(scoreSong(truthGp, mkOut(0.25), flags, gp).gpPitch!.f1!).toBeLessThan(0.5);
// the truth shifted by the same downbeat matches again
const shifted = { ...truthGp, tracks: truthGp.tracks.map((t) => ({ ...t, notes: t.notes.map((x) => ({ ...x, onset: x.onset + 0.25, offset: x.offset + 0.25 })) })) };
expect(scoreSong(shifted, mkOut(0.25), flags, gp).gpPitch!.f1).toBe(1);
// without an output GP both are null; a different bar count is reported
const none = scoreSong(truthGp, mkOut(0), flags);
expect(none.gpPitch).toBeNull();
expect(none.barAgreement).toBeNull();
expect(scoreSong({ ...truthGp, bars: truthGp.bars + 2 }, mkOut(0), flags, gp).barAgreement).toEqual({ refBars: truthGp.bars + 2, outBars: truthGp.bars, timeSigMatch: true });
});