Browse Source

merge osmd-public 1.5.2: add tuplet number limit, add soft-accent, fix rest note TypeLength undefined, fix CollectionUtil.binarySearch

sschmidTU 2 years ago
parent
commit
3d851b1752

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "osmd-extended",
   "name": "osmd-extended",
-  "version": "1.5.1",
+  "version": "1.5.2",
   "description": "Private / sponsor exclusive OSMD mirror/audio player.",
   "description": "Private / sponsor exclusive OSMD mirror/audio player.",
   "main": "build/opensheetmusicdisplay.min.js",
   "main": "build/opensheetmusicdisplay.min.js",
   "types": "build/dist/src/index.d.ts",
   "types": "build/dist/src/index.d.ts",

+ 10 - 0
src/MusicalScore/Graphical/EngravingRules.ts

@@ -109,6 +109,8 @@ export class EngravingRules {
     public DistanceOffsetBetweenTwoHorizontallyCrossedWedges: number;
     public DistanceOffsetBetweenTwoHorizontallyCrossedWedges: number;
     public WedgeMinLength: number;
     public WedgeMinLength: number;
     public WedgeEndDistanceBetweenTimestampsFactor: number;
     public WedgeEndDistanceBetweenTimestampsFactor: number;
+    public SoftAccentWedgePadding: number;
+    public SoftAccentSizeFactor: number;
     public DistanceBetweenAdjacentDynamics: number;
     public DistanceBetweenAdjacentDynamics: number;
     public TempoChangeMeasureValidity: number;
     public TempoChangeMeasureValidity: number;
     public TempoContinousFactor: number;
     public TempoContinousFactor: number;
@@ -151,6 +153,9 @@ export class EngravingRules {
     public TripletsBracketed: boolean;
     public TripletsBracketed: boolean;
     public TupletNumberLabelHeight: number;
     public TupletNumberLabelHeight: number;
     public TupletNumberYOffset: number;
     public TupletNumberYOffset: number;
+    public TupletNumberLimitConsecutiveRepetitions: boolean;
+    public TupletNumberMaxConsecutiveRepetitions: number;
+    public TupletNumberAlwaysDisableAfterFirstMax: boolean;
     public LabelMarginBorderFactor: number;
     public LabelMarginBorderFactor: number;
     public TupletVerticalLineLength: number;
     public TupletVerticalLineLength: number;
     public TupletNumbersInTabs: boolean;
     public TupletNumbersInTabs: boolean;
@@ -490,6 +495,8 @@ export class EngravingRules {
         this.DistanceOffsetBetweenTwoHorizontallyCrossedWedges = 0.3;
         this.DistanceOffsetBetweenTwoHorizontallyCrossedWedges = 0.3;
         this.WedgeMinLength = 2.0;
         this.WedgeMinLength = 2.0;
         this.WedgeEndDistanceBetweenTimestampsFactor = 1.75;
         this.WedgeEndDistanceBetweenTimestampsFactor = 1.75;
+        this.SoftAccentWedgePadding = 0.4;
+        this.SoftAccentSizeFactor = 0.6;
         this.DistanceBetweenAdjacentDynamics = 0.75;
         this.DistanceBetweenAdjacentDynamics = 0.75;
 
 
         // Tempo Variables
         // Tempo Variables
@@ -532,6 +539,9 @@ export class EngravingRules {
         this.TripletsBracketed = false; // special setting for triplets, overrides tuplet setting (for triplets only)
         this.TripletsBracketed = false; // special setting for triplets, overrides tuplet setting (for triplets only)
         this.TupletNumberLabelHeight = 1.5 * EngravingRules.unit;
         this.TupletNumberLabelHeight = 1.5 * EngravingRules.unit;
         this.TupletNumberYOffset = 0.5;
         this.TupletNumberYOffset = 0.5;
+        this.TupletNumberLimitConsecutiveRepetitions = true;
+        this.TupletNumberMaxConsecutiveRepetitions = 2;
+        this.TupletNumberAlwaysDisableAfterFirstMax = true;
         this.LabelMarginBorderFactor = 0.1;
         this.LabelMarginBorderFactor = 0.1;
         this.TupletVerticalLineLength = 0.5;
         this.TupletVerticalLineLength = 0.5;
         this.TupletNumbersInTabs = false; // disabled by default, nonstandard in tabs, at least how we show them in non-tabs.
         this.TupletNumbersInTabs = false; // disabled by default, nonstandard in tabs, at least how we show them in non-tabs.

+ 12 - 4
src/MusicalScore/Graphical/GraphicalContinuousDynamicExpression.ts

@@ -23,6 +23,8 @@ export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpre
     private lines: GraphicalLine[] = [];
     private lines: GraphicalLine[] = [];
     private startMeasure: GraphicalMeasure;
     private startMeasure: GraphicalMeasure;
     private endMeasure: GraphicalMeasure;
     private endMeasure: GraphicalMeasure;
+    //public StartIsEnd: boolean;
+    public IsSoftAccent: boolean;
 
 
     /**
     /**
      * Create a new instance of the GraphicalContinuousDynamicExpression
      * Create a new instance of the GraphicalContinuousDynamicExpression
@@ -67,7 +69,7 @@ export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpre
         const skyBottomLineCalculator: SkyBottomLineCalculator = this.parentStaffLine.SkyBottomLineCalculator;
         const skyBottomLineCalculator: SkyBottomLineCalculator = this.parentStaffLine.SkyBottomLineCalculator;
         const left: number = this.IsVerbal ? this.label.PositionAndShape.RelativePosition.x + this.label.PositionAndShape.BorderMarginLeft : 0;
         const left: number = this.IsVerbal ? this.label.PositionAndShape.RelativePosition.x + this.label.PositionAndShape.BorderMarginLeft : 0;
         const right: number = this.IsVerbal ? this.label.PositionAndShape.RelativePosition.x + this.label.PositionAndShape.BorderMarginRight : 0;
         const right: number = this.IsVerbal ? this.label.PositionAndShape.RelativePosition.x + this.label.PositionAndShape.BorderMarginRight : 0;
-        if (!this.IsVerbal && this.lines.length < 2) {
+        if (!this.IsSoftAccent && !this.IsVerbal && this.lines.length < 2) {
             log.warn("Not enough lines for SkyBottomLine calculation");
             log.warn("Not enough lines for SkyBottomLine calculation");
         }
         }
         if (!this.IsVerbal) {
         if (!this.IsVerbal) {
@@ -80,7 +82,11 @@ export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpre
         }
         }
         switch (this.Placement) {
         switch (this.Placement) {
             case PlacementEnum.Above:
             case PlacementEnum.Above:
-                if (!this.IsVerbal) {
+                if (this.IsSoftAccent) {
+                    skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[0].Start, this.lines[0].End);
+                    skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[2].End, this.lines[2].Start);
+                    skyBottomLineCalculator.updateSkyLineWithLine(this.lines[0].End, this.lines[2].End, this.lines[0].End.y);
+                } else if (!this.IsVerbal) {
                     if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
                     if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
                         skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[0].Start, this.lines[0].End);
                         skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[0].Start, this.lines[0].End);
                     } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
                     } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
@@ -269,8 +275,10 @@ export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpre
         // TODO is the center position correct? it wasn't set before, important for AlignmentManager.alignDynamicExpressions()
         // TODO is the center position correct? it wasn't set before, important for AlignmentManager.alignDynamicExpressions()
         // console.log(`relative y, center y: ${this.PositionAndShape.RelativePosition.y},${this.PositionAndShape.Center.y})`);
         // console.log(`relative y, center y: ${this.PositionAndShape.RelativePosition.y},${this.PositionAndShape.Center.y})`);
 
 
-
-        if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
+        if (this.IsSoftAccent) {
+            this.PositionAndShape.BorderMarginLeft = 0;
+            this.PositionAndShape.BorderMarginRight = this.lines[3].Start.x - this.lines[0].Start.x;
+        } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
             this.PositionAndShape.BorderMarginLeft = 0;
             this.PositionAndShape.BorderMarginLeft = 0;
             this.PositionAndShape.BorderMarginRight = this.lines[0].End.x - this.lines[0].Start.x;
             this.PositionAndShape.BorderMarginRight = this.lines[0].End.x - this.lines[0].Start.x;
         } else {
         } else {

+ 100 - 4
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -1084,6 +1084,75 @@ export abstract class MusicSheetCalculator {
     }
     }
 
 
     protected calculateTupletNumbers(): void {
     protected calculateTupletNumbers(): void {
+        if (!this.rules.TupletNumberLimitConsecutiveRepetitions) {
+            return;
+        }
+        let currentTupletNumber: number = -1;
+        let consecutiveTupletCount: number = 0;
+        let currentTuplet: Tuplet = undefined;
+        let skipTuplet: Tuplet = undefined; // if set, ignore (further) handling of this tuplet
+        const disabledPerVoice: Object = {};
+        for (const instrument of this.graphicalMusicSheet.ParentMusicSheet.Instruments) {
+            for (const voice of instrument.Voices) {
+                consecutiveTupletCount = 0; // reset for next voice
+                disabledPerVoice[voice.VoiceId] = {};
+                for (const ve of voice.VoiceEntries) {
+                    if (ve.Notes.length > 0) {
+                        const firstNote: Note = ve.Notes[0];
+                        if (!firstNote.NoteTuplet) {
+                            currentTupletNumber = -1;
+                            consecutiveTupletCount = 0;
+                            currentTuplet = undefined;
+                            continue;
+                        }
+                        if (firstNote.NoteTuplet === skipTuplet) {
+                            continue;
+                        }
+                        let typeLength: Fraction = firstNote.TypeLength;
+                        if (!typeLength) {
+                            // shouldn't happen, now that rest notes have TypeLength set too, see VoiceGenerator.addRestNote(), addSingleNote()
+                            //   see test_tuplets_starting_with_rests_layout.mxl (first measure bass)
+                            log.warn("note missing TypeLength");
+                            typeLength = firstNote.NoteTuplet.Fractions[0];
+                        }
+                        if (firstNote.NoteTuplet !== currentTuplet) {
+                            if (disabledPerVoice[voice.VoiceId][firstNote.NoteTuplet.TupletLabelNumber]) {
+                                if (disabledPerVoice[voice.VoiceId][firstNote.NoteTuplet.TupletLabelNumber][typeLength.RealValue]) {
+                                    firstNote.NoteTuplet.RenderTupletNumber = false;
+                                    skipTuplet = firstNote.NoteTuplet;
+                                    continue;
+                                }
+                            }
+                        }
+                        if (firstNote.NoteTuplet.TupletLabelNumber !== currentTupletNumber) {
+                            currentTupletNumber = firstNote.NoteTuplet.TupletLabelNumber;
+                            consecutiveTupletCount = 0;
+                        }
+                        currentTuplet = firstNote.NoteTuplet;
+                        consecutiveTupletCount++;
+                        if (consecutiveTupletCount <= this.rules.TupletNumberMaxConsecutiveRepetitions) {
+                            firstNote.NoteTuplet.RenderTupletNumber = true; // need to re-activate after re-render when it was set to false
+                        }
+                        if (consecutiveTupletCount === this.rules.TupletNumberMaxConsecutiveRepetitions && this.rules.TupletNumberAlwaysDisableAfterFirstMax) {
+                            if (!disabledPerVoice[voice.VoiceId][currentTupletNumber]) {
+                                disabledPerVoice[voice.VoiceId][currentTupletNumber] = {};
+                            }
+                            disabledPerVoice[voice.VoiceId][currentTupletNumber][typeLength.RealValue] = true;
+                        }
+                        if (consecutiveTupletCount > this.rules.TupletNumberMaxConsecutiveRepetitions) {
+                            firstNote.NoteTuplet.RenderTupletNumber = false;
+                            if (this.rules.TupletNumberAlwaysDisableAfterFirstMax) {
+                                if (!disabledPerVoice[voice.VoiceId][currentTupletNumber]) {
+                                    disabledPerVoice[voice.VoiceId][currentTupletNumber] = {};
+                                }
+                                disabledPerVoice[voice.VoiceId][currentTupletNumber][typeLength.RealValue] = true;
+                            }
+                        }
+                        skipTuplet = currentTuplet;
+                    }
+                }
+            }
+        }
         return;
         return;
     }
     }
 
 
@@ -1128,6 +1197,7 @@ export abstract class MusicSheetCalculator {
     * @param startPosInStaffline Starting point in staff line
     * @param startPosInStaffline Starting point in staff line
     */
     */
     public calculateGraphicalContinuousDynamic(graphicalContinuousDynamic: GraphicalContinuousDynamicExpression, startPosInStaffline: PointF2D): void {
     public calculateGraphicalContinuousDynamic(graphicalContinuousDynamic: GraphicalContinuousDynamicExpression, startPosInStaffline: PointF2D): void {
+        const isSoftAccent: boolean = graphicalContinuousDynamic.IsSoftAccent;
         const staffIndex: number = graphicalContinuousDynamic.ParentStaffLine.ParentStaff.idInMusicSheet;
         const staffIndex: number = graphicalContinuousDynamic.ParentStaffLine.ParentStaff.idInMusicSheet;
         // TODO: Previously the staffIndex was passed down. BUT you can (and this function actually does this) get it from
         // TODO: Previously the staffIndex was passed down. BUT you can (and this function actually does this) get it from
         // the musicSystem OR from the ParentStaffLine. Is this the same index?
         // the musicSystem OR from the ParentStaffLine. Is this the same index?
@@ -1172,9 +1242,18 @@ export abstract class MusicSheetCalculator {
         const beginOfNextNote: Fraction = Fraction.plus(endAbsoluteTimestamp, maxNoteLength);
         const beginOfNextNote: Fraction = Fraction.plus(endAbsoluteTimestamp, maxNoteLength);
         const nextNotePosInStaffLine: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
         const nextNotePosInStaffLine: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
             beginOfNextNote, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0);
             beginOfNextNote, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0);
+        const wedgePadding: number = this.rules.SoftAccentWedgePadding;
+        const staffEntryWidth: number = container.getFirstNonNullStaffEntry().PositionAndShape.Size.width; // staff entry widths for whole notes is too long
+        const sizeFactor: number = this.rules.SoftAccentSizeFactor;
+        //const standardWidth: number = 2;
+
         //If the next note position is not on the next staffline
         //If the next note position is not on the next staffline
         //extend close to the next note
         //extend close to the next note
-        if (nextNotePosInStaffLine.x > endPosInStaffLine.x && nextNotePosInStaffLine.x < endOfMeasure) {
+        if (isSoftAccent) {
+            //startPosInStaffline.x -= 1;
+            startPosInStaffline.x -= staffEntryWidth / 2 * sizeFactor + wedgePadding;
+            endPosInStaffLine.x = startPosInStaffline.x + staffEntryWidth / 2 * sizeFactor;
+        } else if (nextNotePosInStaffLine.x > endPosInStaffLine.x && nextNotePosInStaffLine.x < endOfMeasure) {
             endPosInStaffLine.x += (nextNotePosInStaffLine.x - endPosInStaffLine.x) / this.rules.WedgeEndDistanceBetweenTimestampsFactor;
             endPosInStaffLine.x += (nextNotePosInStaffLine.x - endPosInStaffLine.x) / this.rules.WedgeEndDistanceBetweenTimestampsFactor;
         } else { //Otherwise extend to the end of the measure
         } else { //Otherwise extend to the end of the measure
             endPosInStaffLine.x = endOfMeasure - this.rules.WedgeHorizontalMargin;
             endPosInStaffLine.x = endOfMeasure - this.rules.WedgeHorizontalMargin;
@@ -1198,13 +1277,13 @@ export abstract class MusicSheetCalculator {
         let secondGraphicalContinuousDynamic: GraphicalContinuousDynamicExpression = undefined;
         let secondGraphicalContinuousDynamic: GraphicalContinuousDynamicExpression = undefined;
 
 
         // last length check
         // last length check
-        if (sameStaffLine && endPosInStaffLine.x - startPosInStaffline.x < this.rules.WedgeMinLength) {
+        if (sameStaffLine && endPosInStaffLine.x - startPosInStaffline.x < this.rules.WedgeMinLength && !isSoftAccent) {
             endPosInStaffLine.x = startPosInStaffline.x + this.rules.WedgeMinLength;
             endPosInStaffLine.x = startPosInStaffline.x + this.rules.WedgeMinLength;
         }
         }
 
 
         // Upper staff wedge always starts at the given position and the lower staff wedge always starts at the begin of measure
         // Upper staff wedge always starts at the given position and the lower staff wedge always starts at the begin of measure
         const upperStartX: number = startPosInStaffline.x;
         const upperStartX: number = startPosInStaffline.x;
-        const lowerStartX: number = endStaffLine.Measures[0].beginInstructionsWidth - this.rules.WedgeHorizontalMargin - 2;
+        let lowerStartX: number = endStaffLine.Measures[0].beginInstructionsWidth - this.rules.WedgeHorizontalMargin - 2;
         //TODO fix this when a range of measures to draw is given that doesn't include all the dynamic's measures (e.g. for crescendo)
         //TODO fix this when a range of measures to draw is given that doesn't include all the dynamic's measures (e.g. for crescendo)
         let upperEndX: number = 0;
         let upperEndX: number = 0;
         let lowerEndX: number = 0;
         let lowerEndX: number = 0;
@@ -1221,6 +1300,17 @@ export abstract class MusicSheetCalculator {
         } else {
         } else {
             upperEndX = endPosInStaffLine.x;
             upperEndX = endPosInStaffLine.x;
         }
         }
+        if (isSoftAccent) {
+            // secondGraphicalContinuousDynamic = new GraphicalContinuousDynamicExpression(
+            //     graphicalContinuousDynamic.ContinuousDynamic,
+            //     graphicalContinuousDynamic.ParentStaffLine,
+            //     graphicalContinuousDynamic.StartMeasure.parentSourceMeasure
+            // );
+            // secondGraphicalContinuousDynamic.StartIsEnd = true;
+            // doesn't work well with secondGraphicalDynamic, positions/rendering messed up
+            lowerStartX = endPosInStaffLine.x + wedgePadding;
+            lowerEndX = lowerStartX + staffEntryWidth / 2 * sizeFactor;
+        }
 
 
         // the Height of the Expression's placement
         // the Height of the Expression's placement
         let idealY: number = 0;
         let idealY: number = 0;
@@ -1409,7 +1499,13 @@ export abstract class MusicSheetCalculator {
         // Crescendo (point to the left, opening to the right)
         // Crescendo (point to the left, opening to the right)
         graphicalContinuousDynamic.Lines.clear();
         graphicalContinuousDynamic.Lines.clear();
         if (graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
         if (graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
-            if (sameStaffLine) {
+            if (isSoftAccent) {
+                graphicalContinuousDynamic.createFirstHalfCrescendoLines(upperStartX, upperEndX, idealY);
+                graphicalContinuousDynamic.createSecondHalfDiminuendoLines(lowerStartX, lowerEndX, idealY);
+                graphicalContinuousDynamic.calcPsi();
+                // secondGraphicalContinuousDynamic.createSecondHalfDiminuendoLines(lowerStartX, lowerEndX, idealY);
+                // secondGraphicalContinuousDynamic.calcPsi();
+            } else if (sameStaffLine && !isSoftAccent) {
                 graphicalContinuousDynamic.createCrescendoLines(upperStartX, upperEndX, idealY);
                 graphicalContinuousDynamic.createCrescendoLines(upperStartX, upperEndX, idealY);
                 graphicalContinuousDynamic.calcPsi();
                 graphicalContinuousDynamic.calcPsi();
             } else {
             } else {

+ 9 - 0
src/MusicalScore/Graphical/SkyBottomLineCalculator.ts

@@ -202,6 +202,15 @@ export class SkyBottomLineCalculator {
 
 
         this.updateLines(results);
         this.updateLines(results);
     }
     }
+
+    public updateSkyLineWithLine(start: PointF2D, end: PointF2D, value: number): void {
+        const startIndex: number = Math.floor(start.x * this.SamplingUnit);
+        const endIndex: number = Math.ceil(end.x * this.SamplingUnit);
+        for (let i: number = startIndex + 1; i < Math.min(endIndex, this.SkyLine.length); i++) {
+            this.SkyLine[i] = value;
+        }
+    }
+
     /**
     /**
      * This method updates the SkyLine for a given Wedge.
      * This method updates the SkyLine for a given Wedge.
      * @param start Start point of the wedge (the point where both lines meet)
      * @param start Start point of the wedge (the point where both lines meet)

+ 23 - 10
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -626,8 +626,20 @@ export class VexFlowMeasure extends GraphicalMeasure {
             // Draw tuplets
             // Draw tuplets
             for (const voiceID in this.vftuplets) {
             for (const voiceID in this.vftuplets) {
                 if (this.vftuplets.hasOwnProperty(voiceID)) {
                 if (this.vftuplets.hasOwnProperty(voiceID)) {
-                    for (const tuplet of this.vftuplets[voiceID]) {
-                        tuplet.setContext(ctx).draw();
+                    for (let i: number = 0; i < this.tuplets[voiceID].length; i++) {
+                        const tuplet: Tuplet = this.tuplets[voiceID][i][0];
+                        const vftuplet: VF.Tuplet = this.vftuplets[voiceID][i];
+                        if (!tuplet.RenderTupletNumber) {
+                            // (vftuplet as any).numerator_glyphs_stored = [...(vftuplet as any).numerator_glyphs];
+                            // (vftuplet as any).numerator_glyphs = [];
+                            (vftuplet as any).RenderTupletNumber = false;
+                        } else {
+                            // issue with restoring glyphs (version without vexflowpatch): need to deep copy array, otherwise the reference is overwritten
+                            // (vftuplet as any).numerator_glyphs = [...(vftuplet as any).numerator_glyphs_stored];
+                            // (vftuplet as any).numerator_glyphs_stored = undefined;
+                            (vftuplet as any).RenderTupletNumber = true;
+                        }
+                        vftuplet.setContext(ctx).draw();
                     }
                     }
                 }
                 }
             }
             }
@@ -1138,14 +1150,15 @@ export class VexFlowMeasure extends GraphicalMeasure {
                       if (tuplet.tupletLabelNumberPlacement === PlacementEnum.Below) {
                       if (tuplet.tupletLabelNumberPlacement === PlacementEnum.Below) {
                           location = VF.Tuplet.LOCATION_BOTTOM;
                           location = VF.Tuplet.LOCATION_BOTTOM;
                       }
                       }
-                      vftuplets.push(new VF.Tuplet( tupletStaveNotes,
-                                                          {
-                                                            bracketed: bracketed,
-                                                            location: location,
-                                                            notes_occupied: notesOccupied,
-                                                            num_notes: tuplet.TupletLabelNumber, //, location: -1, ratioed: true
-                                                            ratioed: this.rules.TupletsRatioed,
-                                                          }));
+                      const vftuplet: VF.Tuplet = new VF.Tuplet(tupletStaveNotes,
+                        {
+                          bracketed: bracketed,
+                          location: location,
+                          notes_occupied: notesOccupied,
+                          num_notes: tuplet.TupletLabelNumber, //, location: -1, ratioed: true
+                          ratioed: this.rules.TupletsRatioed,
+                        });
+                      vftuplets.push(vftuplet);
                     } else {
                     } else {
                         log.debug("Warning! Tuplet with no notes! Trying to ignore, but this is a serious problem.");
                         log.debug("Warning! Tuplet with no notes! Trying to ignore, but this is a serious problem.");
                     }
                     }

+ 2 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -742,6 +742,8 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         staffLine,
         staffLine,
         startMeasure.parentSourceMeasure);
         startMeasure.parentSourceMeasure);
       graphicalContinuousDynamic.StartMeasure = startMeasure;
       graphicalContinuousDynamic.StartMeasure = startMeasure;
+      graphicalContinuousDynamic.IsSoftAccent = multiExpression.StartingContinuousDynamic.IsStartOfSoftAccent;
+      //graphicalContinuousDynamic.StartIsEnd = multiExpression.StartingContinuousDynamic.EndMultiExpression === multiExpression;
 
 
       if (!graphicalContinuousDynamic.IsVerbal && continuousDynamic.EndMultiExpression) {
       if (!graphicalContinuousDynamic.IsVerbal && continuousDynamic.EndMultiExpression) {
         try {
         try {

+ 38 - 0
src/MusicalScore/ScoreIO/MusicSymbolModules/ArticulationReader.ts

@@ -8,6 +8,9 @@ import {AccidentalEnum} from "../../../Common/DataObjects/Pitch";
 import { Articulation } from "../../VoiceData/Articulation";
 import { Articulation } from "../../VoiceData/Articulation";
 import { Note } from "../../VoiceData/Note";
 import { Note } from "../../VoiceData/Note";
 import { EngravingRules } from "../../Graphical/EngravingRules";
 import { EngravingRules } from "../../Graphical/EngravingRules";
+import { MultiExpression } from "../../VoiceData/Expressions/MultiExpression";
+import { SourceMeasure } from "../../VoiceData/SourceMeasure";
+import { ContDynamicEnum, ContinuousDynamicExpression } from "../../VoiceData/Expressions/ContinuousExpressions";
 export class ArticulationReader {
 export class ArticulationReader {
   private rules: EngravingRules;
   private rules: EngravingRules;
 
 
@@ -95,6 +98,41 @@ export class ArticulationReader {
                 newArticulation.articulationEnum = ArticulationEnum.marcatodown;
                 newArticulation.articulationEnum = ArticulationEnum.marcatodown;
               }
               }
             }
             }
+            if (articulationEnum === ArticulationEnum.softaccent) {
+              const staffId: number = currentVoiceEntry.ParentSourceStaffEntry.ParentStaff.Id - 1;
+              if (placement === PlacementEnum.NotYetDefined) {
+                placement = PlacementEnum.Above;
+                if (staffId > 0) {
+                  placement = PlacementEnum.Below;
+                }
+                // TODO place according to whether the corresponding note is higher (-> above) or lower (-> below)
+                //   than the middle note line. Though this could be tricky at this stage of parsing.
+              }
+              const sourceMeasure: SourceMeasure = currentVoiceEntry.ParentSourceStaffEntry.VerticalContainerParent.ParentMeasure;
+              const multi: MultiExpression = new MultiExpression(sourceMeasure, currentVoiceEntry.Timestamp);
+              multi.StartingContinuousDynamic = new ContinuousDynamicExpression(
+                ContDynamicEnum.crescendo,
+                placement,
+                staffId,
+                sourceMeasure,
+                null,
+                -1
+              );
+              multi.StartingContinuousDynamic.IsStartOfSoftAccent = true;
+              multi.StartingContinuousDynamic.StartMultiExpression = multi;
+              multi.StartingContinuousDynamic.EndMultiExpression = multi;
+              multi.EndingContinuousDynamic = new ContinuousDynamicExpression(
+                ContDynamicEnum.diminuendo,
+                placement,
+                staffId,
+                sourceMeasure,
+                null,
+                -1
+              );
+              multi.EndingContinuousDynamic.StartMultiExpression = multi;
+              multi.EndingContinuousDynamic.EndMultiExpression = multi;
+              sourceMeasure.StaffLinkedExpressions[staffId].push(multi);
+            }
 
 
             // don't add the same articulation twice
             // don't add the same articulation twice
             if (!currentVoiceEntry.hasArticulation(newArticulation)) {
             if (!currentVoiceEntry.hasArticulation(newArticulation)) {

+ 4 - 2
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -162,7 +162,7 @@ export class VoiceGenerator {
 
 
     try {
     try {
       this.currentNote = restNote
       this.currentNote = restNote
-        ? this.addRestNote(noteNode.element("rest"), noteDuration, noteTypeXml, normalNotes, printObject, isCueNote, noteheadColorXml)
+        ? this.addRestNote(noteNode.element("rest"), noteDuration, noteTypeXml, typeDuration, normalNotes, printObject, isCueNote, noteheadColorXml)
         : this.addSingleNote(noteNode, noteDuration, noteTypeXml, typeDuration, normalNotes, chord, octavePlusOne,
         : this.addSingleNote(noteNode, noteDuration, noteTypeXml, typeDuration, normalNotes, chord, octavePlusOne,
                              printObject, isCueNote, isGraceNote, stemDirectionXml, tremoloStrokes, stemColorXml, noteheadColorXml);
                              printObject, isCueNote, isGraceNote, stemDirectionXml, tremoloStrokes, stemColorXml, noteheadColorXml);
       this.currentNote.DotsXml = dotsXml;
       this.currentNote.DotsXml = dotsXml;
@@ -562,7 +562,7 @@ export class VoiceGenerator {
    * @param divisions
    * @param divisions
    * @returns {Note}
    * @returns {Note}
    */
    */
-  private addRestNote(node: IXmlElement, noteDuration: Fraction, noteTypeXml: NoteType,
+  private addRestNote(node: IXmlElement, noteDuration: Fraction, noteTypeXml: NoteType, typeDuration: Fraction,
                       normalNotes: number, printObject: boolean, isCueNote: boolean, noteheadColorXml: string): Note {
                       normalNotes: number, printObject: boolean, isCueNote: boolean, noteheadColorXml: string): Note {
     const restFraction: Fraction = Fraction.createFromFraction(noteDuration);
     const restFraction: Fraction = Fraction.createFromFraction(noteDuration);
     const displayStepElement: IXmlElement = node.element("display-step");
     const displayStepElement: IXmlElement = node.element("display-step");
@@ -577,6 +577,8 @@ export class VoiceGenerator {
     }
     }
     const restNote: Note = new Note(this.currentVoiceEntry, this.currentStaffEntry, restFraction, pitch, this.currentMeasure, true);
     const restNote: Note = new Note(this.currentVoiceEntry, this.currentStaffEntry, restFraction, pitch, this.currentMeasure, true);
     this.addNoteInfo(restNote, noteTypeXml, printObject, isCueNote, normalNotes, displayStep, displayOctave, noteheadColorXml, noteheadColorXml);
     this.addNoteInfo(restNote, noteTypeXml, printObject, isCueNote, normalNotes, displayStep, displayOctave, noteheadColorXml, noteheadColorXml);
+    restNote.TypeLength = typeDuration; // needed for tuplet note type information
+    //  (e.g. quarter rest - but length different due to tuplet). see MusicSheetCalculator.calculateTupletNumbers()
     this.currentVoiceEntry.Notes.push(restNote);
     this.currentVoiceEntry.Notes.push(restNote);
     if (this.openBeams.length > 0) {
     if (this.openBeams.length > 0) {
       this.openBeams.last().ExtendedNoteList.push(restNote);
       this.openBeams.last().ExtendedNoteList.push(restNote);

+ 1 - 0
src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression.ts

@@ -35,6 +35,7 @@ export class ContinuousDynamicExpression extends AbstractExpression {
     private endVolume: number;
     private endVolume: number;
     private staffNumber: number;
     private staffNumber: number;
     private label: string;
     private label: string;
+    public IsStartOfSoftAccent: boolean;
     private activeInstantaneousDynamic: InstantaneousDynamicExpression;
     private activeInstantaneousDynamic: InstantaneousDynamicExpression;
 
 
     public setStartAndEndVolume(): void {
     public setStartAndEndVolume(): void {

+ 1 - 0
src/MusicalScore/VoiceData/Tuplet.ts

@@ -15,6 +15,7 @@ export class Tuplet {
     private tupletLabelNumber: number;
     private tupletLabelNumber: number;
     public PlacementFromXml: boolean = false;
     public PlacementFromXml: boolean = false;
     public tupletLabelNumberPlacement: PlacementEnum;
     public tupletLabelNumberPlacement: PlacementEnum;
+    public RenderTupletNumber: boolean = true;
     /** Notes contained in the tuplet, per VoiceEntry (list of VoiceEntries, which has a list of notes). */
     /** Notes contained in the tuplet, per VoiceEntry (list of VoiceEntries, which has a list of notes). */
     private notes: Note[][] = []; // TODO should probably be VoiceEntry[], not Note[][].
     private notes: Note[][] = []; // TODO should probably be VoiceEntry[], not Note[][].
     private fractions: Fraction[] = [];
     private fractions: Fraction[] = [];

+ 2 - 0
src/MusicalScore/VoiceData/VoiceEntry.ts

@@ -268,6 +268,7 @@ export class VoiceEntry {
         switch (articulation) {
         switch (articulation) {
             case ArticulationEnum.accent:
             case ArticulationEnum.accent:
             case ArticulationEnum.strongaccent:
             case ArticulationEnum.strongaccent:
+            case ArticulationEnum.softaccent:
             case ArticulationEnum.invertedstrongaccent:
             case ArticulationEnum.invertedstrongaccent:
             case ArticulationEnum.staccato:
             case ArticulationEnum.staccato:
             case ArticulationEnum.staccatissimo:
             case ArticulationEnum.staccatissimo:
@@ -332,6 +333,7 @@ export class VoiceEntry {
 export enum ArticulationEnum {
 export enum ArticulationEnum {
     accent,
     accent,
     strongaccent,
     strongaccent,
+    softaccent,
     marcatoup,
     marcatoup,
     marcatodown,
     marcatodown,
     invertedstrongaccent,
     invertedstrongaccent,

+ 1 - 1
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -35,7 +35,7 @@ import { DynamicsCalculator } from "../MusicalScore/ScoreIO/MusicSymbolModules/D
  * After the constructor, use load() and render() to load and render a MusicXML file.
  * After the constructor, use load() and render() to load and render a MusicXML file.
  */
  */
 export class OpenSheetMusicDisplay {
 export class OpenSheetMusicDisplay {
-    private version: string = "1.5.1-audio-extended"; // getter: this.Version
+    private version: string = "1.5.2-audio-extended"; // getter: this.Version
     // at release, bump version and change to -release, afterwards to -dev again
     // at release, bump version and change to -release, afterwards to -dev again
 
 
     /**
     /**

+ 7 - 0
src/Util/CollectionUtil.ts

@@ -89,8 +89,14 @@ export class CollectionUtil {
                                   startIndex: number = 0,
                                   startIndex: number = 0,
                                   endIndex: number = array.length - 1): number {
                                   endIndex: number = array.length - 1): number {
         let mid: number = 1;
         let mid: number = 1;
+        let lastMidChecked: number = -1;
         while (startIndex <= endIndex) {
         while (startIndex <= endIndex) {
             mid = Math.floor((startIndex + endIndex) / 2);
             mid = Math.floor((startIndex + endIndex) / 2);
+            if (mid === lastMidChecked) {
+                break;
+                // this fixes a rare infinite loop when no matching element can be found,
+                //   e.g. with very small fraction difference in AbsoluteTimestamp like 511/1024 instead of 1/2 (#1201)
+            }
             const c: number = cmp(array[mid], element);
             const c: number = cmp(array[mid], element);
             if (c === 0) {
             if (c === 0) {
                 return mid;
                 return mid;
@@ -101,6 +107,7 @@ export class CollectionUtil {
             if (0 < c) {
             if (0 < c) {
                 endIndex = mid;
                 endIndex = mid;
             }
             }
+            lastMidChecked = mid;
         }
         }
 
 
         return -mid;
         return -mid;

+ 5 - 2
src/VexFlowPatch/readme.txt

@@ -68,11 +68,14 @@ able to add extra attributes (like svg node id) to a stroke (e.g. stem)
 tabnote.js (merged Vexflow 3.x):
 tabnote.js (merged Vexflow 3.x):
 Add a context group for each tabnote, so that it can be found in the SVG DOM ("vf-tabnote")
 Add a context group for each tabnote, so that it can be found in the SVG DOM ("vf-tabnote")
 
 
+timesignature.js (fixed vexflow 4):
+open group to get SVG group+class for key signature
+
 tremolo.js (fixed vexflow 4):
 tremolo.js (fixed vexflow 4):
 Add extra_stroke_scale, y_spacing_scale
 Add extra_stroke_scale, y_spacing_scale
 
 
-timesignature.js (fixed vexflow 4):
-open group to get SVG group+class for key signature
+tuplet.js (vexflow 4: need to check if this option available):
+Add option tuplet.RenderTupletNumber
 
 
 Currently, we are using Vexflow 1.2.93, because of some formatter advantages
 Currently, we are using Vexflow 1.2.93, because of some formatter advantages
 compared to Vexflow 3.x versions, see this issue:
 compared to Vexflow 3.x versions, see this issue:

+ 368 - 0
src/VexFlowPatch/src/tuplet.js

@@ -0,0 +1,368 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+
+/**
+ * ## Description
+ *
+ * Create a new tuplet from the specified notes. The notes must
+ * be part of the same voice. If they are of different rhythmic
+ * values, then options.num_notes must be set.
+ *
+ * @constructor
+ * @param {Array.<Vex.Flow.StaveNote>} A set of notes: staveNotes,
+ *   notes, etc... any class that inherits stemmableNote at some
+ *   point in its prototype chain.
+ * @param options: object {
+ *
+ *   num_notes: fit this many notes into...
+ *   notes_occupied: ...the space of this many notes
+ *
+ *       Together, these two properties make up the tuplet ratio
+ *     in the form of num_notes : notes_occupied.
+ *       num_notes defaults to the number of notes passed in, so
+ *     it is important that if you omit this property, all of
+ *     the notes passed should be of the same note value.
+ *       notes_occupied defaults to 2 -- so you should almost
+ *     certainly pass this parameter for anything other than
+ *     a basic triplet.
+ *
+ *   location:
+ *     default 1, which is above the notes: ┌─── 3 ───┐
+ *      -1 is below the notes └─── 3 ───┘
+ *
+ *   bracketed: boolean, draw a bracket around the tuplet number
+ *     when true: ┌─── 3 ───┐   when false: 3
+ *     defaults to true if notes are not beamed, false otherwise
+ *
+ *   ratioed: boolean
+ *     when true: ┌─── 7:8 ───┐, when false: ┌─── 7 ───┐
+ *     defaults to true if the difference between num_notes and
+ *     notes_occupied is greater than 1.
+ *
+ *   y_offset: int, default 0
+ *     manually offset a tuplet, for instance to avoid collisions
+ *     with articulations, etc...
+ * }
+ */
+
+ import { Vex } from './vex';
+ import { Element } from './element';
+ import { Formatter } from './formatter';
+ import { Glyph } from './glyph';
+ import { Stem } from './stem';
+ 
+ export class Tuplet extends Element {
+   static get LOCATION_TOP() {
+     return 1;
+   }
+   static get LOCATION_BOTTOM() {
+     return -1;
+   }
+   static get NESTING_OFFSET() {
+     return 15;
+   }
+ 
+   constructor(notes, options) {
+     super();
+     this.setAttribute('type', 'Tuplet');
+     if (!notes || !notes.length) {
+       throw new Vex.RuntimeError('BadArguments', 'No notes provided for tuplet.');
+     }
+ 
+     this.options = Vex.Merge({}, options);
+     this.notes = notes;
+     this.num_notes = 'num_notes' in this.options ?
+       this.options.num_notes : notes.length;
+ 
+     // We accept beats_occupied, but warn that it's deprecated:
+     // the preferred property name is now notes_occupied.
+     if (this.options.beats_occupied) {
+       this.beatsOccupiedDeprecationWarning();
+     }
+     this.notes_occupied = this.options.notes_occupied ||
+       this.options.beats_occupied ||
+       2;
+     if ('bracketed' in this.options) {
+       this.bracketed = this.options.bracketed;
+     } else {
+       this.bracketed =
+         notes.some(note => note.beam === null);
+     }
+ 
+     this.ratioed = 'ratioed' in this.options ?
+       this.options.ratioed :
+       (Math.abs(this.notes_occupied - this.num_notes) > 1);
+     this.point = 28;
+     this.y_pos = 16;
+     this.x_pos = 100;
+     this.width = 200;
+     this.location = this.options.location || Tuplet.LOCATION_TOP;
+ 
+     Formatter.AlignRestsToNotes(notes, true, true);
+     this.resolveGlyphs();
+     this.attach();
+   }
+ 
+   attach() {
+     for (let i = 0; i < this.notes.length; i++) {
+       const note = this.notes[i];
+       note.setTuplet(this);
+     }
+   }
+ 
+   detach() {
+     for (let i = 0; i < this.notes.length; i++) {
+       const note = this.notes[i];
+       note.resetTuplet(this);
+     }
+   }
+ 
+   /**
+    * Set whether or not the bracket is drawn.
+    */
+   setBracketed(bracketed) {
+     this.bracketed = !!bracketed;
+     return this;
+   }
+ 
+   /**
+    * Set whether or not the ratio is shown.
+    */
+   setRatioed(ratioed) {
+     this.ratioed = !!ratioed;
+     return this;
+   }
+ 
+   /**
+    * Set the tuplet to be displayed either on the top or bottom of the stave
+    */
+   setTupletLocation(location) {
+     if (!location) {
+       location = Tuplet.LOCATION_TOP;
+     } else if (location !== Tuplet.LOCATION_TOP && location !== Tuplet.LOCATION_BOTTOM) {
+       throw new Vex.RERR('BadArgument', 'Invalid tuplet location: ' + location);
+     }
+ 
+     this.location = location;
+     return this;
+   }
+ 
+   getNotes() {
+     return this.notes;
+   }
+ 
+   getNoteCount() {
+     return this.num_notes;
+   }
+ 
+   beatsOccupiedDeprecationWarning() {
+     const msg = [
+       'beats_occupied has been deprecated as an ',
+       'option for tuplets. Please use notes_occupied ',
+       'instead. Calls to getBeatsOccupied and ',
+       'setBeatsOccupied should now be routed to ',
+       'getNotesOccupied and setNotesOccupied instead',
+     ].join('');
+ 
+     if (console && console.warn) { // eslint-disable-line no-console
+       console.warn(msg); // eslint-disable-line no-console
+     } else if (console) {
+       console.log(msg); // eslint-disable-line no-console
+     }
+   }
+ 
+   getBeatsOccupied() {
+     this.beatsOccupiedDeprecationWarning();
+     return this.getNotesOccupied();
+   }
+ 
+   setBeatsOccupied(beats) {
+     this.beatsOccupiedDeprecationWarning();
+     return this.setNotesOccupied(beats);
+   }
+ 
+   getNotesOccupied() {
+     return this.notes_occupied;
+   }
+ 
+   setNotesOccupied(notes) {
+     this.detach();
+     this.notes_occupied = notes;
+     this.resolveGlyphs();
+     this.attach();
+   }
+ 
+   resolveGlyphs() {
+     this.numerator_glyphs = [];
+     let n = this.num_notes;
+     while (n >= 1) {
+       this.numerator_glyphs.unshift(new Glyph('v' + (n % 10), this.point));
+       n = parseInt(n / 10, 10);
+     }
+ 
+     this.denom_glyphs = [];
+     n = this.notes_occupied;
+     while (n >= 1) {
+       this.denom_glyphs.unshift(new Glyph('v' + (n % 10), this.point));
+       n = parseInt(n / 10, 10);
+     }
+   }
+ 
+   // determine how many tuplets are nested within this tuplet
+   // on the same side (above/below), to calculate a y
+   // offset for this tuplet:
+   getNestedTupletCount() {
+     const location = this.location;
+     const first_note = this.notes[0];
+     let maxTupletCount = countTuplets(first_note, location);
+     let minTupletCount = countTuplets(first_note, location);
+ 
+     // Count the tuplets that are on the same side (above/below)
+     // as this tuplet:
+     function countTuplets(note, location) {
+       return note.tupletStack.filter(tuplet => tuplet.location === location).length;
+     }
+ 
+     this.notes.forEach(note => {
+       const tupletCount = countTuplets(note, location);
+       maxTupletCount = tupletCount > maxTupletCount ? tupletCount : maxTupletCount;
+       minTupletCount = tupletCount < minTupletCount ? tupletCount : minTupletCount;
+     });
+ 
+     return maxTupletCount - minTupletCount;
+   }
+ 
+   // determine the y position of the tuplet:
+   getYPosition() {
+     // offset the tuplet for any nested tuplets between
+     // it and the notes:
+     const nested_tuplet_y_offset =
+       this.getNestedTupletCount() *
+       Tuplet.NESTING_OFFSET *
+       -this.location;
+ 
+     // offset the tuplet for any manual y_offset:
+     const y_offset = this.options.y_offset || 0;
+ 
+     // now iterate through the notes and find our highest
+     // or lowest locations, to form a base y_pos
+     const first_note = this.notes[0];
+     let y_pos;
+     if (this.location === Tuplet.LOCATION_TOP) {
+       y_pos = first_note.getStave().getYForLine(0) - 15;
+       // y_pos = first_note.getStemExtents().topY - 10;
+ 
+       for (let i = 0; i < this.notes.length; ++i) {
+         const top_y = this.notes[i].getStemDirection() === Stem.UP
+           ? this.notes[i].getStemExtents().topY - 10
+           : this.notes[i].getStemExtents().baseY - 20;
+ 
+         if (top_y < y_pos) {
+           y_pos = top_y;
+         }
+       }
+     } else {
+       y_pos = first_note.getStave().getYForLine(4) + 20;
+ 
+       for (let i = 0; i < this.notes.length; ++i) {
+         const bottom_y = this.notes[i].getStemDirection() === Stem.UP
+           ? this.notes[i].getStemExtents().baseY + 20
+           : this.notes[i].getStemExtents().topY + 10;
+         if (bottom_y > y_pos) {
+           y_pos = bottom_y;
+         }
+       }
+     }
+ 
+     return y_pos + nested_tuplet_y_offset + y_offset;
+   }
+ 
+   draw() {
+     this.checkContext();
+     this.setRendered();
+ 
+     // determine x value of left bound of tuplet
+     const first_note = this.notes[0];
+     const last_note = this.notes[this.notes.length - 1];
+ 
+     if (!this.bracketed) {
+       this.x_pos = first_note.getStemX();
+       this.width = last_note.getStemX() - this.x_pos;
+     } else {
+       this.x_pos = first_note.getTieLeftX() - 5;
+       this.width = last_note.getTieRightX() - this.x_pos + 5;
+     }
+ 
+     // determine y value for tuplet
+     this.y_pos = this.getYPosition();
+ 
+     const addGlyphWidth = (width, glyph) => width + glyph.getMetrics().width;
+ 
+     // calculate total width of tuplet notation
+     let width = this.numerator_glyphs.reduce(addGlyphWidth, 0);
+     if (this.ratioed) {
+       width = this.denom_glyphs.reduce(addGlyphWidth, width);
+       width += this.point * 0.32;
+     }
+ 
+     const notation_center_x = this.x_pos + (this.width / 2);
+     const notation_start_x = notation_center_x - (width / 2);
+ 
+     // draw bracket if the tuplet is not beamed
+     if (this.bracketed) {
+       const line_width = this.width / 2 - width / 2 - 5;
+ 
+       // only draw the bracket if it has positive length
+       if (line_width > 0) {
+         this.context.fillRect(this.x_pos, this.y_pos, line_width, 1);
+         this.context.fillRect(
+           this.x_pos + this.width / 2 + width / 2 + 5,
+           this.y_pos,
+           line_width,
+           1
+         );
+         this.context.fillRect(
+           this.x_pos,
+           this.y_pos + (this.location === Tuplet.LOCATION_BOTTOM),
+           1,
+           this.location * 10
+         );
+         this.context.fillRect(
+           this.x_pos + this.width,
+           this.y_pos + (this.location === Tuplet.LOCATION_BOTTOM),
+           1,
+           this.location * 10
+         );
+       }
+     }
+ 
+     // VexFlowPatch: add option to not render tuplet numbers
+     if (this.RenderTupletNumber !== false) {
+         // draw numerator glyphs
+         let x_offset = 0;
+         this.numerator_glyphs.forEach(glyph => {
+           glyph.render(this.context, notation_start_x + x_offset, this.y_pos + (this.point / 3) - 2);
+           x_offset += glyph.getMetrics().width;
+         });
+     }
+ 
+     // display colon and denominator if the ratio is to be shown
+     if (this.ratioed) {
+       const colon_x = notation_start_x + x_offset + this.point * 0.16;
+       const colon_radius = this.point * 0.06;
+       this.context.beginPath();
+       this.context.arc(colon_x, this.y_pos - this.point * 0.08, colon_radius, 0, Math.PI * 2, true);
+       this.context.closePath();
+       this.context.fill();
+       this.context.beginPath();
+       this.context.arc(colon_x, this.y_pos + this.point * 0.12, colon_radius, 0, Math.PI * 2, true);
+       this.context.closePath();
+       this.context.fill();
+       x_offset += this.point * 0.32;
+       this.denom_glyphs.forEach(glyph => {
+         glyph.render(this.context, notation_start_x + x_offset, this.y_pos + (this.point / 3) - 2);
+         x_offset += glyph.getMetrics().width;
+       });
+     }
+   }
+ }
+ 

+ 111 - 0
test/data/test_soft-accent_cresc_decresc_single_note.musicxml

@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>Title</work-title>
+    </work>
+  <identification>
+    <creator type="composer">Composer</creator>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-08-23</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>6.99911</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1596.77</page-height>
+      <page-width>1233.87</page-width>
+      <page-margins type="even">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="616.935" default-y="1511.05" justify="center" valign="top" font-size="22">Title</credit-words>
+    </credit>
+  <credit page="1">
+    <credit-type>composer</credit-type>
+    <credit-words default-x="1148.15" default-y="1411.05" justify="right" valign="bottom">Composer</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="304.16">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>674.54</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <note default-x="80.72" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        <notations>
+          <articulations>
+            <soft-accent/>
+            </articulations>
+          </notations>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 536 - 0
test/data/test_tuplet_consecutive_3and5_tuplets.musicxml

@@ -0,0 +1,536 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test_tuplet_consecutive_3and5_tuplets</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-08-11</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1697.14</page-height>
+      <page-width>1200</page-width>
+      <page-margins type="even">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="600" default-y="1611.43" justify="center" valign="top" font-size="22">tuplet_consecutive_test_simple</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="426.54">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>65.90</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        <staff-layout number="2">
+          <staff-distance>65.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>15</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>3</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <staves>2</staves>
+        <clef number="1">
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        <clef number="2">
+          <sign>F</sign>
+          <line>4</line>
+          </clef>
+        </attributes>
+      <note default-x="83.49" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>45</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>45</duration>
+        </backup>
+      <note default-x="83.49" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>5</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="126.41" default-y="-135.00">
+        <pitch>
+          <step>B</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>5</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="169.33" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>5</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="212.25" default-y="-125.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>5</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="255.17" default-y="-120.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>5</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="298.09" default-y="-125.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>5</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="341.00" default-y="-120.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>15</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        </note>
+      </measure>
+    <measure number="2" width="536.13">
+      <note default-x="13.00" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>45</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>45</duration>
+        </backup>
+      <note default-x="13.00" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <beam number="2">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="47.17" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="81.33" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="115.50" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="149.66" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="183.83" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <accidental>sharp</accidental>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <beam number="2">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="217.99" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="252.16" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="286.32" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="320.49" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="354.65" default-y="-105.00">
+        <pitch>
+          <step>A</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <accidental>sharp</accidental>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <beam number="2">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="388.82" default-y="-105.00">
+        <pitch>
+          <step>A</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="422.98" default-y="-105.00">
+        <pitch>
+          <step>A</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="457.15" default-y="-105.00">
+        <pitch>
+          <step>A</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="491.31" default-y="-105.00">
+        <pitch>
+          <step>A</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <time-modification>
+          <actual-notes>5</actual-notes>
+          <normal-notes>4</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 300 - 0
test/data/test_tuplet_consecutive_simple.musicxml

@@ -0,0 +1,300 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test_tuplet_consecutive_simple</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-08-11</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1697.14</page-height>
+      <page-width>1200</page-width>
+      <page-margins type="even">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="600" default-y="1611.43" justify="center" valign="top" font-size="22">tuplet_consecutive_test_simple</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="575.97">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>65.90</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        <staff-layout number="2">
+          <staff-distance>65.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>3</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>3</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <staves>2</staves>
+        <clef number="1">
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        <clef number="2">
+          <sign>F</sign>
+          <line>4</line>
+          </clef>
+        </attributes>
+      <note default-x="83.49" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>9</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>9</duration>
+        </backup>
+      <note default-x="83.49" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="138.01" default-y="-135.00">
+        <pitch>
+          <step>B</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="192.53" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="247.05" default-y="-125.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="301.57" default-y="-120.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="356.09" default-y="-125.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="410.61" default-y="-120.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="465.13" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="519.65" default-y="-120.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      </measure>
+    <measure number="2" width="386.70">
+      <note default-x="13.00" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>9</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>9</duration>
+        </backup>
+      <note>
+        <rest measure="yes"/>
+        <duration>9</duration>
+        <voice>5</voice>
+        <staff>2</staff>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 319 - 0
test/data/test_tuplet_consecutive_simple_alwaysdisable.musicxml

@@ -0,0 +1,319 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test_tuplet_consecutive_simple_alwaysdisable</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-08-11</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1697.14</page-height>
+      <page-width>1200</page-width>
+      <page-margins type="even">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="600" default-y="1611.43" justify="center" valign="top" font-size="22">tuplet_consecutive_test_simple</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="529.40">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>65.90</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        <staff-layout number="2">
+          <staff-distance>65.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>3</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>3</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <staves>2</staves>
+        <clef number="1">
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        <clef number="2">
+          <sign>F</sign>
+          <line>4</line>
+          </clef>
+        </attributes>
+      <note default-x="83.49" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>9</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>9</duration>
+        </backup>
+      <note default-x="83.49" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="139.34" default-y="-135.00">
+        <pitch>
+          <step>B</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="195.20" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="251.06" default-y="-125.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="306.91" default-y="-120.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="362.77" default-y="-125.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note default-x="418.63" default-y="-120.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        </note>
+      </measure>
+    <measure number="2" width="433.27">
+      <note default-x="13.00" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>9</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>9</duration>
+        </backup>
+      <note default-x="13.00" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet type="start" bracket="no"/>
+          </notations>
+        </note>
+      <note default-x="72.35" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="131.70" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+          </time-modification>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet type="stop"/>
+          </notations>
+        </note>
+      <note>
+        <rest/>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>3</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>