From 1865c8b4a0a0bb4b44d91dc9f8e1238e2f1e461c Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Sun, 23 Feb 2025 10:36:01 -0500 Subject: [PATCH 1/7] ensure stop sequence events get processed before start sequence events --- src/playback/sequencefactory.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/playback/sequencefactory.ts b/src/playback/sequencefactory.ts index 138cc8bae..d6cf0f214 100644 --- a/src/playback/sequencefactory.ts +++ b/src/playback/sequencefactory.ts @@ -108,7 +108,13 @@ export class SequenceFactory { measureStartTime = nextMeasureStartTime; } - return events.sort((a, b) => a.time.ms - b.time.ms); + return events.sort((a, b) => { + // When the times match, we want the stop event to come first. + if (a.time.ms === b.time.ms) { + return a.type === 'stop' ? -1 : 1; + } + return a.time.ms - b.time.ms; + }); } private toSequenceEntries(events: SequenceEvent[]): SequenceEntry[] { From 16eedbd5877e565856aea06364e42fd800cb50f6 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Sun, 23 Feb 2025 10:37:14 -0500 Subject: [PATCH 2/7] copy the active elements array when pushing sequence entries --- src/playback/sequencefactory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playback/sequencefactory.ts b/src/playback/sequencefactory.ts index d6cf0f214..ce434b7c6 100644 --- a/src/playback/sequencefactory.ts +++ b/src/playback/sequencefactory.ts @@ -261,7 +261,7 @@ class SequenceEntryBuilder { ): void { const durationRange = new DurationRange(t1, t2); const xRange = new NumberRange(x1, x2); - this.entries.push({ durationRange, xRange, anchorElement: anchor, activeElements: active }); + this.entries.push({ durationRange, xRange, anchorElement: anchor, activeElements: [...active] }); } private getLeftBoundaryX(element: PlaybackElement): number { From 51d66c099d688d483242664eaeb4f4f1b54ab045 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Sun, 23 Feb 2025 10:37:35 -0500 Subject: [PATCH 3/7] add an activate-only case when building sequence entries --- src/playback/sequencefactory.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/playback/sequencefactory.ts b/src/playback/sequencefactory.ts index ce434b7c6..a50010516 100644 --- a/src/playback/sequencefactory.ts +++ b/src/playback/sequencefactory.ts @@ -131,6 +131,7 @@ export class SequenceFactory { type XRangeInstruction = | 'anchor-to-next-event' + | 'activate-only' | 'terminate-to-measure-end-and-reanchor' | 'defer-for-interpolation' | 'ignore'; @@ -205,6 +206,9 @@ class SequenceEntryBuilder { this.pending.push(event); } else if (instruction === 'ignore') { // noop + } else if (instruction === 'activate-only') { + this.entries.at(-1)?.activeElements.push(event.element); + this.active.push(event.element); } else { util.assertUnreachable(); } @@ -321,14 +325,20 @@ class SequenceEntryBuilder { const startMeasureBeat1 = previous.getStartMeasureBeat(); const startMeasureBeat2 = current.getStartMeasureBeat(); + const x1 = previous.rect().center().x; + const x2 = current.rect().center().x; + + if (x1 === x2) { + // This is common when a part has multiple staves. When elements have the same x-coordinate, we'll just add the + // current element to the active list. + return 'activate-only'; + } + const isProgressingNormallyInTheSameMeasure = measureIndex1 === measureIndex2 && startMeasureBeat1.isLessThan(startMeasureBeat2); const isProgressingNormallyAcrossMeasures = measureIndex1 + 1 === measureIndex2; const isProgressingNormally = isProgressingNormallyInTheSameMeasure || isProgressingNormallyAcrossMeasures; - const x1 = previous.rect().center().x; - const x2 = current.rect().center().x; - if (isProgressingNormally && x1 < x2) { return 'anchor-to-next-event'; } From e6d8c2018f2d2537d414062b4dcdab92b71973d2 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Sun, 23 Feb 2025 11:45:43 -0500 Subject: [PATCH 4/7] sort sequence events by x-coordinate when their time and type are the same --- src/playback/sequencefactory.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/playback/sequencefactory.ts b/src/playback/sequencefactory.ts index a50010516..53b7735a4 100644 --- a/src/playback/sequencefactory.ts +++ b/src/playback/sequencefactory.ts @@ -109,11 +109,17 @@ export class SequenceFactory { } return events.sort((a, b) => { - // When the times match, we want the stop event to come first. - if (a.time.ms === b.time.ms) { + if (a.time.ms !== b.time.ms) { + return a.time.ms - b.time.ms; + } + + if (a.type !== b.type) { + // Stop events should come before start events. return a.type === 'stop' ? -1 : 1; } - return a.time.ms - b.time.ms; + + // If two events occur at the same time and have the same type, sort by x-coordinate. + return a.element.rect().center().x - b.element.rect().center().x; }); } From 7a3a806d47588c8fcea94cf8d8e7197ffac4044a Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Sun, 23 Feb 2025 11:54:12 -0500 Subject: [PATCH 5/7] add logging to SequenceFactory --- src/elements/score.ts | 2 +- src/playback/sequencefactory.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/elements/score.ts b/src/elements/score.ts index c8837d182..7cc67463f 100644 --- a/src/elements/score.ts +++ b/src/elements/score.ts @@ -338,7 +338,7 @@ export class Score { @util.memoize() private getSequences(): playback.Sequence[] { - const sequences = new playback.SequenceFactory(this).create(); + const sequences = new playback.SequenceFactory(this.log, this).create(); return sequences; } diff --git a/src/playback/sequencefactory.ts b/src/playback/sequencefactory.ts index 53b7735a4..9a36c5566 100644 --- a/src/playback/sequencefactory.ts +++ b/src/playback/sequencefactory.ts @@ -6,6 +6,7 @@ import { Sequence } from './sequence'; import { PlaybackElement, SequenceEntry } from './types'; import { DurationRange } from './durationrange'; import { MeasureSequenceIterator } from './measuresequenceiterator'; +import { Logger } from '@/debug'; const LAST_SYSTEM_MEASURE_X_RANGE_PADDING_RIGHT = 10; @@ -16,7 +17,7 @@ type SequenceEvent = { }; export class SequenceFactory { - constructor(private score: elements.Score) {} + constructor(private log: Logger, private score: elements.Score) {} create(): Sequence[] { const sequences = new Array(); @@ -125,7 +126,7 @@ export class SequenceFactory { private toSequenceEntries(events: SequenceEvent[]): SequenceEntry[] { const measures = this.score.getMeasures(); - const builder = new SequenceEntryBuilder(measures); + const builder = new SequenceEntryBuilder(this.log, measures); for (const event of events) { builder.add(event); @@ -152,7 +153,7 @@ class SequenceEntryBuilder { private t = Duration.ms(-1); private built = false; - constructor(private measures: elements.Measure[]) {} + constructor(private log: Logger, private measures: elements.Measure[]) {} add(event: SequenceEvent): void { if (event.type === 'start') { From 8b0c4a98fdce5c230f40354cf6589caaa2eb9790 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Sun, 23 Feb 2025 11:55:19 -0500 Subject: [PATCH 6/7] force a fix when x1 > x2 when creating sequence entries --- src/playback/sequencefactory.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/playback/sequencefactory.ts b/src/playback/sequencefactory.ts index 9a36c5566..ae800bf49 100644 --- a/src/playback/sequencefactory.ts +++ b/src/playback/sequencefactory.ts @@ -186,11 +186,22 @@ class SequenceEntryBuilder { const instruction = this.getXRangeInstruction(this.anchor, event.element); if (instruction === 'anchor-to-next-event') { - const x1 = this.x; + let x1 = this.x; const x2 = this.getLeftBoundaryX(event.element); const t1 = this.t; const t2 = event.time; + if (x1 > x2) { + // See https://github.com/stringsync/vexml/issues/264 for context. + this.log.warn('encountered a sequence-building issue where x1 > x2, forcing a fix', { + x1, + x2, + x: this.x, + absoluteMeasureIndex: event.element.getAbsoluteMeasureIndex(), + }); + x1 = this.anchor.rect().center().x; + } + this.processPending(new NumberRange(x1, x2), t1); this.active.push(event.element); this.push(x1, x2, t1, t2, this.anchor, this.active); From d3115972f1b0dbd58594337269c488c664ee4d03 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Sun, 23 Feb 2025 12:01:13 -0500 Subject: [PATCH 7/7] emphasize vexml.renderMXL returns a promise --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a3df4261b..46db5618c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ import * as vexml from '@stringsync/vexml'; const mxl = new Blob(['some', 'valid', 'mxl', 'bytes']); const div = document.getElementById('my-id'); const scorePromise = vexml.renderMXL(musicXML, div); +// From here, you need to await or call then() on the scorePromise to extract the score. ``` ## Advanced Usage