Files
STSheets/src/align/tempo.ts
T
Lucas WintherandClaude Opus 5.5 894fd9b9ef feat(align): alignment web page
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>
2026-10-30 14:38:36 +01:00

36 lines
1.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 ? " ⚠" : ""}`;
}