@stringsync/vexml
Version:
MusicXML to Vexflow
324 lines (323 loc) • 13.4 kB
JavaScript
import * as util from '../util';
import { DurationRange } from './durationrange';
export class CursorFrame {
tRangeSources;
xRangeSources;
yRangeSources;
activeElements;
describer;
constructor(tRangeSources, xRangeSources, yRangeSources, activeElements, describer) {
this.tRangeSources = tRangeSources;
this.xRangeSources = xRangeSources;
this.yRangeSources = yRangeSources;
this.activeElements = activeElements;
this.describer = describer;
}
static create(log, score, timeline, span) {
const partCount = score.getPartCount();
if (partCount === 0) {
log.warn('No parts found in score, returning empty cursor frames.');
return [];
}
if (0 > span.fromPartIndex || span.fromPartIndex >= partCount) {
throw new Error(`Invalid fromPartIndex: ${span.fromPartIndex}, must be in [0,${partCount - 1}]`);
}
if (0 > span.toPartIndex || span.toPartIndex >= partCount) {
throw new Error(`Invalid toPartIndex: ${span.toPartIndex}, must be in [0,${partCount - 1}]`);
}
const factory = new CursorFrameFactory(log, score, timeline, span);
return factory.create();
}
get tRange() {
const t1 = this.tRangeSources[0].moment.time;
const t2 = this.tRangeSources[1].moment.time;
return new DurationRange(t1, t2);
}
get xRange() {
const x1 = this.toXRangeBound(this.xRangeSources[0]);
const x2 = this.toXRangeBound(this.xRangeSources[1]);
return new util.NumberRange(x1, x2);
}
get yRange() {
const y1 = this.getYRangeBound(this.yRangeSources[0]);
const y2 = this.getYRangeBound(this.yRangeSources[1]);
return new util.NumberRange(y1, y2);
}
getHints(previousFrame) {
return [...this.getRetriggerHints(previousFrame), ...this.getSustainHints(previousFrame)];
}
getActiveElements() {
return [...this.activeElements];
}
toHumanReadable() {
const tRangeDescription = this.describer.describeTRange(this.tRangeSources);
const xRangeDescription = this.describer.describeXRange(this.xRangeSources);
const yRangeDescription = this.describer.describeYRange(this.yRangeSources);
return [`t: ${tRangeDescription}`, `x: ${xRangeDescription}`, `y: ${yRangeDescription}`];
}
toXRangeBound(source) {
const rect = this.getXRangeRect(source);
switch (source.type) {
case 'system':
return source.bound === 'left' ? rect.left() : rect.right();
case 'measure':
return source.bound === 'left' ? rect.left() : rect.right();
case 'element':
return source.bound === 'left' ? rect.left() : rect.right();
}
}
getXRangeRect(source) {
switch (source.type) {
case 'system':
return (source.system
.getMeasures()
.at(0)
?.getFragments()
.at(0)
?.getParts()
.at(0)
?.getStaves()
.at(0)
?.intrinsicRect() ?? source.system.rect());
case 'measure':
return (source.measure.getFragments().at(0)?.getParts().at(0)?.getStaves().at(0)?.intrinsicRect() ??
source.measure.rect());
case 'element':
return source.element.rect();
}
}
getYRangeBound(source) {
return source.bound === 'top' ? source.part.rect().top() : source.part.rect().bottom();
}
getRetriggerHints(previousFrame) {
const hints = new Array();
if (this === previousFrame) {
return hints;
}
const previousNotes = previousFrame.activeElements.filter((e) => e.name === 'note');
const currentNotes = this.activeElements.filter((e) => e.name === 'note');
// Let N be the number of notes in a frame. This algorithm is O(N^2) in the worst case, but we expect to N to be
// very small.
for (const currentNote of currentNotes) {
const previousNote = previousNotes.find((previousNote) => this.isPitchEqual(currentNote.getPitch(), previousNote.getPitch()));
if (previousNote && !previousNote.sharesACurveWith(currentNote)) {
hints.push({
type: 'retrigger',
untriggerElement: previousNote,
retriggerElement: currentNote,
});
}
}
return hints;
}
getSustainHints(previousFrame) {
const hints = new Array();
if (this === previousFrame) {
return hints;
}
const previousNotes = previousFrame.activeElements.filter((e) => e.name === 'note');
const currentNotes = this.activeElements.filter((e) => e.name === 'note');
// Let N be the number of notes in a frame. This algorithm is O(N^2) in the worst case, but we expect to N to be
// very small.
for (const currentNote of currentNotes) {
const previousNote = previousNotes.find((previousNote) => this.isPitchEqual(currentNote.getPitch(), previousNote.getPitch()));
if (previousNote && previousNote.sharesACurveWith(currentNote)) {
hints.push({
type: 'sustain',
previousElement: previousNote,
currentElement: currentNote,
});
}
}
return hints;
}
isPitchEqual(a, b) {
return a.step === b.step && a.octave === b.octave && a.accidentalCode === b.accidentalCode;
}
}
class CursorFrameFactory {
logger;
score;
timeline;
span;
frames = new Array();
activeElements = new Set();
describer;
constructor(logger, score, timeline, span) {
this.logger = logger;
this.score = score;
this.timeline = timeline;
this.span = span;
this.describer = CursorFrameDescriber.create(score, timeline.getPartIndex());
}
create() {
this.frames = [];
this.activeElements = new Set();
for (let index = 0; index < this.timeline.getMomentCount() - 1; index++) {
const currentMoment = this.timeline.getMoment(index);
const nextMoment = this.timeline.getMoment(index + 1);
util.assertNotNull(currentMoment);
util.assertNotNull(nextMoment);
const tRangeSources = this.getTRangeSources(currentMoment, nextMoment);
const xRangeSources = this.getXRangeSources(currentMoment, nextMoment);
const yRangeSources = this.getYRangeSources(currentMoment);
this.updateActiveElements(currentMoment);
this.addFrame(tRangeSources, xRangeSources, yRangeSources);
}
return this.frames;
}
getTRangeSources(currentMoment, nextMoment) {
return [{ moment: currentMoment }, { moment: nextMoment }];
}
getXRangeSources(currentMoment, nextMoment) {
return [this.getStartXSource(currentMoment), this.getEndXSource(nextMoment)];
}
getStartXSource(moment) {
const hasStartingTransition = moment.events.some((e) => e.type === 'transition' && e.kind === 'start');
if (hasStartingTransition) {
return this.getLeftmostStartingXRangeSource(moment);
}
this.logger.warn('No starting transition found for moment, ' +
'but the moment is trying to be used as a starting anchor. ' +
'How was the moment created?', { moment });
const event = moment.events.at(0);
util.assertDefined(event);
switch (event.type) {
case 'transition':
return { type: 'element', element: event.element, bound: 'left' };
case 'systemend':
return { type: 'system', system: event.system, bound: 'left' };
case 'jump':
return { type: 'measure', measure: event.measure, bound: 'left' };
}
}
getEndXSource(nextMoment) {
const shouldUseMeasureEndBoundary = nextMoment.events.some((e) => e.type === 'jump' || e.type === 'systemend');
if (shouldUseMeasureEndBoundary) {
const event = nextMoment.events.at(0);
util.assertDefined(event);
switch (event.type) {
case 'transition':
return { type: 'measure', measure: event.measure, bound: 'right' };
case 'systemend':
return { type: 'system', system: event.system, bound: 'right' };
case 'jump':
return { type: 'measure', measure: event.measure, bound: 'right' };
}
}
return this.getStartXSource(nextMoment);
}
getLeftmostStartingXRangeSource(currentMoment) {
const elements = currentMoment.events
.filter((e) => e.type === 'transition')
.filter((e) => e.kind === 'start')
.map((e) => e.element);
let min = Infinity;
let leftmost = undefined;
for (const element of elements) {
const left = element.rect().left();
if (left < min) {
min = left;
leftmost = element;
}
}
util.assertDefined(leftmost);
return { type: 'element', element: leftmost, bound: 'left' };
}
getYRangeSources(currentMoment) {
const systemIndex = this.getSystemIndex(currentMoment);
const parts = this.score
.getSystems()
.at(systemIndex)
.getMeasures()
.flatMap((measure) => measure.getFragments())
.flatMap((fragment) => fragment.getParts());
const topPart = parts.find((part) => part.getIndex() === this.span.fromPartIndex);
const bottomPart = parts.find((part) => part.getIndex() === this.span.toPartIndex);
util.assertDefined(topPart);
util.assertDefined(bottomPart);
return [
{ part: topPart, bound: 'top' },
{ part: bottomPart, bound: 'bottom' },
];
}
getSystemIndex(currentMoment) {
const events = currentMoment.events.toSorted((a, b) => {
const kindOrder = { start: 0, stop: 1 };
if (a.type === 'transition' && b.type === 'transition') {
return kindOrder[a.kind] - kindOrder[b.kind];
}
const typeOrder = { transition: 0, systemend: 1, jump: 2 };
return typeOrder[a.type] - typeOrder[b.type];
});
for (const event of events) {
switch (event.type) {
case 'transition':
return event.measure.getSystemIndex();
case 'systemend':
return event.system.getIndex();
case 'jump':
return event.measure.getSystemIndex();
}
}
util.assertUnreachable();
}
updateActiveElements(moment) {
for (const event of moment.events) {
if (event.type === 'transition') {
if (event.kind === 'start') {
this.activeElements.add(event.element);
}
else if (event.kind === 'stop') {
this.activeElements.delete(event.element);
}
}
}
}
addFrame(tRangeSources, xRangeSources, yRangeSources) {
const frame = new CursorFrame(tRangeSources, xRangeSources, yRangeSources, [...this.activeElements], this.describer);
this.frames.push(frame);
}
}
class CursorFrameDescriber {
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 CursorFrameDescriber(elements);
}
describeTRange(tRangeSources) {
return `[${tRangeSources[0].moment.time.ms}ms - ${tRangeSources[1].moment.time.ms}ms]`;
}
describeXRange(xRangeSources) {
return `[${this.describeXRangeSource(xRangeSources[0])} - ${this.describeXRangeSource(xRangeSources[1])}]`;
}
describeYRange(yRangeSources) {
return `[${this.describeYRangeSource(yRangeSources[0])} - ${this.describeYRangeSource(yRangeSources[1])}]`;
}
describeXRangeSource(source) {
switch (source.type) {
case 'system':
return `${source.bound}(system(${source.system.getIndex()}))`;
case 'measure':
return `${source.bound}(measure(${source.measure.getAbsoluteMeasureIndex()}))`;
case 'element':
return `${source.bound}(element(${this.elements.get(source.element)}))`;
}
}
describeYRangeSource(source) {
return `${source.bound}(system(${source.part.getSystemIndex()}), part(${source.part.getIndex()}))`;
}
}