@stringsync/vexml
Version:
MusicXML to Vexflow
292 lines (291 loc) • 10.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Timeline = void 0;
const duration_1 = require("./duration");
const measuresequenceiterator_1 = require("./measuresequenceiterator");
const util = __importStar(require("../util"));
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_1.Duration.zero();
}
toHumanReadable() {
return this.describer.describe(this.moments);
}
}
exports.Timeline = Timeline;
class TimelineFactory {
logger;
score;
partIndex;
// timeMs -> moment
moments = new Map();
currentMeasureStartTime = duration_1.Duration.zero();
nextMeasureStartTime = duration_1.Duration.zero();
constructor(logger, score, partIndex) {
this.logger = logger;
this.score = score;
this.partIndex = partIndex;
}
create() {
this.moments = new Map();
this.currentMeasureStartTime = duration_1.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_1.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_1.Duration.max(this.nextMeasureStartTime, time);
}
toDuration(beat, bpm) {
const duration = duration_1.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_1.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_1.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';
}
}