fix: drum single hit, dedupe notes, validate role

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 02:42:35 +01:00
co-authored by Claude Sonnet 5.5
parent ec7cb0e568
commit 470e089f92
3 changed files with 40 additions and 2 deletions
+6 -2
View File
@@ -25,11 +25,15 @@ function checkGrid(v: number, what: string): void {
/** Group by onset; length = shortest dur in group clipped to next onset; gaps become rests; padded to totalTicks. */
function buildBeats(notes: ScoreNote[], totalTicks: number): Beat[] {
const groups = new Map<number, ScoreNote[]>();
const seen = new Set<string>();
for (const n of notes) {
const key = `${n.tick}:${n.pitch}`;
if (seen.has(key)) continue; // exact duplicate (same tick+pitch)
seen.add(key);
checkGrid(n.tick, "tick");
checkGrid(n.dur, "dur");
if (n.dur <= 0) throw new Error("note dur must be > 0");
if (n.tick + 0 >= totalTicks) throw new Error(`note at tick ${n.tick} beyond song end ${totalTicks}`);
if (n.tick >= totalTicks) throw new Error(`note at tick ${n.tick} beyond song end ${totalTicks}`);
(groups.get(n.tick) ?? groups.set(n.tick, []).get(n.tick)!).push(n);
}
const ticks = [...groups.keys()].sort((a, b) => a - b);
@@ -92,7 +96,7 @@ function trackToTex(track: Track, barTicks: number, bars: number): string {
const segEnd = Math.min(end, barEnd);
for (const piece of decompose(segEnd - pos)) {
const tie = !first && !drums && beat.notes.length > 0;
const body = beat.notes.length === 0
const body = beat.notes.length === 0 || (drums && !first)
? "r"
: beat.notes.length === 1 ? noteText(track, beat.notes[0]!, tie) : `(${beat.notes.map((n) => noteText(track, n, tie)).join(" ")})`;
perBar[Math.floor(pos / barTicks)]!.push(`:${piece.div} ${body}${piece.dotted ? " {d}" : ""}`);
+3
View File
@@ -41,6 +41,8 @@ export function songToMidi(song: Song): at.midi.MidiFile {
return midi;
}
const ROLES: readonly string[] = ["guitar", "bass", "keys", "vocals", "drums"];
export function truthFromGp(bytes: Uint8Array): { bpm: number; timeSig: [number, number]; tracks: TruthTrack[] } {
const score = at.importer.ScoreLoader.loadScoreFromBytes(bytes, new at.Settings());
const bpm = score.tempo;
@@ -73,6 +75,7 @@ export function truthFromGp(bytes: Uint8Array): { bpm: number; timeSig: [number,
}
}
});
if (!ROLES.includes(track.shortName as Role)) throw new Error(`track "${track.name}": shortName "${track.shortName}" is not a valid Role`);
const tr: TruthTrack = { name: track.name, role: track.shortName as Role, notes };
if (!track.isPercussion) tr.tuning = [...staff.tuning];
return tr;