import type { TempoEvent, TimeSigEvent } from '../types.ts'; // TempoMap converts between ticks and seconds under a piecewise-constant // tempo, and enumerates beat/bar grid lines under the time signature map. interface Segment { tick: number; sec: number; secPerTick: number; } export class TempoMap { private segs: Segment[]; constructor(readonly ppq: number, tempo: TempoEvent[], readonly timeSig: TimeSigEvent[] = []) { const evs = [...tempo].sort((a, b) => a.tick - b.tick); if (evs.length === 0 || evs[0].tick !== 0) evs.unshift({ tick: 0, usPerQuarter: 500000 }); this.segs = []; let sec = 0; for (let i = 0; i < evs.length; i++) { const e = evs[i]; if (i > 0) { const prev = this.segs[this.segs.length - 1]; sec = prev.sec + (e.tick - prev.tick) * prev.secPerTick; } const secPerTick = e.usPerQuarter / 1e6 / ppq; // A later event at the same tick replaces the earlier one. if (this.segs.length > 0 && this.segs[this.segs.length - 1].tick === e.tick) { this.segs[this.segs.length - 1] = { tick: e.tick, sec, secPerTick }; } else { this.segs.push({ tick: e.tick, sec, secPerTick }); } } if (this.timeSig.length === 0 || this.timeSig[0].tick !== 0) { this.timeSig = [{ tick: 0, numerator: 4, denominator: 4 }, ...this.timeSig]; } this.timeSig = [...this.timeSig].sort((a, b) => a.tick - b.tick); } private segAtTick(tick: number): Segment { let lo = 0; let hi = this.segs.length - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (this.segs[mid].tick <= tick) lo = mid; else hi = mid - 1; } return this.segs[lo]; } private segAtSec(sec: number): Segment { let lo = 0; let hi = this.segs.length - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (this.segs[mid].sec <= sec) lo = mid; else hi = mid - 1; } return this.segs[lo]; } tickToSec(tick: number): number { const s = this.segAtTick(Math.max(0, tick)); return s.sec + (tick - s.tick) * s.secPerTick; } secToTick(sec: number): number { const s = this.segAtSec(Math.max(0, sec)); return s.tick + (sec - s.sec) / s.secPerTick; } bpmAt(tick: number): number { const s = this.segAtTick(Math.max(0, tick)); return 60 / (s.secPerTick * this.ppq); } // Bar/beat position for display, 1-based. barBeatAt(tick: number): { bar: number; beat: number } { let bar = 0; let t = 0; for (let i = 0; i < this.timeSig.length; i++) { const sig = this.timeSig[i]; const next = i + 1 < this.timeSig.length ? this.timeSig[i + 1].tick : Infinity; const beatTicks = (this.ppq * 4) / sig.denominator; const barTicks = beatTicks * sig.numerator; if (tick < next) { const rel = tick - sig.tick; const barsIn = Math.floor(rel / barTicks); const beatIn = Math.floor((rel - barsIn * barTicks) / beatTicks); return { bar: bar + barsIn + 1, beat: beatIn + 1 }; } bar += Math.round((next - sig.tick) / barTicks); t = next; } void t; return { bar: 1, beat: 1 }; } // Beat grid lines in [fromTick, toTick), marking bar starts. gridLines(fromTick: number, toTick: number): { tick: number; isBar: boolean }[] { const out: { tick: number; isBar: boolean }[] = []; for (let i = 0; i < this.timeSig.length; i++) { const sig = this.timeSig[i]; const next = i + 1 < this.timeSig.length ? this.timeSig[i + 1].tick : toTick; if (next <= fromTick || sig.tick >= toTick) continue; const beatTicks = (this.ppq * 4) / sig.denominator; const start = Math.max(sig.tick, fromTick); let k = Math.floor((start - sig.tick) / beatTicks); for (;;) { const tick = sig.tick + k * beatTicks; if (tick >= Math.min(next, toTick)) break; if (tick >= fromTick) out.push({ tick, isBar: k % sig.numerator === 0 }); k++; if (out.length > 20000) return out; // safety } } return out; } }