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(); 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"); } });