fix: drum single hit, dedupe notes, validate role
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
ec7cb0e568
commit
470e089f92
@@ -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}" : ""}`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -80,3 +80,34 @@ test("GP round trip preserves notes, ties, strings, roles", () => {
|
||||
test("songToMidi generates events", () => {
|
||||
expect(songToMidi(song).tracks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
function drumSong(notes: { tick: number; dur: number; pitch: number }[]): Song {
|
||||
return {
|
||||
title: "d", tier: "t1", seed: 1, bpm: BPM, timeSig: [4, 4], bars: 2, key: "C",
|
||||
tracks: [{ name: "Kit", role: "drums", program: 0, pan: 0, fx: "none", gainDb: 0, notes }],
|
||||
};
|
||||
}
|
||||
|
||||
test("drum note with multi-piece duration yields exactly one hit", () => {
|
||||
const truth = truthFromGp(songToGp(drumSong([{ tick: 0, dur: 5 * S, pitch: 38 }])));
|
||||
expect(truth.tracks[0]!.notes.map((n) => [n.pitch, n.onset])).toEqual([[38, 0]]);
|
||||
});
|
||||
|
||||
test("drum note crossing a barline yields exactly one hit", () => {
|
||||
const truth = truthFromGp(songToGp(drumSong([{ tick: 3 * Q, dur: 2 * Q, pitch: 36 }])));
|
||||
expect(truth.tracks[0]!.notes.map((n) => [n.pitch, round3(n.onset)])).toEqual([[36, round3(t(3 * Q))]]);
|
||||
});
|
||||
|
||||
test("exact duplicate notes are de-duplicated", () => {
|
||||
const dup = { tick: Q, dur: Q, pitch: 60 };
|
||||
const s = drumSong([]);
|
||||
s.tracks = [{ name: "V", role: "vocals", program: 53, pan: 0, fx: "none", gainDb: 0, notes: [dup, { ...dup }] }];
|
||||
const truth = truthFromGp(songToGp(s));
|
||||
expect(truth.tracks[0]!.notes.length).toBe(1);
|
||||
});
|
||||
|
||||
test("truthFromGp rejects an invalid role short name", () => {
|
||||
const s = drumSong([{ tick: 0, dur: Q, pitch: 36 }]);
|
||||
(s.tracks[0] as any).role = "banjo";
|
||||
expect(() => truthFromGp(songToGp(s))).toThrow(/not a valid Role/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user