@stringsync/vexml
Version:
MusicXML to Vexflow
255 lines (254 loc) • 9.18 kB
JavaScript
import { Duration } from './duration';
import { MeasureSequenceIterator } from './measuresequenceiterator';
import * as util from '../util';
export class Timeline {
partIndex;
moments;
describer;
constructor(partIndex, moments, describer) {
this.partIndex = partIndex;
this.moments = moments;
this.describer = describer;
}
static create(log, score) {
const partCount = score.getPartCount();
const timelines = new Array(partCount);
for (let partIndex = 0; partIndex < partCount; partIndex++) {
timelines[partIndex] = new TimelineFactory(log, score, partIndex).create();
}
return timelines;
}
getPartIndex() {
return this.partIndex;
}
getMoment(index) {
return this.moments.at(index) ?? null;
}
getMoments() {
return this.moments;
}
getMomentCount() {
return this.moments.length;
}
getDuration() {
return this.moments.at(-1)?.time ?? Duration.zero();
}
toHumanReadable() {
return this.describer.describe(this.moments);
}
}
class TimelineFactory {
logger;
score;
partIndex;
// timeMs -> moment
moments = new Map();
currentMeasureStartTime = Duration.zero();
nextMeasureStartTime = Duration.zero();
constructor(logger, score, partIndex) {
this.logger = logger;
this.score = score;
this.partIndex = partIndex;
}
create() {
this.moments = new Map();
this.currentMeasureStartTime = Duration.zero();
this.populateMoments();
this.sortEventsWithinMoments();
const moments = this.getSortedMoments();
const describer = TimelineDescriber.create(this.score, this.partIndex);
return new Timeline(this.partIndex, moments, describer);
}
getMeasuresInPlaybackOrder() {
const measures = this.score.getMeasures();
const measureIndexes = Array.from(new MeasureSequenceIterator(measures.map((measure, index) => ({ index, jumps: measure.getJumps() }))));
const result = new Array();
for (let i = 0; i < measureIndexes.length; i++) {
const current = measureIndexes[i];
const next = measureIndexes.at(i + 1);
const willJump = typeof next === 'number' && next !== current + 1;
const measure = measures[current];
result.push({ measure, willJump });
}
return result;
}
proposeNextMeasureStartTime(time) {
this.nextMeasureStartTime = Duration.max(this.nextMeasureStartTime, time);
}
toDuration(beat, bpm) {
const duration = Duration.minutes(beat.divide(new util.Fraction(bpm)).toDecimal());
// Round to the nearest 100ms. This is needed to correctly group transitions that should belong together.
const ms = Math.round(duration.ms / 100) * 100;
return Duration.ms(ms);
}
populateMoments() {
for (const { measure, willJump } of this.getMeasuresInPlaybackOrder()) {
if (measure.isMultiMeasure()) {
this.populateMultiMeasureEvents(measure);
}
else {
this.populateFragmentEvents(measure);
}
this.currentMeasureStartTime = this.nextMeasureStartTime;
if (willJump) {
this.addJumpEvent(this.currentMeasureStartTime, measure);
}
else if (measure.isLastMeasureInSystem()) {
const system = this.score.getSystems().at(measure.getSystemIndex());
util.assertDefined(system);
this.addSystemEndEvent(this.currentMeasureStartTime, system);
}
}
}
populateMultiMeasureEvents(measure) {
util.assert(measure.isMultiMeasure(), 'measure must be a multi-measure');
const bpm = measure.getBpm();
const duration = this.toDuration(measure.getBeatCount(), bpm);
const startTime = this.currentMeasureStartTime;
const stopTime = startTime.add(duration);
this.addTransitionStartEvent(startTime, measure, measure);
this.addTransitionStopEvent(stopTime, measure, measure);
this.proposeNextMeasureStartTime(stopTime);
}
populateFragmentEvents(measure) {
for (const fragment of measure.getFragments()) {
if (fragment.isNonMusicalGap()) {
this.populateNonMusicalGapEvents(fragment, measure);
}
else {
this.populateVoiceEntryEvents(fragment, measure);
}
}
}
populateNonMusicalGapEvents(fragment, measure) {
const duration = Duration.ms(fragment.getNonMusicalDurationMs());
const startTime = this.currentMeasureStartTime;
const stopTime = startTime.add(duration);
this.addTransitionStartEvent(startTime, measure, fragment);
this.addTransitionStopEvent(stopTime, measure, fragment);
this.proposeNextMeasureStartTime(stopTime);
}
populateVoiceEntryEvents(fragment, measure) {
const voiceEntries = fragment
.getParts()
.filter((part) => part.getIndex() === this.partIndex)
.flatMap((fragmentPart) => fragmentPart.getStaves())
.flatMap((stave) => stave.getVoices())
.flatMap((voice) => voice.getEntries());
const bpm = fragment.getBpm();
for (const voiceEntry of voiceEntries) {
const duration = this.toDuration(voiceEntry.getBeatCount(), bpm);
// NOTE: getStartMeasureBeat() is relative to the start of the measure.
const startTime = this.currentMeasureStartTime.add(this.toDuration(voiceEntry.getStartMeasureBeat(), bpm));
const stopTime = startTime.add(duration);
this.addTransitionStartEvent(startTime, measure, voiceEntry);
this.addTransitionStopEvent(stopTime, measure, voiceEntry);
this.proposeNextMeasureStartTime(stopTime);
}
}
sortEventsWithinMoments() {
for (const moment of this.moments.values()) {
moment.events.sort((a, b) => {
return this.getEventTypeOrder(a) - this.getEventTypeOrder(b);
});
}
}
getEventTypeOrder(event) {
if (event.type === 'transition' && event.kind === 'stop') {
return 0;
}
if (event.type === 'jump') {
return 1;
}
if (event.type === 'systemend') {
return 2;
}
if (event.type === 'transition' && event.kind === 'start') {
return 3;
}
util.assertUnreachable();
}
upsert(time, event) {
let moment;
if (this.moments.has(time.ms)) {
moment = this.moments.get(time.ms);
moment.events.push(event);
}
else {
moment = { time, events: [event] };
this.moments.set(time.ms, moment);
}
return moment;
}
addTransitionStartEvent(time, measure, element) {
this.upsert(time, {
type: 'transition',
kind: 'start',
measure,
element,
});
}
addTransitionStopEvent(time, measure, element) {
this.upsert(time, {
type: 'transition',
kind: 'stop',
measure,
element,
});
}
addJumpEvent(time, measure) {
this.upsert(time, { type: 'jump', measure });
}
addSystemEndEvent(time, system) {
this.upsert(time, { type: 'systemend', system });
}
getSortedMoments() {
const moments = Array.from(this.moments.values());
return moments.sort((a, b) => a.time.compare(b.time));
}
}
class TimelineDescriber {
elements;
constructor(elements) {
this.elements = elements;
}
static create(score, partIndex) {
const elements = new Map();
score
.getMeasures()
.flatMap((measure) => measure.getFragments())
.flatMap((fragment) => fragment.getParts().at(partIndex) ?? [])
.flatMap((part) => part.getStaves())
.flatMap((stave) => stave.getVoices())
.flatMap((voice) => voice.getEntries())
.forEach((element, index) => {
elements.set(element, index);
});
return new TimelineDescriber(elements);
}
describe(moments) {
return moments.map((moment) => this.describeMoment(moment));
}
describeMoment(moment) {
return `[${moment.time.ms}ms] ${moment.events.map((event) => this.describeEvent(event)).join(', ')}`;
}
describeEvent(event) {
switch (event.type) {
case 'transition':
return this.describeTransition(event);
case 'jump':
return this.describeJump();
case 'systemend':
return this.describeSystemEnd();
}
}
describeTransition(event) {
return `${event.kind}(${this.elements.get(event.element)})`;
}
describeJump() {
return 'jump';
}
describeSystemEnd() {
return 'systemend';
}
}