UNPKG

@stringsync/vexml

Version:

MusicXML to Vexflow

324 lines (323 loc) 13.4 kB
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()}))`; } }