UNPKG

@stringsync/vexml

Version:

MusicXML to Vexflow

255 lines (254 loc) 9.18 kB
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'; } }