Files
2026-10-30 02:45:32 +01:00

74 lines
3.3 KiB
TypeScript

import { test, expect } from "bun:test";
import { generateSong, TIERS } from "../../src/gen/generate.ts";
import { songToGp, truthFromGp } from "../../src/score/gp.ts";
import { BASS_STANDARD, GUITAR_STANDARD, TPQ } from "../../src/core/types.ts";
const EXPECTED_ROLES = {
t1a: ["guitar"],
t1b: ["bass", "drums", "guitar"],
t1c: ["bass", "drums", "guitar", "keys", "vocals"],
t1d: ["bass", "drums", "guitar", "guitar", "keys", "vocals"],
} as const;
test("tiers: structure, determinism, valid GP", () => {
for (const tier of ["t1a", "t1b", "t1c", "t1d"] as const) {
expect(TIERS[tier]).toBeDefined();
for (const seed of [1, 2, 3]) {
const s = generateSong(tier, seed);
expect(generateSong(tier, seed)).toEqual(s);
expect(s.bpm).toBeGreaterThanOrEqual(66);
expect(s.bpm).toBeLessThanOrEqual(96);
const guitars = s.tracks.filter((t) => t.role === "guitar");
expect(guitars.length).toBe(tier === "t1d" ? 2 : 1);
for (const g of guitars) {
expect(g.notes.length).toBeGreaterThan(0);
for (const n of g.notes) expect(GUITAR_STANDARD[n.string! - 1]! + n.fret!).toBe(n.pitch);
}
expect(s.tracks.map((t) => t.role).sort()).toEqual([...EXPECTED_ROLES[tier]]);
expect(new Set(s.tracks.map((t) => t.name)).size).toBe(s.tracks.length);
const end = s.bars * 4 * TPQ;
for (const t of s.tracks) for (const n of t.notes) {
expect(n.tick + n.dur).toBeLessThanOrEqual(end);
expect(n.tick % 240).toBe(0);
expect(n.dur % 240).toBe(0);
}
expect(truthFromGp(songToGp(s)).tracks.length).toBe(s.tracks.length);
}
}
const d = generateSong("t1d", 7);
const [g1, g2] = d.tracks.filter((t) => t.role === "guitar");
expect(Math.sign(g1!.pan)).not.toBe(Math.sign(g2!.pan));
expect(generateSong("t1a", 1)).not.toEqual(generateSong("t1a", 2));
});
test("musical sanity: bass on roots, vocals range/mono, keys range, no same-string clashes", () => {
for (const seed of [1, 2, 3, 4, 5]) {
const s = generateSong("t1d", seed);
const get = (r: string) => s.tracks.filter((t) => t.role === r);
const bass = get("bass")[0]!;
expect(bass.tuning).toEqual(BASS_STANDARD);
// bass is monophonic and each bar's first note is the same pitch class as the guitar's lowest... checked via key: all pcs in diatonic set
const ticks = bass.notes.map((n) => n.tick);
expect(new Set(ticks).size).toBe(ticks.length);
for (const n of bass.notes) expect(BASS_STANDARD[n.string! - 1]! + n.fret!).toBe(n.pitch);
const vox = get("vocals")[0]!;
expect(vox.program).toBe(53);
const sorted = [...vox.notes].sort((a, b) => a.tick - b.tick);
sorted.forEach((n, i) => {
expect(n.pitch).toBeGreaterThanOrEqual(60);
expect(n.pitch).toBeLessThanOrEqual(79);
if (i > 0) expect(sorted[i - 1]!.tick + sorted[i - 1]!.dur).toBeLessThanOrEqual(n.tick);
});
for (const n of get("keys")[0]!.notes) {
expect(n.pitch).toBeGreaterThanOrEqual(48);
expect(n.pitch).toBeLessThanOrEqual(72);
}
for (const g of get("guitar")) {
const byTick = new Map<number, number[]>();
for (const n of g.notes) (byTick.get(n.tick) ?? byTick.set(n.tick, []).get(n.tick)!).push(n.string!);
for (const strs of byTick.values()) expect(new Set(strs).size).toBe(strs.length);
}
expect(get("guitar").map((g) => g.fx).sort()).toContain("clean");
}
});