Token-protected Bun.serve server (bun run align [--host] [--port] [--url-host] [--root]) with the layout-A page: recording and warped-MIDI lanes on one zoomable timeline, draggable anchors (recording end / MIDI end), stretch tempo labels with warnings, fit-flagged windows, Web Audio playback with mix choice, auto-align, save, reset, undo. API per the task brief; transcode/peaks cache under reference/<id>/. Adds bun run align auto <id>, writeSmf (tests), encodeWav, recordingOnsets/fitFromOnsets, fitSummary and stretch tempo helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
36 lines
1.7 KiB
TypeScript
36 lines
1.7 KiB
TypeScript
// Tempo of alignment stretches: the MIDI's own tempo vs the tempo after warping. Pure (shared by server and page).
|
||
import type { TempoPoint } from "./smf.ts";
|
||
import type { Stretch } from "./warp.ts";
|
||
|
||
export interface StretchInfo extends Stretch { midiBpm: number; bpm: number; pct: number }
|
||
|
||
/** Average MIDI tempo over [from, to] (MIDI seconds) = beats / minutes; 120 bpm before the first tempo point. */
|
||
export function avgBpm(tempoMap: TempoPoint[], from: number, to: number): number {
|
||
const pts = [...tempoMap].sort((a, b) => a.sec - b.sec);
|
||
const bpmAt = (t: number) => { let b = 120; for (const p of pts) if (p.sec <= t) b = p.bpm; else break; return b; };
|
||
if (!(to > from)) return bpmAt(from);
|
||
let beats = 0, t = from;
|
||
for (const p of pts) {
|
||
if (p.sec <= t) continue;
|
||
if (p.sec >= to) break;
|
||
beats += ((p.sec - t) * bpmAt(t)) / 60;
|
||
t = p.sec;
|
||
}
|
||
beats += ((to - t) * bpmAt(t)) / 60;
|
||
return (beats * 60) / (to - from);
|
||
}
|
||
|
||
/** Adds the MIDI tempo, the effective (warped) tempo = MIDI bpm / ratio, and the change in percent. */
|
||
export function stretchInfo(stretches: Stretch[], tempoMap: TempoPoint[]): StretchInfo[] {
|
||
return stretches.map((s) => {
|
||
const midiBpm = avgBpm(tempoMap, s.fromMidi, s.toMidi);
|
||
return { ...s, midiBpm, bpm: midiBpm / s.ratio, pct: (1 / s.ratio - 1) * 100 };
|
||
});
|
||
}
|
||
|
||
/** "78.0 → 79.1 bpm (+1.4%)", with " ⚠" when the stretch is warned; the percent is omitted below 0.05%. */
|
||
export function stretchLabel(s: StretchInfo): string {
|
||
const pct = Math.abs(s.pct) < 0.05 ? "" : ` (${s.pct > 0 ? "+" : "−"}${Math.abs(s.pct).toFixed(1)}%)`;
|
||
return `${s.midiBpm.toFixed(1)} → ${s.bpm.toFixed(1)} bpm${pct}${s.warn ? " ⚠" : ""}`;
|
||
}
|