@perry-rylance/midi-to-milliseconds
Version:
TypeScript library for resolving MIDI event times to milliseconds. Useful for syncing things like graphics to MIDI.
108 lines (77 loc) • 3.08 kB
text/typescript
import { File, SetTempoEvent, ResolutionUnits, Event, NoteOnEvent } from "@perry-rylance/midi";
import TimeResolvedTrack from "./TimeResolvedTrack";
import TimeResolvedEvent from "./TimeResolvedEvent";
import InjectedSetTempoEvent from "./InjectedSetTempoEvent";
export interface TimeResolverOptions
{
stable?: boolean;
}
export default class TimeResolver
{
private options?: TimeResolverOptions;
readonly tracks: TimeResolvedTrack[];
constructor(file: File, options?: TimeResolverOptions)
{
if(file.resolution.units !== ResolutionUnits.PPQ)
throw new Error("Only PPQ resolution is supported presently");
this.options = options;
// NB: Get absolute ticks for all events
this.tracks = file.tracks.map(track => new TimeResolvedTrack(track, options));
// NB: Get resolved tempo events from all tracks
const resolvedSetTempoEvents: TimeResolvedEvent[] = this.tracks.map(track => {
return track.events.filter(event => event.original instanceof SetTempoEvent) as TimeResolvedEvent[];
})
.flat();
// NB: Inject the tempo events into each track
for(const track of this.tracks)
{
this.injectResolvedSetTempoEvents(track, resolvedSetTempoEvents);
if(this.options?.stable)
this.indexEvents(track);
}
// NB: Walk the events for each track resolving time
const ppqn = file.resolution.ticksPerQuarterNote;
let trackIndex = 0;
for(const track of this.tracks)
{
let milliseconds = 0;
let bpm = 120;
let prev: TimeResolvedEvent | null = null;
for(const event of track.events)
{
// NB: Deltas won't work here because we've injected set tempos. Need to track our own delta from absolute times.
const delta = event.absolute.ticks - (prev ? prev.absolute.ticks : 0);
milliseconds += delta * 60000 / ppqn / bpm;
// NB: Round down to avoid floating-point comparison issues. Milliseconds is accurate enough for the purposes of this library.
event.absolute.milliseconds = Math.floor(milliseconds);
if(event.original instanceof SetTempoEvent)
bpm = event.original.bpm;
prev = event;
}
trackIndex++;
}
}
private injectResolvedSetTempoEvents(track: TimeResolvedTrack, events: TimeResolvedEvent[]): void
{
const cloned = events.map(event => {
const cloned = new InjectedSetTempoEvent(event.original.delta);
cloned.bpm = (event.original as SetTempoEvent).bpm;
return new TimeResolvedEvent(cloned, event.absolute);
});
// NB: Important to unshift these so that the set tempo events come first
track.events.unshift(...cloned);
// TODO: Could speed this up with an insertion sort or something more specific than JS's native sort
track.events.sort((a, b) => {
if(a.absolute.ticks === b.absolute.ticks)
return 0;
if(a.absolute.ticks > b.absolute.ticks)
return 1;
return -1;
});
}
private indexEvents(track: TimeResolvedTrack): void
{
for(let i = 0; i < track.events.length; i++)
track.events[i].index = i;
}
}