import { formatQuarterLengths, quartersMatch } from "@/lib/duration"; export type TimeSignature = { beats: number; beatType: number; }; export type ScoreEvent = { id: string; kind: "note" | "rest"; pitch: string | null; quarters: number; label: string; tied: boolean; }; export type Measure = { id: string; events: ScoreEvent[]; /** A forward repeat barline stands at the start of this measure. */ repeatForward?: boolean; /** A backward repeat ends on this measure. The number is how many times to play the passage. */ repeatTimes?: number; }; export type ScoreOrigin = "sample" | "upload" | "blank"; export type Score = { title: string; attribution: string; timeSignature: TimeSignature; measures: Measure[]; origin: ScoreOrigin; }; export const TIME_SIGNATURES: TimeSignature[] = [ { beats: 2, beatType: 4 }, { beats: 3, beatType: 4 }, { beats: 4, beatType: 4 }, { beats: 6, beatType: 8 }, { beats: 3, beatType: 8 }, { beats: 2, beatType: 2 }, ]; export function newId(prefix: string): string { return `${prefix}-${Math.random().toString(36).slice(2, 10)}`; } export function signatureKey(timeSignature: TimeSignature): string { return `${timeSignature.beats}/${timeSignature.beatType}`; } export function parseSignature(key: string): TimeSignature { const [beats, beatType] = key.split("/").map(Number); return { beats, beatType }; } export function expectedQuarters(timeSignature: TimeSignature): number { return (timeSignature.beats * 4) / timeSignature.beatType; } export function barStatus( measure: Measure, timeSignature: TimeSignature, ): { ok: boolean; text: string } { const sum = measure.events.reduce((total, event) => total + event.quarters, 0); const expected = expectedQuarters(timeSignature); const signature = signatureKey(timeSignature); if (quartersMatch(sum, expected)) { return { ok: true, text: `${signature} ยท ${formatQuarterLengths(sum)}` }; } return { ok: false, text: `${formatQuarterLengths(sum)} in this bar; ${signature} holds ${formatQuarterLengths(expected)}.`, }; } export function startBeats(events: ScoreEvent[], timeSignature: TimeSignature): number[] { const quartersPerBeat = 4 / timeSignature.beatType; let cursor = 0; return events.map((event) => { const beat = cursor / quartersPerBeat + 1; cursor += event.quarters; return beat; }); } export function formatBeat(beat: number): string { const rounded = Math.round(beat * 100) / 100; return Number.isInteger(rounded) ? String(rounded) : String(rounded); } /** Extra full beats after the note itself. A half note in 4/4 returns 1. A dotted quarter returns 0. */ export function extraBeatArrows(quarters: number, timeSignature: TimeSignature): number { const perBeat = 4 / timeSignature.beatType; if (!(perBeat > 0) || !(quarters > 0)) return 0; const wholeBeats = Math.floor(quarters / perBeat + 1e-6); return Math.max(0, wholeBeats - 1); } /** Group a lead into horizontal rows. A row holds about `budget` notes, and never splits a bar. */ export function leadRows(measures: T[], budget = 14): T[][] { const rows: T[][] = []; let current: T[] = []; let count = 0; for (const measure of measures) { const notes = Math.max(1, measure.events.length); if (current.length > 0 && count + notes > budget) { rows.push(current); current = []; count = 0; } current.push(measure); count += notes; } if (current.length > 0) rows.push(current); return rows; } export function blankScore(): Score { return { title: "Untitled line", attribution: "Hand-entered lead line. Add notes, then read the tabs.", timeSignature: { beats: 4, beatType: 4 }, origin: "blank", measures: [{ id: newId("m"), events: [] }], }; }