fix: final-review findings (median pass tick, R11 secondary metrics, R12 chord duration, render version, dsp resample)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 05:55:04 +01:00
co-authored by Claude Sonnet 5.5
parent 2b7d5ffbeb
commit b631bd2c7e
16 changed files with 199 additions and 48 deletions
+21 -2
View File
@@ -1,6 +1,7 @@
import { secondsToTicks } from "../core/time.ts";
import type { Grid, Role, ScoreNote, TimedNote, TruthTrack } from "../core/types.ts";
import { matchNotes, maxAssignment, prf, type MatchResult } from "./notes.ts";
import { truthFromGp } from "../score/gp.ts";
import { alignGrid, rhythmScore } from "./rhythm.ts";
export interface OracleFlags { oracleStems: boolean; oracleTempo: boolean; oracleNotes: boolean }
@@ -16,6 +17,10 @@ export interface SongScore {
rhythm: { factor: number; beatOffset: number; onBeat: number | null; durationOk: number | null } | null;
stringAgreement: number | null;
spuriousRate: number | null;
/** Merged-guitar pitch match computed from the re-imported output GP file (null without outGp). */
gpPitch: MatchResult | null;
/** Truth vs output-GP bar count and time signature (null without outGp). */
barAgreement: { refBars: number; outBars: number; timeSigMatch: boolean } | null;
}
export const ROLE_ORDER: Role[] = ["guitar", "vocals", "bass", "drums", "keys"];
@@ -39,8 +44,8 @@ const notesOf = (tracks: { role: Role; notes: TimedNote[] }[], role: Role) =>
tracks.filter((t) => t.role === role).flatMap((t) => t.notes);
export function scoreSong(
truth: { bpm: number; timeSig: [number, number]; tracks: TruthTrack[] },
out: PipelineOutput, flags: OracleFlags,
truth: { bpm: number; timeSig: [number, number]; bars?: number; tracks: TruthTrack[] },
out: PipelineOutput, flags: OracleFlags, outGp?: Uint8Array,
): SongScore {
const refGuitar = notesOf(truth.tracks, "guitar");
const outGuitar = notesOf(out.tracks, "guitar");
@@ -101,9 +106,23 @@ export function scoreSong(
if (a !== undefined && b !== undefined) { withString++; if (a === b) agree++; }
}
let gpPitch: SongScore["gpPitch"] = null;
let barAgreement: SongScore["barAgreement"] = null;
if (outGp) {
const re = truthFromGp(outGp);
const shift = out.grid.firstDownbeat; // the GP's time zero is the grid's first downbeat
const gpGuitar = notesOf(re.tracks, "guitar").map((n) => ({ ...n, onset: n.onset + shift, offset: n.offset + shift }));
gpPitch = matchNotes(refGuitar, gpGuitar);
const barSec = (60 / truth.bpm) * truth.timeSig[0] * (4 / truth.timeSig[1]);
const lastEnd = Math.max(0, ...truth.tracks.flatMap((t) => t.notes.map((n) => n.offset)));
const refBars = truth.bars ?? Math.ceil(lastEnd / barSec - 1e-9);
barAgreement = { refBars, outBars: re.bars, timeSigMatch: truth.timeSig[0] === re.timeSig[0] && truth.timeSig[1] === re.timeSig[1] };
}
return {
flags, guitarMerged, partSplit, perRole, drumsPerClass, rhythm,
stringAgreement: withString ? agree / withString : null,
spuriousRate: outGuitar.length ? gm.fp / outGuitar.length : null,
gpPitch, barAgreement,
};
}