diff --git a/src/playback/cursorframe.ts b/src/playback/cursorframe.ts index 0dc132296..93255cf27 100644 --- a/src/playback/cursorframe.ts +++ b/src/playback/cursorframe.ts @@ -61,14 +61,14 @@ export class CursorFrame { } get xRange(): util.NumberRange { - const x1 = this.toXRangeBound(this.xRangeSources[0]); - const x2 = this.toXRangeBound(this.xRangeSources[1]); + const x1 = getXRangeBound(this.xRangeSources[0]); + const x2 = getXRangeBound(this.xRangeSources[1]); return new util.NumberRange(x1, x2); } get yRange(): util.NumberRange { - const y1 = this.getYRangeBound(this.yRangeSources[0]); - const y2 = this.getYRangeBound(this.yRangeSources[1]); + const y1 = getYRangeBound(this.yRangeSources[0]); + const y2 = getYRangeBound(this.yRangeSources[1]); return new util.NumberRange(y1, y2); } @@ -88,47 +88,6 @@ export class CursorFrame { return [`t: ${tRangeDescription}`, `x: ${xRangeDescription}`, `y: ${yRangeDescription}`]; } - private toXRangeBound(source: XRangeSource): number { - 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(); - } - } - - private getXRangeRect(source: XRangeSource) { - 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(); - } - } - - private getYRangeBound(source: YRangeSource): number { - return source.bound === 'top' ? source.part.rect().top() : source.part.rect().bottom(); - } - private getRetriggerHints(previousFrame: CursorFrame): RetriggerHint[] { const hints = new Array(); if (this === previousFrame) { @@ -194,7 +153,7 @@ class CursorFrameFactory { private describer: CursorFrameDescriber; constructor( - private logger: Logger, + private log: Logger, private score: elements.Score, private timeline: Timeline, private span: CursorVerticalSpan @@ -229,20 +188,23 @@ class CursorFrameFactory { } private getXRangeSources(currentMoment: TimelineMoment, nextMoment: TimelineMoment): [XRangeSource, XRangeSource] { - return [this.getStartXSource(currentMoment), this.getEndXSource(nextMoment)]; + const startXRangeSource = this.getStartXRangeSource(currentMoment); + const endXRangeSource = this.getEndXRangeSource(startXRangeSource, nextMoment); + + return [startXRangeSource, endXRangeSource]; } - private getStartXSource(moment: TimelineMoment): XRangeSource { + private getStartXRangeSource(moment: TimelineMoment): XRangeSource { const hasStartingTransition = moment.events.some((e) => e.type === 'transition' && e.kind === 'start'); if (hasStartingTransition) { return this.getLeftmostStartingXRangeSource(moment); } - this.logger.warn( + this.log.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 } + { momentTimeMs: moment.time.ms } ); const event = moment.events.at(0); @@ -258,7 +220,7 @@ class CursorFrameFactory { } } - private getEndXSource(nextMoment: TimelineMoment): XRangeSource { + private getEndXRangeSource(startXRangeSource: XRangeSource, nextMoment: TimelineMoment): XRangeSource { const shouldUseMeasureEndBoundary = nextMoment.events.some((e) => e.type === 'jump' || e.type === 'systemend'); if (shouldUseMeasureEndBoundary) { const event = nextMoment.events.at(0); @@ -274,7 +236,22 @@ class CursorFrameFactory { } } - return this.getStartXSource(nextMoment); + const proposedXRangeSource = this.getStartXRangeSource(nextMoment); + const startBound = getXRangeBound(startXRangeSource); + const proposedBound = getXRangeBound(proposedXRangeSource); + + // Ensure that the proposed X range source is to the right of the start X range source. If it's not, we'll fall back + // to the start X range source's right bound (since we know the start X range source is based on the left bound). + if (proposedBound >= startBound) { + return proposedXRangeSource; + } else { + this.log.warn( + 'Proposed end X range source is to the left of the start X range source. ' + + "Falling back to the start X range source's right bound.", + { momentTimeMs: nextMoment.time.ms } + ); + return { ...startXRangeSource, bound: 'right' }; + } } private getLeftmostStartingXRangeSource(currentMoment: TimelineMoment): XRangeSource { @@ -414,3 +391,36 @@ class CursorFrameDescriber { return `${source.bound}(system(${source.part.getSystemIndex()}), part(${source.part.getIndex()}))`; } } + +function getXRangeBound(source: XRangeSource): number { + const rect = 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(); + } +} + +function getXRangeRect(source: XRangeSource) { + 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(); + } +} + +function getYRangeBound(source: YRangeSource): number { + return source.bound === 'top' ? source.part.rect().top() : source.part.rect().bottom(); +} diff --git a/src/playback/timeline.ts b/src/playback/timeline.ts index aa8fcf97d..6b131c644 100644 --- a/src/playback/timeline.ts +++ b/src/playback/timeline.ts @@ -89,8 +89,8 @@ class TimelineFactory { private toDuration(beat: util.Fraction, bpm: number): Duration { 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; + // Round to the nearest ms. This is needed to correctly group transitions that should belong together. + const ms = Math.round(duration.ms); return Duration.ms(ms); } @@ -133,7 +133,7 @@ class TimelineFactory { if (fragment.isNonMusicalGap()) { this.populateNonMusicalGapEvents(fragment, measure); } else { - this.populateVoiceEntryEvents(fragment, measure); + this.populateVoiceEvents(fragment, measure); } } } @@ -149,17 +149,26 @@ class TimelineFactory { this.proposeNextMeasureStartTime(stopTime); } - private populateVoiceEntryEvents(fragment: elements.Fragment, measure: elements.Measure): void { - const voiceEntries = fragment + private populateVoiceEvents(fragment: elements.Fragment, measure: elements.Measure): void { + const voices = fragment .getParts() .filter((part) => part.getIndex() === this.partIndex) .flatMap((fragmentPart) => fragmentPart.getStaves()) - .flatMap((stave) => stave.getVoices()) - .flatMap((voice) => voice.getEntries()); + .flatMap((stave) => stave.getVoices()); + + for (const voice of voices) { + this.populateVoiceEntryEvents(voice, fragment, measure); + } + } + private populateVoiceEntryEvents( + voice: elements.Voice, + fragment: elements.Fragment, + measure: elements.Measure + ): void { const bpm = fragment.getBpm(); - for (const voiceEntry of voiceEntries) { + for (const voiceEntry of voice.getEntries()) { 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)); diff --git a/tests/__data__/vexml/playback_backwards_formatting.musicxml b/tests/__data__/vexml/playback_backwards_formatting.musicxml new file mode 100644 index 000000000..40cb9486f --- /dev/null +++ b/tests/__data__/vexml/playback_backwards_formatting.musicxml @@ -0,0 +1,414 @@ + + + + + + MuseScore 2.0.2 + 2015-08-06 + + + + + + + http://musescore.com/score/210606 + + + + 7.05556 + 40 + + + 1683.36 + 1190.88 + + 56.6929 + 57.0217 + 56.6929 + 113.386 + + + 56.6929 + 57.0217 + 56.6929 + 113.386 + + + + + + + Prélude No. 1 in C Major + + + from “Das Wohltemperierte Klavier” Book I + + BWV 846 + + + Johann Sebastian Bach + + (1685 - 1750) + + + + Piano + + Piano + + + + 1 + 1 + 78.7402 + 0 + + + + + + + + + 86.83 + -0.00 + + 240.00 + + + 70.00 + + + + 4 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + 80 + + 1 + + + + + 2 + 1 + eighth + 1 + + + + G + 4 + + 1 + 1 + 16th + up + 1 + begin + begin + + + + C + 5 + + 1 + 1 + 16th + up + 1 + end + end + + + + E + 5 + + 1 + 1 + 16th + up + 1 + begin + begin + + + + G + 4 + + 1 + 1 + 16th + up + 1 + continue + continue + + + + C + 5 + + 1 + 1 + 16th + up + 1 + continue + continue + + + + E + 5 + + 1 + 1 + 16th + up + 1 + end + end + + + + 2 + 1 + eighth + 1 + + + + G + 4 + + 1 + 1 + 16th + up + 1 + begin + begin + + + + C + 5 + + 1 + 1 + 16th + up + 1 + end + end + + + + E + 5 + + 1 + 1 + 16th + up + 1 + begin + begin + + + + G + 4 + + 1 + 1 + 16th + up + 1 + continue + continue + + + + C + 5 + + 1 + 1 + 16th + up + 1 + continue + continue + + + + E + 5 + + 1 + 1 + 16th + up + 1 + end + end + + + 16 + + + + + + 2 + + + + C + 4 + + 1 + 5 + 16th + 2 + + + + E + 4 + + 3 + + 5 + eighth + + up + 2 + + + + + + + E + 4 + + 4 + + 5 + quarter + up + 2 + + + + + + + + + 2 + + + + + + 2 + + + + C + 4 + + 1 + 5 + 16th + 2 + + + + E + 4 + + 3 + + 5 + eighth + + up + 2 + + + + + + + E + 4 + + 4 + + 5 + quarter + up + 2 + + + + + + + + + 2 + + + 16 + + + + C + 4 + + 8 + 6 + half + down + 2 + + + + C + 4 + + 8 + 6 + half + down + 2 + + + + diff --git a/tests/__data__/vexml/playback_multi_system.musicxml b/tests/__data__/vexml/playback_multi_system.musicxml index 172ffc537..f34ff6a09 100644 --- a/tests/__data__/vexml/playback_multi_system.musicxml +++ b/tests/__data__/vexml/playback_multi_system.musicxml @@ -70,85 +70,5 @@ whole - - - - G - 4 - - 4 - 1 - whole - - - - - - A - 4 - - 4 - 1 - whole - - - - - - B - 4 - - 4 - 1 - whole - - - - - - C - 5 - - 4 - 1 - whole - - - - - - D - 5 - - 4 - 1 - whole - - - - - - E - 5 - - 4 - 1 - whole - - - - - - F - 5 - - 4 - 1 - whole - - - light-heavy - - diff --git a/tests/unit/playback/cursorframe.test.ts b/tests/unit/playback/cursorframe.test.ts index b8d1663c3..6adaa7c27 100644 --- a/tests/unit/playback/cursorframe.test.ts +++ b/tests/unit/playback/cursorframe.test.ts @@ -7,18 +7,18 @@ import fs from 'fs'; const DATA_DIR = path.resolve(__dirname, '..', '..', '__data__', 'vexml'); describe(CursorFrame, () => { - let logger: MemoryLogger; + let log: MemoryLogger; beforeEach(() => { - logger = new MemoryLogger(); + log = new MemoryLogger(); }); it('creates for: single measure, single stave, different notes', () => { const [score, timelines] = render('playback_simple.musicxml'); - const frames = CursorFrame.create(logger, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); + const frames = CursorFrame.create(log, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); - expect(logger.getLogs()).toBeEmpty(); + expect(log.getLogs()).toBeEmpty(); expect(timelines).toHaveLength(1); expect(frames).toHaveLength(4); // stave0: 0 1 2 3 @@ -47,9 +47,9 @@ describe(CursorFrame, () => { it('creates for: single measure, single stave, same notes', () => { const [score, timelines] = render('playback_same_note.musicxml'); - const frames = CursorFrame.create(logger, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); + const frames = CursorFrame.create(log, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); - expect(logger.getLogs()).toBeEmpty(); + expect(log.getLogs()).toBeEmpty(); expect(timelines).toHaveLength(1); expect(frames).toHaveLength(4); // stave0: 0 1 2 3 @@ -78,9 +78,9 @@ describe(CursorFrame, () => { it('creates for: single measure, multiple staves, different notes', () => { const [score, timelines] = render('playback_multi_stave.musicxml'); - const frames = CursorFrame.create(logger, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); + const frames = CursorFrame.create(log, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); - expect(logger.getLogs()).toBeEmpty(); + expect(log.getLogs()).toBeEmpty(); expect(timelines).toHaveLength(1); expect(frames).toHaveLength(8); // stave0: 0 1 2 3 @@ -134,10 +134,10 @@ describe(CursorFrame, () => { const span0 = { fromPartIndex: 0, toPartIndex: 0 }; const span1 = { fromPartIndex: 0, toPartIndex: 1 }; - const framesPart0 = CursorFrame.create(logger, score, timelines[0], span0); - const framesPart1 = CursorFrame.create(logger, score, timelines[1], span1); + const framesPart0 = CursorFrame.create(log, score, timelines[0], span0); + const framesPart1 = CursorFrame.create(log, score, timelines[1], span1); - expect(logger.getLogs()).toBeEmpty(); + expect(log.getLogs()).toBeEmpty(); expect(timelines).toHaveLength(2); expect(framesPart0).toHaveLength(4); // part0, stave0: 0 1 2 3 @@ -190,9 +190,9 @@ describe(CursorFrame, () => { it('creates for: multiple measures, single stave, different notes', () => { const [score, timelines] = render('playback_multi_measure.musicxml'); - const frames = CursorFrame.create(logger, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); + const frames = CursorFrame.create(log, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); - expect(logger.getLogs()).toBeEmpty(); + expect(log.getLogs()).toBeEmpty(); expect(timelines).toHaveLength(1); expect(frames).toHaveLength(8); // stave0: 0 1 2 3 4 | 5 6 7 8 @@ -241,9 +241,9 @@ describe(CursorFrame, () => { it('creates for: single measure, single stave, repeat', () => { const [score, timelines] = render('playback_repeat.musicxml'); - const frames = CursorFrame.create(logger, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); + const frames = CursorFrame.create(log, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); - expect(logger.getLogs()).toBeEmpty(); + expect(log.getLogs()).toBeEmpty(); expect(timelines).toHaveLength(1); expect(frames).toHaveLength(8); // stave0: 0 1 2 3 :|| @@ -292,9 +292,9 @@ describe(CursorFrame, () => { it('creates for: multiple measures, single stave, repeat with endings', () => { const [score, timelines] = render('playback_repeat_endings.musicxml'); - const frames = CursorFrame.create(logger, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); + const frames = CursorFrame.create(log, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); - expect(logger.getLogs()).toBeEmpty(); + expect(log.getLogs()).toBeEmpty(); expect(timelines).toHaveLength(1); expect(frames).toHaveLength(6); // stave0: 0 | [ending1 -> 1] :|| [ending2 -> 2] :|| [ending3 -> 3] @@ -331,70 +331,128 @@ describe(CursorFrame, () => { }); it('creates for: multiple measures, single stave, multiple systems', () => { - const [score, timelines] = render('playback_multi_system.musicxml'); + const [score, timelines] = render('playback_multi_system.musicxml', 100); - const frames = CursorFrame.create(logger, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); + const frames = CursorFrame.create(log, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); - expect(logger.getLogs()).toBeEmpty(); + expect(log.getLogs()).toBeEmpty(); expect(timelines).toHaveLength(1); - // system0, stave0: 0 | 1 | 2 | 3 | 4 | 5 - // system1, stave0: 6 | 7 | 8 - expect(frames).toHaveLength(9); + // system0, stave0: 0 + // system1, stave0: 1 + expect(frames).toHaveLength(2); expect(frames[0].toHumanReadable()).toEqual([ 't: [0ms - 2400ms]', - 'x: [left(element(0)) - left(element(1))]', + 'x: [left(element(0)) - right(measure(0))]', 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', ]); expect(frames[1].toHumanReadable()).toEqual([ 't: [2400ms - 4800ms]', - 'x: [left(element(1)) - left(element(2))]', + 'x: [left(element(1)) - right(measure(1))]', + 'y: [top(system(1), part(0)) - bottom(system(1), part(0))]', + ]); + }); + + it('creates for: documents that have backwards formatting', () => { + const [score, timelines] = render('playback_backwards_formatting.musicxml'); + + const frames = CursorFrame.create(log, score, timelines[0], { fromPartIndex: 0, toPartIndex: 0 }); + + expect(timelines).toHaveLength(1); + expect(log.getLogs()).toBeEmpty(); + // stave0, voice0: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + // stave1, voice0: 14 15 16 17 18 19 + // stave2, voice1: 20 21 + expect(frames).toHaveLength(16); + expect(frames[0].toHumanReadable()).toEqual([ + 't: [0ms - 150ms]', + 'x: [left(element(0)) - left(element(15))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', + ]); + expect(frames[1].toHumanReadable()).toEqual([ + 't: [150ms - 300ms]', + 'x: [left(element(15)) - left(element(1))]', 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', ]); expect(frames[2].toHumanReadable()).toEqual([ - 't: [4800ms - 7200ms]', - 'x: [left(element(2)) - left(element(3))]', + 't: [300ms - 450ms]', + 'x: [left(element(1)) - left(element(2))]', 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', ]); expect(frames[3].toHumanReadable()).toEqual([ - 't: [7200ms - 9600ms]', - 'x: [left(element(3)) - left(element(4))]', + 't: [450ms - 600ms]', + 'x: [left(element(2)) - left(element(3))]', 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', ]); expect(frames[4].toHumanReadable()).toEqual([ - 't: [9600ms - 12000ms]', - 'x: [left(element(4)) - left(element(5))]', + 't: [600ms - 750ms]', + 'x: [left(element(3)) - left(element(4))]', 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', ]); expect(frames[5].toHumanReadable()).toEqual([ - 't: [12000ms - 14400ms]', - 'x: [left(element(5)) - right(measure(5))]', + 't: [750ms - 900ms]', + 'x: [left(element(4)) - left(element(5))]', 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', ]); expect(frames[6].toHumanReadable()).toEqual([ - 't: [14400ms - 16800ms]', - 'x: [left(element(6)) - left(element(7))]', - 'y: [top(system(1), part(0)) - bottom(system(1), part(0))]', + 't: [900ms - 1050ms]', + 'x: [left(element(5)) - left(element(6))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', ]); expect(frames[7].toHumanReadable()).toEqual([ - 't: [16800ms - 19200ms]', - 'x: [left(element(7)) - left(element(8))]', - 'y: [top(system(1), part(0)) - bottom(system(1), part(0))]', + 't: [1050ms - 1200ms]', + 'x: [left(element(6)) - left(element(7))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', ]); expect(frames[8].toHumanReadable()).toEqual([ - 't: [19200ms - 21600ms]', - 'x: [left(element(8)) - right(measure(8))]', - 'y: [top(system(1), part(0)) - bottom(system(1), part(0))]', + 't: [1200ms - 1350ms]', + 'x: [left(element(7)) - left(element(18))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', + ]); + expect(frames[9].toHumanReadable()).toEqual([ + 't: [1350ms - 1500ms]', + 'x: [left(element(18)) - left(element(8))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', + ]); + expect(frames[10].toHumanReadable()).toEqual([ + 't: [1500ms - 1650ms]', + 'x: [left(element(8)) - left(element(9))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', + ]); + expect(frames[11].toHumanReadable()).toEqual([ + 't: [1650ms - 1800ms]', + 'x: [left(element(9)) - left(element(10))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', + ]); + expect(frames[12].toHumanReadable()).toEqual([ + 't: [1800ms - 1950ms]', + 'x: [left(element(10)) - left(element(11))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', + ]); + expect(frames[13].toHumanReadable()).toEqual([ + 't: [1950ms - 2100ms]', + 'x: [left(element(11)) - left(element(12))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', + ]); + expect(frames[14].toHumanReadable()).toEqual([ + 't: [2100ms - 2250ms]', + 'x: [left(element(12)) - left(element(13))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', + ]); + expect(frames[15].toHumanReadable()).toEqual([ + 't: [2250ms - 2400ms]', + 'x: [left(element(13)) - right(measure(0))]', + 'y: [top(system(0), part(0)) - bottom(system(0), part(0))]', ]); }); }); -function render(filename: string): [vexml.Score, Timeline[]] { +function render(filename: string, width = 900): [vexml.Score, Timeline[]] { const musicXMLPath = path.resolve(DATA_DIR, filename); const musicXML = fs.readFileSync(musicXMLPath).toString(); const div = document.createElement('div'); const score = vexml.renderMusicXML(musicXML, div, { config: { - WIDTH: 900, + WIDTH: width, }, }); const timelines = Timeline.create(new NoopLogger(), score); diff --git a/tests/unit/playback/timeline.test.ts b/tests/unit/playback/timeline.test.ts index 5dc72e893..5219ee8b3 100644 --- a/tests/unit/playback/timeline.test.ts +++ b/tests/unit/playback/timeline.test.ts @@ -146,35 +146,58 @@ describe(Timeline, () => { }); it('creates for: multiple measures, single stave, multiple systems', () => { - const score = render('playback_multi_system.musicxml'); + const score = render('playback_multi_system.musicxml', 100); const timelines = Timeline.create(logger, score); expect(timelines).toHaveLength(1); expect(timelines[0].toHumanReadable()).toEqual([ - // system0, stave0: 0 | 1 | 2 | 3 | 4 | 5 - // system1, stave0: 6 | 7 | 8 + // system0, stave0: 0 + // system1, stave0: 1 '[0ms] start(0)', - '[2400ms] stop(0), start(1)', - '[4800ms] stop(1), start(2)', - '[7200ms] stop(2), start(3)', - '[9600ms] stop(3), start(4)', - '[12000ms] stop(4), start(5)', - '[14400ms] stop(5), systemend, start(6)', - '[16800ms] stop(6), start(7)', - '[19200ms] stop(7), start(8)', - '[21600ms] stop(8), systemend', + '[2400ms] stop(0), systemend, start(1)', + '[4800ms] stop(1), systemend', + ]); + }); + + it('creates for: documents that have backwards formatting', () => { + const score = render('playback_backwards_formatting.musicxml'); + + const timelines = Timeline.create(logger, score); + + expect(timelines).toHaveLength(1); + expect(timelines[0].toHumanReadable()).toEqual([ + // stave0, voice0: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + // stave1, voice0: 14 15 16 17 18 19 + // stave2, voice1: 20 21 + '[0ms] start(0), start(14), start(20)', + '[150ms] stop(14), start(15)', + '[300ms] stop(0), start(1)', + '[450ms] stop(1), start(2)', + '[600ms] stop(2), stop(15), start(3), start(16)', + '[750ms] stop(3), start(4)', + '[900ms] stop(4), start(5)', + '[1050ms] stop(5), start(6)', + '[1200ms] stop(6), stop(16), stop(20), start(7), start(17), start(21)', + '[1350ms] stop(17), start(18)', + '[1500ms] stop(7), start(8)', + '[1650ms] stop(8), start(9)', + '[1800ms] stop(9), stop(18), start(10), start(19)', + '[1950ms] stop(10), start(11)', + '[2100ms] stop(11), start(12)', + '[2250ms] stop(12), start(13)', + '[2400ms] stop(13), stop(19), stop(21), systemend', ]); }); }); -function render(filename: string) { +function render(filename: string, width = 900): vexml.Score { const musicXMLPath = path.resolve(DATA_DIR, filename); const musicXML = fs.readFileSync(musicXMLPath).toString(); const div = document.createElement('div'); return vexml.renderMusicXML(musicXML, div, { config: { - WIDTH: 900, + WIDTH: width, }, }); }