Pārlūkot izejas kodu

merge osmd-public (1.0.0) into osmd-extended

sschmid 4 gadi atpakaļ
vecāks
revīzija
6c21947dfd

+ 3 - 0
demo/index.js

@@ -59,6 +59,7 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
             "OSMD Function Test - Auto Multirest Measures Single Staff": "Test_Auto_Multirest_1.musicxml",
             "OSMD Function Test - Auto Multirest Measures Multiple Staves": "Test_Auto_Multirest_2.musicxml",
             "OSMD Function Test - String number collisions": "test_string_number_collisions.musicxml",
+            "OSMD Function Test - Repeat Stave Connectors": "OSMD_function_Test_Repeat.musicxml",
             "Schubert, F. - An Die Musik": "Schubert_An_die_Musik.xml",
             "Actor, L. - Prelude (Large Sample, loading time)": "ActorPreludeSample.xml",
             "Actor, L. - Prelude (Large, No Print Part Names)": "ActorPreludeSample_PartName.xml",
@@ -545,6 +546,7 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
                 rerender();
             }
         }
+
         playbackControl = demoPlaybackControl(openSheetMusicDisplay);
         playbackControlsButton.addEventListener("click", function(){
             if(!playbackControl.IsClosed()){
@@ -649,6 +651,7 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
                 // This gives you access to the osmd object in the console. Do not use in production code
                 window.osmd = openSheetMusicDisplay;
                 openSheetMusicDisplay.zoom = zoom;
+                //openSheetMusicDisplay.Sheet.Transpose = 3; // try transposing between load and first render if you have transpose issues with F# etc
                 return openSheetMusicDisplay.render();
             },
             function (e) {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "opensheetmusicdisplay-private",
-  "version": "0.9.5",
+  "version": "1.0.0",
   "description": "Private OSMD mirror/audio player.",
   "main": "build/opensheetmusicdisplay.min.js",
   "typings": "build/dist/src/",

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

@@ -102,6 +102,7 @@ export class EngravingRules {
     public WedgeVerticalMargin: number;
     public DistanceOffsetBetweenTwoHorizontallyCrossedWedges: number;
     public WedgeMinLength: number;
+    public WedgeEndDistanceBetweenTimestampsFactor: number;
     public DistanceBetweenAdjacentDynamics: number;
     public TempoChangeMeasureValidity: number;
     public TempoContinousFactor: number;
@@ -424,6 +425,7 @@ export class EngravingRules {
         this.WedgeVerticalMargin = 0.5;
         this.DistanceOffsetBetweenTwoHorizontallyCrossedWedges = 0.3;
         this.WedgeMinLength = 2.0;
+        this.WedgeEndDistanceBetweenTimestampsFactor = 1.75;
         this.DistanceBetweenAdjacentDynamics = 0.75;
 
         // Tempo Variables

+ 2 - 0
src/MusicalScore/Graphical/GraphicalStaffEntry.ts

@@ -17,6 +17,7 @@ import {GraphicalStaffEntryLink} from "./GraphicalStaffEntryLink";
 import {CollectionUtil} from "../../Util/CollectionUtil";
 import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
 import { MusicSheetCalculator } from "./MusicSheetCalculator";
+import { Tie } from "../VoiceData/Tie";
 
 /**
  * The graphical counterpart of a [[SourceStaffEntry]].
@@ -53,6 +54,7 @@ export abstract class GraphicalStaffEntry extends GraphicalObject {
     public MaxAccidentals: number = 0;
 
     private graphicalInstructions: AbstractGraphicalInstruction[] = [];
+    public ties: Tie[] = [];
     private graphicalTies: GraphicalTie[] = [];
     private lyricsEntries: GraphicalLyricEntry[] = [];
 

+ 63 - 2
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -1058,10 +1058,35 @@ export abstract class MusicSheetCalculator {
         }
 
         const endAbsoluteTimestamp: Fraction = Fraction.createFromFraction(graphicalContinuousDynamic.ContinuousDynamic.EndMultiExpression.AbsoluteTimestamp);
-
+        const container: VerticalGraphicalStaffEntryContainer = this.graphicalMusicSheet.GetVerticalContainerFromTimestamp(endAbsoluteTimestamp);
+        const parentMeasure: GraphicalMeasure = container.getFirstNonNullStaffEntry().parentMeasure;
+        const endOfMeasure: number = parentMeasure.PositionAndShape.AbsolutePosition.x + parentMeasure.PositionAndShape.BorderRight;
+        let maxNoteLength: Fraction = new Fraction(0, 0, 0);
+        for (const staffEntry of container.StaffEntries) {
+            const currentMaxLength: Fraction = staffEntry?.sourceStaffEntry?.calculateMaxNoteLength();
+            if ( currentMaxLength?.gt(maxNoteLength) ) {
+                maxNoteLength = currentMaxLength;
+            }
+        }
         const endPosInStaffLine: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
             endAbsoluteTimestamp, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0);
 
+        const beginOfNextNote: Fraction = Fraction.plus(endAbsoluteTimestamp, maxNoteLength);
+        const nextNotePosInStaffLine: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
+            beginOfNextNote, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0);
+        //If the next note position is not on the next staffline
+        //extend close to the next note
+        if (nextNotePosInStaffLine.x > endPosInStaffLine.x && nextNotePosInStaffLine.x < endOfMeasure) {
+            endPosInStaffLine.x += (nextNotePosInStaffLine.x - endPosInStaffLine.x) / this.rules.WedgeEndDistanceBetweenTimestampsFactor;
+        } else { //Otherwise extend to the end of the measure
+            endPosInStaffLine.x = endOfMeasure - this.rules.WedgeHorizontalMargin;
+        }
+
+        const startCollideBox: BoundingBox =
+            this.dynamicExpressionMap.get(graphicalContinuousDynamic.ContinuousDynamic.StartMultiExpression.AbsoluteTimestamp.RealValue);
+        if (startCollideBox) {
+            startPosInStaffline.x = startCollideBox.RelativePosition.x + startCollideBox.BorderMarginRight + this.rules.WedgeHorizontalMargin;
+        }
         //currentMusicSystem and currentStaffLine
         const musicSystem: MusicSystem = staffLine.ParentMusicSystem;
         const currentStaffLineIndex: number = musicSystem.StaffLines.indexOf(staffLine);
@@ -1309,6 +1334,7 @@ export abstract class MusicSheetCalculator {
                 secondGraphicalContinuousDynamic.calcPsi();
             }
         } //End Diminuendo
+        this.dynamicExpressionMap.set(endAbsoluteTimestamp.RealValue, graphicalContinuousDynamic.PositionAndShape);
     }
 
     /**
@@ -1317,12 +1343,13 @@ export abstract class MusicSheetCalculator {
      * @param startPosInStaffline Starting point in staff line
      */
     protected calculateGraphicalInstantaneousDynamicExpression(graphicalInstantaneousDynamic: GraphicalInstantaneousDynamicExpression,
-                                                               startPosInStaffline: PointF2D): void {
+                                                               startPosInStaffline: PointF2D, timestamp: Fraction): void {
         // get Margin Dimensions
         const staffLine: StaffLine = graphicalInstantaneousDynamic.ParentStaffLine;
         if (!staffLine) {
             return; // TODO can happen when drawing range modified (osmd.setOptions({drawFromMeasureNumber...}))
         }
+
         const left: number = startPosInStaffline.x + graphicalInstantaneousDynamic.PositionAndShape.BorderMarginLeft;
         const right: number = startPosInStaffline.x + graphicalInstantaneousDynamic.PositionAndShape.BorderMarginRight;
         const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
@@ -2030,6 +2057,7 @@ export abstract class MusicSheetCalculator {
                                 }
                             }
                         }
+                        this.setTieDirections(startStaffEntry);
                     }
                 }
             }
@@ -2041,6 +2069,8 @@ export abstract class MusicSheetCalculator {
             // console.log('tie not found in measure number ' + measureIndex - 1);
             return;
         }
+        startGraphicalStaffEntry.ties.push(tie);
+
         let startGse: GraphicalStaffEntry = startGraphicalStaffEntry;
         let startNote: GraphicalNote = undefined;
         let endGse: GraphicalStaffEntry = undefined;
@@ -2066,6 +2096,31 @@ export abstract class MusicSheetCalculator {
         }
     }
 
+    private setTieDirections(staffEntry: GraphicalStaffEntry): void {
+        if (!staffEntry) {
+            return;
+        }
+        const ties: Tie[] = staffEntry.ties;
+        if (ties.length > 1) {
+            let highestNote: Note = undefined;
+            for (const gseTie of ties) {
+                const tieNote: Note = gseTie.Notes[0];
+                if (!highestNote || tieNote.Pitch.getHalfTone() > highestNote.Pitch.getHalfTone()) {
+                    highestNote = tieNote;
+                }
+            }
+            for (const gseTie of ties) {
+                if (gseTie.TieDirection === PlacementEnum.NotYetDefined) { // only set/change if not already set by xml
+                    if (gseTie.Notes[0] === highestNote) {
+                        gseTie.TieDirection = PlacementEnum.Above;
+                    } else {
+                        gseTie.TieDirection = PlacementEnum.Below;
+                    }
+                }
+            }
+        }
+    }
+
     private createAccidentalCalculators(): AccidentalCalculator[] {
         const accidentalCalculators: AccidentalCalculator[] = [];
         const firstSourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.getFirstSourceMeasure();
@@ -2842,11 +2897,16 @@ export abstract class MusicSheetCalculator {
         return (rightDash.PositionAndShape.RelativePosition.x - leftDash.PositionAndShape.RelativePosition.x);
     }
 
+    //So we can track shared notes bounding boxes to avoid collision + skyline issues
+    protected dynamicExpressionMap: Map<number, BoundingBox> = new Map<number, BoundingBox>();
+
     private calculateDynamicExpressions(): void {
         const maxIndex: number = Math.min(this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length - 1, this.rules.MaxMeasureToDrawIndex);
         const minIndex: number = Math.min(this.rules.MinMeasureToDrawIndex, this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length);
         for (let i: number = minIndex; i <= maxIndex; i++) {
             const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
+            //Reset, beginning of new measure
+            this.dynamicExpressionMap.clear();
             for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
                 if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
                     continue;
@@ -2865,6 +2925,7 @@ export abstract class MusicSheetCalculator {
                 }
             }
         }
+        this.dynamicExpressionMap.clear();
     }
 
     private calculateOctaveShifts(): void {

+ 1 - 1
src/MusicalScore/Graphical/MusicSystemBuilder.ts

@@ -534,7 +534,7 @@ export class MusicSystemBuilder {
             }
             if (printRhythm) {
                 measure.addRhythmAtBegin(currentRhythm);
-                measure.parentSourceMeasure.RhythmPrinted = true;
+                measure.parentSourceMeasure.RhythmPrinted = currentRhythm;
                 rhythmAdded = true;
             }
         }

+ 1 - 1
src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts

@@ -876,7 +876,7 @@ export class VexFlowConverter {
                 }
                 return Vex.Flow.StaveConnector.type.SINGLE_RIGHT;
             case SystemLinesEnum.DoubleThin:
-                return Vex.Flow.StaveConnector.type.DOUBLE;
+                return Vex.Flow.StaveConnector.type.THIN_DOUBLE;
             case SystemLinesEnum.ThinBold:
                 return Vex.Flow.StaveConnector.type.BOLD_DOUBLE_RIGHT;
             case SystemLinesEnum.BoldThinDots:

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

@@ -652,6 +652,8 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
           });
           if (tie.Tie.TieDirection === PlacementEnum.Below) {
             vfTie.setDirection(1); // + is down in vexflow
+          } else if (tie.Tie.TieDirection === PlacementEnum.Above) {
+            vfTie.setDirection(-1);
           }
         }
 
@@ -667,7 +669,6 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
       // we do already use the min/max in MusicSheetCalculator.calculateDynamicsExpressions,
       // but this may be necessary for StaffLinkedExpressions, not tested.
     }
-
     // calculate absolute Timestamp
     const absoluteTimestamp: Fraction = multiExpression.AbsoluteTimestamp;
     const measures: GraphicalMeasure[] = this.graphicalMusicSheet.MeasureList[measureIndex];
@@ -694,7 +695,8 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         multiExpression.InstantaneousDynamic,
         staffLine,
         startMeasure);
-      this.calculateGraphicalInstantaneousDynamicExpression(graphicalInstantaneousDynamic, dynamicStartPosition);
+      this.calculateGraphicalInstantaneousDynamicExpression(graphicalInstantaneousDynamic, dynamicStartPosition, absoluteTimestamp);
+      this.dynamicExpressionMap.set(absoluteTimestamp.RealValue, graphicalInstantaneousDynamic.PositionAndShape);
     }
     if (multiExpression.StartingContinuousDynamic) {
       const continuousDynamic: ContinuousDynamicExpression = multiExpression.StartingContinuousDynamic;
@@ -707,6 +709,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
       if (!graphicalContinuousDynamic.IsVerbal && continuousDynamic.EndMultiExpression) {
         try {
         this.calculateGraphicalContinuousDynamic(graphicalContinuousDynamic, dynamicStartPosition);
+        graphicalContinuousDynamic.updateSkyBottomLine();
         } catch (e) {
           // TODO this sometimes fails when the measure range to draw doesn't include all the dynamic's measures, method needs to be adjusted
           //   see calculateGraphicalContinuousDynamic(), also in MusicSheetCalculator.

+ 10 - 1
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSystem.ts

@@ -60,10 +60,19 @@ export class VexFlowMusicSystem extends MusicSystem {
 
         if (bottomMeasure) {
             renderInitialLine = true;
-            // ToDo: feature/Repetitions
             // create here the correct lines according to the given lineType.
             (bottomMeasure as VexFlowMeasure).lineTo(topMeasure as VexFlowMeasure, VexFlowConverter.line(lineType, linePosition));
             (bottomMeasure as VexFlowMeasure).addMeasureLine(lineType, linePosition);
+            //Double repeat. VF doesn't have concept of double repeat. Need to add stave connector to begin of next measure
+            if (lineType === SystemLinesEnum.DotsBoldBoldDots) {
+                const nextIndex: number = bottomMeasure.ParentStaffLine.Measures.indexOf(bottomMeasure) + 1;
+                const nextBottomMeasure: VexFlowMeasure = bottomMeasure.ParentStaffLine.Measures[nextIndex] as VexFlowMeasure;
+                const nextTopMeasure: VexFlowMeasure = topMeasure.ParentStaffLine.Measures[nextIndex] as VexFlowMeasure;
+                if (nextBottomMeasure && nextTopMeasure) {
+                    nextBottomMeasure.lineTo(nextTopMeasure, VexFlowConverter.line(SystemLinesEnum.BoldThinDots, linePosition));
+                    nextBottomMeasure.addMeasureLine(SystemLinesEnum.BoldThinDots, linePosition);
+                }
+            }
         }
         if (vfTopMeasure) {
             vfTopMeasure.addMeasureLine(lineType, linePosition, renderInitialLine);

+ 1 - 1
src/MusicalScore/ScoreIO/InstrumentReader.ts

@@ -535,7 +535,7 @@ export class InstrumentReader {
              expressionReader.readExpressionParameters(
                xmlNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, false
              );
-             expressionReader.read(xmlNode, this.currentMeasure, currentFraction);
+             expressionReader.read(xmlNode, this.currentMeasure, currentFraction, previousFraction.clone());
            }
           }
         } else if (xmlNode.name === "barline") {

+ 10 - 3
src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts

@@ -140,7 +140,8 @@ export class ExpressionReader {
                  } else { this.placement = PlacementEnum.Below; }
         }
     }
-    public read(directionNode: IXmlElement, currentMeasure: SourceMeasure, inSourceMeasureCurrentFraction: Fraction): void {
+    public read(directionNode: IXmlElement, currentMeasure: SourceMeasure,
+                inSourceMeasureCurrentFraction: Fraction, inSourceMeasurePreviousFraction: Fraction = undefined): void {
         let isTempoInstruction: boolean = false;
         let isDynamicInstruction: boolean = false;
         const n: IXmlElement = directionNode.element("sound");
@@ -226,7 +227,7 @@ export class ExpressionReader {
 
         dirContentNode = dirNode.element("wedge");
         if (dirContentNode) {
-            this.interpretWedge(dirContentNode, currentMeasure, inSourceMeasureCurrentFraction, currentMeasure.MeasureNumber);
+            this.interpretWedge(dirContentNode, currentMeasure, inSourceMeasurePreviousFraction, currentMeasure.MeasureNumber);
             return;
         }
 
@@ -395,7 +396,13 @@ export class ExpressionReader {
         if (wedgeNode !== undefined && wedgeNode.hasAttributes && wedgeNode.attribute("default-x")) {
             this.directionTimestamp = Fraction.createFromFraction(inSourceMeasureCurrentFraction);
         }
-        this.createNewMultiExpressionIfNeeded(currentMeasure);
+        //Ending needs to use previous fraction, not current.
+        //If current is used, when there is a system break it will mess up
+        if (wedgeNode?.attribute("type")?.value?.toLowerCase() === "stop") {
+            this.createNewMultiExpressionIfNeeded(currentMeasure, inSourceMeasureCurrentFraction);
+        } else {
+            this.createNewMultiExpressionIfNeeded(currentMeasure);
+        }
         this.addWedge(wedgeNode, currentMeasure);
         this.initialize();
     }

+ 107 - 30
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -712,8 +712,7 @@ export class VoiceGenerator {
           if (bracketAttr && bracketAttr.value === "yes") {
             bracketed = true;
           }
-          const placementAttr: Attr = tupletNode.attribute("placement");
-          const placementBelow: boolean = placementAttr && placementAttr.value === "below";
+
           const type: Attr = tupletNode.attribute("type");
           if (type && type.value === "start") {
             let tupletNumber: number = 1;
@@ -733,7 +732,17 @@ export class VoiceGenerator {
 
             }
             const tuplet: Tuplet = new Tuplet(tupletLabelNumber, bracketed);
-            tuplet.tupletLabelNumberPlacement = placementBelow ? PlacementEnum.Below : PlacementEnum.Above;
+            //Default to above
+            tuplet.tupletLabelNumberPlacement = PlacementEnum.Above;
+            //If we ever encounter a placement attribute for this tuplet, should override.
+            //Even previous placement attributes for the tuplet
+            const placementAttr: Attr = tupletNode.attribute("placement");
+            if (placementAttr) {
+              if (placementAttr.value === "below") {
+                tuplet.tupletLabelNumberPlacement = PlacementEnum.Below;
+              }
+              tuplet.PlacementFromXml = true;
+            }
             if (this.tupletDict[tupletNumber]) {
               delete this.tupletDict[tupletNumber];
               if (Object.keys(this.tupletDict).length === 0) {
@@ -756,9 +765,39 @@ export class VoiceGenerator {
             }
             const tuplet: Tuplet = this.tupletDict[tupletNumber];
             if (tuplet) {
+              const placementAttr: Attr = tupletNode.attribute("placement");
+              if (placementAttr) {
+                if (placementAttr.value === "below") {
+                  tuplet.tupletLabelNumberPlacement = PlacementEnum.Below;
+                }  else {
+                  tuplet.tupletLabelNumberPlacement = PlacementEnum.Above;
+                }
+                tuplet.PlacementFromXml = true;
+              }
               const subnotelist: Note[] = [];
               subnotelist.push(this.currentNote);
               tuplet.Notes.push(subnotelist);
+              //If our placement hasn't been from XML, check all the notes in the tuplet
+              //Search for the first non-rest and use it's stem direction
+              if (!tuplet.PlacementFromXml) {
+                let foundNonRest: boolean = false;
+                for (const subList of tuplet.Notes) {
+                  for (const note of subList) {
+                    if (!note.isRest()) {
+                      if(note.StemDirectionXml === StemDirectionType.Down) {
+                        tuplet.tupletLabelNumberPlacement = PlacementEnum.Below;
+                      } else {
+                        tuplet.tupletLabelNumberPlacement = PlacementEnum.Above;
+                      }
+                      foundNonRest = true;
+                      break;
+                    }
+                  }
+                  if (foundNonRest) {
+                    break;
+                  }
+                }
+              }
               tuplet.Fractions.push(this.getTupletNoteDurationFromType(node));
               this.currentNote.NoteTuplet = tuplet;
               delete this.tupletDict[tupletNumber];
@@ -785,9 +824,6 @@ export class VoiceGenerator {
         if (bracketAttr && bracketAttr.value === "yes") {
           bracketed = true;
         }
-        const placementAttr: Attr = n.attribute("placement");
-        const placementBelow: boolean = placementAttr && placementAttr.value === "below";
-
         if (type === "start") {
           let tupletLabelNumber: number = 0;
           let timeModNode: IXmlElement = node.element("time-modification");
@@ -812,7 +848,20 @@ export class VoiceGenerator {
           let tuplet: Tuplet = this.tupletDict[tupletnumber];
           if (!tuplet) {
             tuplet = this.tupletDict[tupletnumber] = new Tuplet(tupletLabelNumber, bracketed);
-            tuplet.tupletLabelNumberPlacement = placementBelow ? PlacementEnum.Below : PlacementEnum.Above;
+            //Default to above
+            tuplet.tupletLabelNumberPlacement = PlacementEnum.Above;
+          }
+          //If we ever encounter a placement attribute for this tuplet, should override.
+          //Even previous placement attributes for the tuplet
+          const placementAttr: Attr = n.attribute("placement");
+          if (placementAttr) {
+            if (placementAttr.value === "below") {
+              tuplet.tupletLabelNumberPlacement = PlacementEnum.Below;
+            } else {
+              //Just in case
+              tuplet.tupletLabelNumberPlacement = PlacementEnum.Above;
+            }
+            tuplet.PlacementFromXml = true;
           }
           const subnotelist: Note[] = [];
           subnotelist.push(this.currentNote);
@@ -826,9 +875,39 @@ export class VoiceGenerator {
           }
           const tuplet: Tuplet = this.tupletDict[this.openTupletNumber];
           if (tuplet) {
+            const placementAttr: Attr = n.attribute("placement");
+            if (placementAttr) {
+              if (placementAttr.value === "below") {
+                tuplet.tupletLabelNumberPlacement = PlacementEnum.Below;
+              } else {
+                tuplet.tupletLabelNumberPlacement = PlacementEnum.Above;
+              }
+              tuplet.PlacementFromXml = true;
+            }
             const subnotelist: Note[] = [];
             subnotelist.push(this.currentNote);
             tuplet.Notes.push(subnotelist);
+            //If our placement hasn't been from XML, check all the notes in the tuplet
+            //Search for the first non-rest and use it's stem direction
+            if (!tuplet.PlacementFromXml) {
+              let foundNonRest: boolean = false;
+              for (const subList of tuplet.Notes) {
+                for (const note of subList) {
+                  if (!note.isRest()) {
+                    if(note.StemDirectionXml === StemDirectionType.Down) {
+                      tuplet.tupletLabelNumberPlacement = PlacementEnum.Below;
+                    } else {
+                      tuplet.tupletLabelNumberPlacement = PlacementEnum.Above;
+                    }
+                    foundNonRest = true;
+                    break;
+                  }
+                }
+                if (foundNonRest) {
+                  break;
+                }
+              }
+            }
             tuplet.Fractions.push(this.getTupletNoteDurationFromType(node));
             this.currentNote.NoteTuplet = tuplet;
             if (Object.keys(this.tupletDict).length === 0) {
@@ -889,6 +968,26 @@ export class VoiceGenerator {
       if (tieNodeList.length === 1) {
         const tieNode: IXmlElement = tieNodeList[0];
         if (tieNode !== undefined && tieNode.attributes()) {
+          let tieDirection: PlacementEnum = PlacementEnum.NotYetDefined;
+          // read tie direction/placement from XML
+          const placementAttr: IXmlAttribute = tieNode.attribute("placement");
+          if (placementAttr) {
+            if (placementAttr.value === "above") {
+              tieDirection = PlacementEnum.Above;
+            } else if (placementAttr.value === "below") {
+              tieDirection = PlacementEnum.Below;
+            }
+          }
+          // tie direction also be given like this:
+          const orientationAttr: IXmlAttribute = tieNode.attribute("orientation");
+          if (orientationAttr) {
+            if (orientationAttr.value === "over") {
+              tieDirection = PlacementEnum.Above;
+            } else if (orientationAttr.value === "under") {
+              tieDirection = PlacementEnum.Below;
+            }
+          }
+
           const type: string = tieNode.attribute("type").value;
           try {
             if (type === "start") {
@@ -900,7 +999,7 @@ export class VoiceGenerator {
               const tie: Tie = new Tie(this.currentNote, tieType);
               this.openTieDict[newTieNumber] = tie;
               tie.TieNumber = newTieNumber;
-              this.setTieDirections();
+              tie.TieDirection = tieDirection;
             } else if (type === "stop") {
               const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
               const tie: Tie = this.openTieDict[tieNumber];
@@ -925,28 +1024,6 @@ export class VoiceGenerator {
     }
   }
 
-  // TODO do same for slurs, optimize.
-  /** Sets the directions of open ties: up for the top one, down for the others. */
-  private setTieDirections(): void {
-    const tieKeys: string[] = Object.keys(this.openTieDict);
-    let highestNote: Note = undefined;
-    for (const tieKey of tieKeys) {
-      const tie: Tie = this.openTieDict[tieKey];
-      const tieNote: Note = tie.Notes[0];
-      if (!highestNote || tieNote.Pitch.OperatorFundamentalGreaterThan(highestNote.Pitch)) {
-        highestNote = tieNote;
-      }
-    }
-    for (const tieKey of tieKeys) {
-      const tie: Tie = this.openTieDict[tieKey];
-      if (tie.Notes[0] === highestNote) {
-        tie.TieDirection = PlacementEnum.Above;
-      } else {
-        tie.TieDirection = PlacementEnum.Below;
-      }
-    }
-  }
-
   /**
    * Find the next free int (starting from 0) to use as key in TieDict.
    * @returns {number}

+ 2 - 1
src/MusicalScore/VoiceData/SourceMeasure.ts

@@ -16,6 +16,7 @@ import {Repetition} from "../MusicSource/Repetition";
 import {SystemLinesEnum} from "../Graphical/SystemLinesEnum";
 import {EngravingRules} from "../Graphical/EngravingRules";
 import {GraphicalMeasure} from "../Graphical/GraphicalMeasure";
+import { RhythmInstruction } from "./Instructions";
 //import {BaseIdClass} from "../../Util/BaseIdClass"; // SourceMeasure originally extended BaseIdClass, but ids weren't used.
 
 /**
@@ -63,7 +64,7 @@ export class SourceMeasure {
     private measureNumber: number;
     public MeasureNumberXML: number;
     public MeasureNumberPrinted: number; // measureNumber if MeasureNumberXML undefined or NaN. Set in getPrintedMeasureNumber()
-    public RhythmPrinted: boolean = false; // whether this measure prints a rhythm on the score
+    public RhythmPrinted: RhythmInstruction; // the rhythm printed (rendered) in this measure
     public multipleRestMeasures: number; // usually undefined (0), unless "multiple-rest" given in XML (e.g. 4 measure rest)
     // public multipleRestMeasuresPerStaff: Dictionary<number, number>; // key: staffId. value: how many rest measures
     private absoluteTimestamp: Fraction;

+ 1 - 1
src/MusicalScore/VoiceData/Tie.ts

@@ -17,7 +17,7 @@ export class Tie {
     private notes: Note[] = [];
     private type: TieTypes;
     public TieNumber: number = 1;
-    public TieDirection: PlacementEnum = PlacementEnum.Above;
+    public TieDirection: PlacementEnum = PlacementEnum.NotYetDefined;
 
     public get Notes(): Note[] {
         return this.notes;

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

@@ -13,6 +13,7 @@ export class Tuplet {
     }
 
     private tupletLabelNumber: number;
+    public PlacementFromXml: boolean = false;
     public tupletLabelNumberPlacement: PlacementEnum;
     /** 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[][].

+ 71 - 17
src/OpenSheetMusicDisplay/Cursor.ts

@@ -13,6 +13,8 @@ import { SourceMeasure } from "../MusicalScore/VoiceData/SourceMeasure";
 import { StaffLine } from "../MusicalScore/Graphical/StaffLine";
 import { GraphicalMeasure } from "../MusicalScore/Graphical/GraphicalMeasure";
 import { VexFlowMeasure } from "../MusicalScore/Graphical/VexFlow/VexFlowMeasure";
+import { CursorOptions } from "./OSMDOptions";
+import { BoundingBox } from "../MusicalScore";
 import { IPlaybackListener } from "../Common/Interfaces/IPlaybackListener";
 import { CursorPosChangedData } from "../Common/DataObjects/CursorPosChangedData";
 import { PointF2D } from "../Common/DataObjects";
@@ -21,10 +23,11 @@ import { PointF2D } from "../Common/DataObjects";
  * A cursor which can iterate through the music sheet.
  */
 export class Cursor implements IPlaybackListener {
-  constructor(container: HTMLElement, openSheetMusicDisplay: OpenSheetMusicDisplay) {
+  constructor(container: HTMLElement, openSheetMusicDisplay: OpenSheetMusicDisplay, cursorOptions: CursorOptions) {
     this.container = container;
     this.openSheetMusicDisplay = openSheetMusicDisplay;
     this.rules = this.openSheetMusicDisplay.EngravingRules;
+    this.cursorOptions = cursorOptions;
 
     // set cursor id
     // TODO add this for the OSMD object as well and refactor this into a util method?
@@ -39,7 +42,11 @@ export class Cursor implements IPlaybackListener {
     const curs: HTMLElement = document.createElement("img");
     curs.id = this.cursorElementId;
     curs.style.position = "absolute";
-    curs.style.zIndex = "-1";
+    if (this.cursorOptions.follow === true) {
+      curs.style.zIndex = "-1";
+    } else {
+      curs.style.zIndex = "-2";
+    }
     this.cursorElement = <HTMLImageElement>curs;
     this.container.appendChild(curs);
   }
@@ -82,6 +89,7 @@ export class Cursor implements IPlaybackListener {
   private graphic: GraphicalMusicSheet;
   public hidden: boolean = false;
   public currentPageNumber: number = 1;
+  private cursorOptions: CursorOptions;
 
   /** Initialize the cursor. Necessary before using functions like show() and next(). */
   public init(manager: MusicPartManager, graphic: GraphicalMusicSheet): void {
@@ -220,17 +228,53 @@ export class Cursor implements IPlaybackListener {
 
     // This the current HTML Cursor:
     const cursorElement: HTMLImageElement = this.cursorElement;
-    cursorElement.style.top = (y * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
-    cursorElement.style.left = ((x - 1.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
-    cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
-    const newWidth: number = 3 * 10.0 * this.openSheetMusicDisplay.zoom;
+
+    let newWidth: number = 0;
+    const meassurePositionAndShape: BoundingBox = this.graphic.findGraphicalMeasure(iterator.CurrentMeasureIndex, 0).PositionAndShape;
+    switch (this.cursorOptions.type) {
+      case 1:
+        cursorElement.style.top = (y * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
+        cursorElement.style.left = ((x - 1.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
+        cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
+        newWidth = 5 * this.openSheetMusicDisplay.zoom;
+        break;
+      case 2:
+        cursorElement.style.top = ((y-2.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
+        cursorElement.style.left = (x * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
+        cursorElement.height = (1.5 * 10.0 * this.openSheetMusicDisplay.zoom);
+        newWidth = 5 * this.openSheetMusicDisplay.zoom;
+        break;
+      case 3:
+        cursorElement.style.top = meassurePositionAndShape.AbsolutePosition.y * 10.0 * this.openSheetMusicDisplay.zoom +"px";
+        cursorElement.style.left = meassurePositionAndShape.AbsolutePosition.x * 10.0 * this.openSheetMusicDisplay.zoom +"px";
+        cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
+        newWidth = meassurePositionAndShape.Size.width * 10 * this.openSheetMusicDisplay.zoom;
+        break;
+      case 4:
+        cursorElement.style.top = meassurePositionAndShape.AbsolutePosition.y * 10.0 * this.openSheetMusicDisplay.zoom +"px";
+        cursorElement.style.left = meassurePositionAndShape.AbsolutePosition.x * 10.0 * this.openSheetMusicDisplay.zoom +"px";
+        cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
+        newWidth = (x-meassurePositionAndShape.AbsolutePosition.x) * 10 * this.openSheetMusicDisplay.zoom;
+        break;
+        default:
+        cursorElement.style.top = (y * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
+        cursorElement.style.left = ((x - 1.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
+        cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
+        newWidth = 3 * 10.0 * this.openSheetMusicDisplay.zoom;
+        break;
+    }
+
     if (newWidth !== cursorElement.width) {
       cursorElement.width = newWidth;
-      this.updateStyle(newWidth);
+      this.updateStyle(newWidth, this.cursorOptions);
     }
     if (this.openSheetMusicDisplay.FollowCursor) {
-      const diff: number = this.cursorElement.getBoundingClientRect().top;
-      this.cursorElement.scrollIntoView({behavior: diff < 1000 ? "smooth" : "auto", block: "center"});
+      if (!this.openSheetMusicDisplay.EngravingRules.RenderSingleHorizontalStaffline) {
+        const diff: number = this.cursorElement.getBoundingClientRect().top;
+        this.cursorElement.scrollIntoView({behavior: diff < 1000 ? "smooth" : "auto", block: "center"});
+      } else {
+        this.cursorElement.scrollIntoView({behavior: "smooth", inline: "center"});
+      }
     }
     // Show cursor
     // // Old cursor: this.graphic.Cursors.push(cursor);
@@ -269,9 +313,9 @@ export class Cursor implements IPlaybackListener {
     this.updateWithTimestamp(iterTmp.CurrentEnrolledTimestamp);
   }
 
-  private updateStyle(width: number, color: string = undefined): void {
-    if (!color) {
-      color = this.rules.DefaultColorCursor;
+  private updateStyle(width: number, cursorOptions: CursorOptions = undefined): void {
+    if (cursorOptions !== undefined) {
+      this.cursorOptions = cursorOptions;
     }
     // Create a dummy canvas to generate the gradient for the cursor
     // FIXME This approach needs to be improved
@@ -279,13 +323,23 @@ export class Cursor implements IPlaybackListener {
     c.width = this.cursorElement.width;
     c.height = 1;
     const ctx: CanvasRenderingContext2D = c.getContext("2d");
-    ctx.globalAlpha = 0.5;
+    ctx.globalAlpha = this.cursorOptions.alpha;
     // Generate the gradient
     const gradient: CanvasGradient = ctx.createLinearGradient(0, 0, this.cursorElement.width, 0);
-    gradient.addColorStop(0, "white"); // it was: "transparent"
-    gradient.addColorStop(0.4, color);
-    gradient.addColorStop(0.6, color);
-    gradient.addColorStop(1, "white"); // it was: "transparent"
+    switch (this.cursorOptions.type) {
+      case 1:
+      case 2:
+      case 3:
+      case 4:
+        gradient.addColorStop(1, this.cursorOptions.color);
+        break;
+      default:
+        gradient.addColorStop(0, "white"); // it was: "transparent"
+        gradient.addColorStop(0.2, this.cursorOptions.color);
+        gradient.addColorStop(0.8, this.cursorOptions.color);
+        gradient.addColorStop(1, "white"); // it was: "transparent"
+      break;
+    }
     ctx.fillStyle = gradient;
     ctx.fillRect(0, 0, width, 1);
     // Set the actual image

+ 36 - 0
src/OpenSheetMusicDisplay/OSMDOptions.ts

@@ -1,6 +1,14 @@
 import { DrawingParametersEnum, ColoringModes } from "../MusicalScore/Graphical/DrawingParameters";
 import { FontStyles } from "../Common/Enums/FontStyles";
 
+export enum CursorType {
+    Standard = 0,
+    ThinLeft = 1,
+    ShortThinTopLeft = 2,
+    CurrentArea = 3,
+    CurrentAreaLeft = 4,
+}
+
 /** Possible options for the OpenSheetMusicDisplay constructor and osmd.setOptions(). None are mandatory.
  *  Note that after using setOptions(), you have to call osmd.render() again to make changes visible.
  *  Example: osmd.setOptions({defaultColorRest: "#AAAAAA", drawSubtitle: false}); osmd.render();
@@ -239,6 +247,10 @@ export interface IOSMDOptions {
      * This works across instruments- If all instruments have subsequent measures with nothing but rests, multirest measures are generated
      */
     autoGenerateMutipleRestMeasuresFromRestMeasures?: boolean;
+    /**
+     * Defines multiple simultaneous cursors. If left undefined the standard cursor will be used.
+     */
+    cursorsOptions?: CursorOptions[];
 }
 
 export enum AlignRestOption {
@@ -293,3 +305,27 @@ export interface AutoBeamOptions {
      */
     groups?: [number[]];
 }
+
+export interface CursorOptions {
+    /**
+     * Type of cursor:
+     * 0: Standard highlighting current notes
+     * 1: Thin line left to the current notes
+     * 2: Short thin line on top of stave and left to the current notes
+     * 3: Current measure
+     * 4: Current measure to left of current notes
+     */
+    type: CursorType;
+    /**
+     * Color to draw the cursor
+     */
+    color: string;
+    /**
+     * If true, this cursor will be followed.
+     */
+    alpha: number;
+    /**
+     * alpha value to be used with color (0.0 transparent, 0.5 medium, 1.0 opaque).
+     */
+    follow: boolean;
+}

+ 56 - 41
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -13,7 +13,7 @@ import { MXLHelper } from "../Common/FileIO/Mxl";
 import { AJAX } from "./AJAX";
 import log from "loglevel";
 import { DrawingParametersEnum, DrawingParameters, ColoringModes } from "../MusicalScore/Graphical/DrawingParameters";
-import { IOSMDOptions, OSMDOptions, AutoBeamOptions, BackendType } from "./OSMDOptions";
+import { IOSMDOptions, OSMDOptions, AutoBeamOptions, BackendType, CursorOptions } from "./OSMDOptions";
 import { EngravingRules, PageFormat } from "../MusicalScore/Graphical/EngravingRules";
 import { AbstractExpression } from "../MusicalScore/VoiceData/Expressions/AbstractExpression";
 import { Dictionary } from "typescript-collections";
@@ -34,7 +34,7 @@ import { DynamicsCalculator } from "../MusicalScore/ScoreIO/MusicSymbolModules/D
  * After the constructor, use load() and render() to load and render a MusicXML file.
  */
 export class OpenSheetMusicDisplay {
-    private version: string = "0.9.5-audio-extended"; // getter: this.Version
+    private version: string = "1.0.0-audio-extended"; // getter: this.Version
     // at release, bump version and change to -release, afterwards to -dev again
 
     /**
@@ -69,7 +69,11 @@ export class OpenSheetMusicDisplay {
         this.renderingManager = new SheetRenderingManager(this.interactionManager);
     }
 
-    public cursor: Cursor;
+    private cursorsOptions: CursorOptions[] = [];
+    public cursors: Cursor[] = [];
+    public get cursor(): Cursor { // lowercase for backwards compatibility since cursor -> cursors change
+        return this.cursors[0];
+    }
     public zoom: number = 1.0;
     protected zoomUpdated: boolean = false;
     /** Timeout in milliseconds used in osmd.load(string) when string is a URL. */
@@ -249,8 +253,10 @@ export class OpenSheetMusicDisplay {
     public updateGraphic(): void {
         const calc: MusicSheetCalculator = new VexFlowMusicSheetCalculator(this.rules);
         this.graphic = new GraphicalMusicSheet(this.sheet, calc);
-        if (this.drawingParameters.drawCursors && this.cursor) {
-            this.cursor.init(this.sheet.MusicPartManager, this.graphic);
+        if (this.drawingParameters.drawCursors) {
+            this.cursors.forEach(cursor => {
+                cursor.init(this.sheet.MusicPartManager, this.graphic);
+            });
         }
         this.renderingManager.setMusicSheet(this.graphic);
         this.interactionManager.Initialize();
@@ -312,11 +318,13 @@ export class OpenSheetMusicDisplay {
         // Finally, draw
         this.drawer.drawSheet(this.graphic);
 
-        this.enableOrDisableCursor(this.drawingParameters.drawCursors);
+        this.enableOrDisableCursors(this.drawingParameters.drawCursors);
 
-        if (this.drawingParameters.drawCursors && this.cursor) {
+        if (this.drawingParameters.drawCursors) {
             // Update the cursor position
-            this.cursor.update();
+            this.cursors.forEach(cursor => {
+                cursor.update();
+            });
         }
         this.zoomUpdated = false;
         //need to init values
@@ -656,6 +664,11 @@ export class OpenSheetMusicDisplay {
         if (options.autoGenerateMutipleRestMeasuresFromRestMeasures !== undefined) {
             this.rules.AutoGenerateMutipleRestMeasuresFromRestMeasures = options.autoGenerateMutipleRestMeasuresFromRestMeasures;
         }
+        if (options.cursorsOptions !== undefined) {
+            this.cursorsOptions = options.cursorsOptions;
+        } else {
+            this.cursorsOptions = [{type: 0, color: this.EngravingRules.DefaultColorCursor, alpha: 0.5, follow: options.followCursor}];
+        }
     }
 
     public setColoringMode(options: IOSMDOptions): void {
@@ -737,8 +750,10 @@ export class OpenSheetMusicDisplay {
      * FIXME: Probably unnecessary
      */
     protected reset(): void {
-        if (this.drawingParameters.drawCursors && this.cursor) {
-            this.cursor.hide();
+        if (this.drawingParameters.drawCursors) {
+            this.cursors.forEach(cursor => {
+                cursor.hide();
+            });
         }
         this.sheet = undefined;
         this.graphic = undefined;
@@ -838,43 +853,43 @@ export class OpenSheetMusicDisplay {
     /** Enable or disable (hide) the cursor.
      * @param enable whether to enable (true) or disable (false) the cursor
      */
-    public enableOrDisableCursor(enable: boolean): void {
+    public enableOrDisableCursors(enable: boolean): void {
         this.drawingParameters.drawCursors = enable;
         if (enable) {
-            // save previous cursor state
-            //const hidden: boolean = this.cursor?.Hidden;
-            const previousIterator: MusicPartManagerIterator = this.cursor?.Iterator;
-            this.cursor?.hide();
-
-            // check which page/backend to draw the cursor on (the pages may have changed since last cursor)
-            let backendToDrawOn: VexFlowBackend = this.drawer?.Backends[0];
-            if (backendToDrawOn && this.rules.RestoreCursorAfterRerender && this.cursor) {
-                const newPageNumber: number = this.cursor.updateCurrentPage();
-                backendToDrawOn = this.drawer.Backends[newPageNumber - 1];
-            }
-            // create new cursor
-            if (backendToDrawOn && backendToDrawOn.getRenderElement()) {
-                this.cursor = new Cursor(backendToDrawOn.getRenderElement(), this);
-            }
-            if (this.sheet && this.graphic && this.cursor) { // else init is called in load()
-                this.cursor.init(this.sheet.MusicPartManager, this.graphic);
-            }
-            this.cursor.show();
-            // restore old cursor state
-            if (this.rules.RestoreCursorAfterRerender) {
-                //this.cursor.hidden = hidden;
-                if (previousIterator) {
-                    this.cursor.iterator = previousIterator;
-                    //this.cursor.cursorPositionChanged(previousIterator.currentTimeStamp, undefined);
-                    this.cursor.update();
+            for (let i: number = 0; i < this.cursorsOptions.length; i++){
+                // save previous cursor state
+                const hidden: boolean = this.cursors[i]?.Hidden;
+                const previousIterator: MusicPartManagerIterator = this.cursors[i]?.Iterator;
+                this.cursors[i]?.hide();
+
+                // check which page/backend to draw the cursor on (the pages may have changed since last cursor)
+                let backendToDrawOn: VexFlowBackend = this.drawer?.Backends[0];
+                if (backendToDrawOn && this.rules.RestoreCursorAfterRerender && this.cursors[i]) {
+                    const newPageNumber: number = this.cursors[i].updateCurrentPage();
+                    backendToDrawOn = this.drawer.Backends[newPageNumber - 1];
+                }
+                // create new cursor
+                if (backendToDrawOn && backendToDrawOn.getRenderElement()) {
+                    this.cursors[i] = new Cursor(backendToDrawOn.getRenderElement(), this, this.cursorsOptions[i]);
+                }
+                if (this.sheet && this.graphic && this.cursors[i]) { // else init is called in load()
+                    this.cursors[i].init(this.sheet.MusicPartManager, this.graphic);
+                }
+
+                // restore old cursor state
+                if (this.rules.RestoreCursorAfterRerender) {
+                    this.cursors[i].hidden = hidden;
+                    if (previousIterator) {
+                        this.cursors[i].iterator = previousIterator;
+                        this.cursors[i].update();
+                    }
                 }
             }
             this.renderingManager.PlaybackManager?.addListener(this.cursor);
         } else { // disable cursor
-            if (!this.cursor) {
-                return;
-            }
-            this.cursor.hide();
+            this.cursors.forEach(cursor => {
+                cursor.hide();
+            });
             // this.cursor = undefined;
             // TODO cursor should be disabled, not just hidden. otherwise user can just call osmd.cursor.hide().
             // however, this could cause null calls (cursor.next() etc), maybe that needs some solution.

+ 4 - 0
src/Plugins/Transpose/TransposeCalculator.ts

@@ -2,6 +2,10 @@ import { ITransposeCalculator } from "../../MusicalScore/Interfaces";
 import { Pitch, NoteEnum, AccidentalEnum } from "../../Common/DataObjects";
 import { KeyInstruction } from "../../MusicalScore/VoiceData/Instructions";
 
+/** Calculates transposition of individual notes and keys,
+ * which is used by multiple OSMD classes to transpose the whole sheet.
+ * Note: This class may not look like much, but a lot of thought has gone into the algorithms,
+ * and the exact usage within OSMD classes. */
 export class TransposeCalculator implements ITransposeCalculator {
     private static keyMapping: number[] = [0, -5, 2, -3, 4, -1, 6, 1, -4, 3, -2, 5];
     private static noteEnums: NoteEnum[] = [NoteEnum.C, NoteEnum.D, NoteEnum.E, NoteEnum.F, NoteEnum.G, NoteEnum.A, NoteEnum.B];

+ 9 - 9
test/Common/OSMD/OSMD_Test.ts

@@ -290,12 +290,12 @@ describe("OpenSheetMusicDisplay Main Export", () => {
         it("should move cursor after instrument is hidden", () => {
             osmd.Sheet.Instruments[1].Visible = false;
             osmd.render();
-            osmd.cursor.show();
+            osmd.cursors[0].show();
             for (let i: number = 0; i < 100; i++) {
-                osmd.cursor.next();
+                osmd.cursors[0].next();
             }
             // After 100 steps in the visible score, cursor reached 3rd note from 17, a C
-            chai.expect(osmd.cursor.NotesUnderCursor()[0].halfTone).to.equal(60);
+            chai.expect(osmd.cursors[0].NotesUnderCursor()[0].halfTone).to.equal(60);
         });
     });
     describe("cursor", () => {
@@ -307,7 +307,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
             opensheetmusicdisplay.load(score).then(
                 (_: {}) => {
                     opensheetmusicdisplay.render();
-                    opensheetmusicdisplay.cursor.show();
+                    opensheetmusicdisplay.cursors[0].show();
                     done();
                 },
                 done
@@ -316,19 +316,19 @@ describe("OpenSheetMusicDisplay Main Export", () => {
 
         describe("get AllVoicesUnderCursor", () => {
             it("retrieves all voices under cursor", () => {
-                const voiceEntries: VoiceEntry[] = opensheetmusicdisplay.cursor.VoicesUnderCursor();
+                const voiceEntries: VoiceEntry[] = opensheetmusicdisplay.cursors[0].VoicesUnderCursor();
                 chai.expect(voiceEntries.length).to.equal(2);
             });
         });
 
         describe("VoicesUnderCursor", () => {
             it("retrieves voices for a specific instrument under cursor", () => {
-                const voiceEntries: VoiceEntry[] = opensheetmusicdisplay.cursor.VoicesUnderCursor();
+                const voiceEntries: VoiceEntry[] = opensheetmusicdisplay.cursors[0].VoicesUnderCursor();
                 chai.expect(voiceEntries.length).to.equal(2);
             });
             it("retrieves all voices under cursor when instrument not specified", () => {
                 const instrument: Instrument = opensheetmusicdisplay.Sheet.Instruments[1];
-                const voiceEntries: VoiceEntry[] = opensheetmusicdisplay.cursor.VoicesUnderCursor(instrument);
+                const voiceEntries: VoiceEntry[] = opensheetmusicdisplay.cursors[0].VoicesUnderCursor(instrument);
                 chai.expect(voiceEntries.length).to.equal(1);
             });
         });
@@ -336,12 +336,12 @@ describe("OpenSheetMusicDisplay Main Export", () => {
         describe("NotesUnderCursor", () => {
             it("gets notes for a specific instrument under cursor", () => {
                 const instrument: Instrument = opensheetmusicdisplay.Sheet.Instruments[0];
-                const notes: Note[] = opensheetmusicdisplay.cursor.NotesUnderCursor(instrument);
+                const notes: Note[] = opensheetmusicdisplay.cursors[0].NotesUnderCursor(instrument);
                 chai.expect(notes.length).to.equal(1);
             });
 
             it("gets all notes under cursor when instrument unspecified", () => {
-                const notes: Note[] = opensheetmusicdisplay.cursor.NotesUnderCursor();
+                const notes: Note[] = opensheetmusicdisplay.cursors[0].NotesUnderCursor();
                 chai.expect(notes.length).to.equal(2);
             });
         });

+ 6 - 1
test/Util/generateImages_browserless.js

@@ -300,7 +300,12 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
         }
     }
 
-    await osmdInstance.load(loadParameter); // if using load.then() without await, memory will not be freed up between renders
+    try {
+        await osmdInstance.load(loadParameter); // if using load.then() without await, memory will not be freed up between renders
+    } catch (ex) {
+        console.log("couldn't load sample " + sampleFilename + ", skipping. Error: \n" + ex);
+        return;
+    }
     debug("xml loaded", DEBUG);
     try {
         osmdInstance.render();

+ 330 - 0
test/data/OSMD_function_Test_Repeat.musicxml

@@ -0,0 +1,330 @@
+<?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>OSMD Function Test - Repeat Stave Connectors</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.5.0</software>
+      <encoding-date>2021-04-22</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.05556</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1584</page-height>
+      <page-width>1224</page-width>
+      <page-margins type="even">
+        <left-margin>56.6929</left-margin>
+        <right-margin>56.6929</right-margin>
+        <top-margin>56.6929</top-margin>
+        <bottom-margin>113.386</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>56.6929</left-margin>
+        <right-margin>56.6929</right-margin>
+        <top-margin>56.6929</top-margin>
+        <bottom-margin>113.386</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="FreeSerif" font-size="10"/>
+    <lyric-font font-family="FreeSerif" font-size="11"/>
+    </defaults>
+  <credit page="1">
+    <credit-words default-x="612" default-y="1529.66" justify="center" valign="top" font-size="24">OSMD Function Test - Repeat Stave Connectors</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="327.60">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>21.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>105.00</top-system-distance>
+          </system-layout>
+        <staff-layout number="2">
+          <staff-distance>65.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</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="82.47" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note default-x="82.47" default-y="-140.00">
+        <pitch>
+          <step>A</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>half</type>
+        <staff>2</staff>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        <repeat direction="backward"/>
+        </barline>
+      </measure>
+    <measure number="2" width="250.14">
+      <barline location="left">
+        <bar-style>heavy-light</bar-style>
+        <repeat direction="forward"/>
+        </barline>
+      <note default-x="23.37" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note default-x="23.37" default-y="-140.00">
+        <pitch>
+          <step>A</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>half</type>
+        <staff>2</staff>
+        </note>
+      </measure>
+    <measure number="3" width="256.74">
+      <barline location="left">
+        <bar-style>heavy-light</bar-style>
+        <repeat direction="forward"/>
+        </barline>
+      <note default-x="28.37" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note default-x="28.37" default-y="-125.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>half</type>
+        <staff>2</staff>
+        </note>
+      </measure>
+    <measure number="4" width="255.14">
+      <note default-x="10.00" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note default-x="10.00" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>half</type>
+        <staff>2</staff>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        <repeat direction="backward"/>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 428 - 0
test/data/test_tie_direction_simple.musicxml

@@ -0,0 +1,428 @@
+<?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 - Tie Direction Simple</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2021-05-07</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.86" justify="center" valign="top" font-size="22">Test - Tie Direction 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="963.67">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>64.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>2</divisions>
+        <key>
+          <fifths>1</fifths>
+          </key>
+        <time>
+          <beats>4</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="106.75" default-y="-55.00">
+        <pitch>
+          <step>B</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <technical>
+            <fingering>1</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="106.75" default-y="-45.00">
+        <chord/>
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <technical>
+            <fingering>2</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="224.28" default-y="-55.00">
+        <pitch>
+          <step>B</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <tie type="start"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">continue</beam>
+        <notations>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <note default-x="224.28" default-y="-45.00">
+        <chord/>
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <tie type="start"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <note default-x="341.82" default-y="-55.00">
+        <pitch>
+          <step>B</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <tie type="stop"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">continue</beam>
+        <notations>
+          <tied type="stop"/>
+          </notations>
+        </note>
+      <note default-x="341.82" default-y="-45.00">
+        <chord/>
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <tie type="stop"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <tied type="stop"/>
+          </notations>
+        </note>
+      <note default-x="459.36" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <technical>
+            <fingering>2</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="459.36" default-y="-40.00">
+        <chord/>
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <technical>
+            <fingering>4</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="576.90" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="start"/>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <note default-x="576.90" default-y="-35.00">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="start"/>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <note default-x="588.80" default-y="-30.00">
+        <chord/>
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="start"/>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <note default-x="764.96" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="stop"/>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <tied type="stop"/>
+          </notations>
+        </note>
+      <note default-x="764.96" default-y="-35.00">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="stop"/>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <tied type="stop"/>
+          </notations>
+        </note>
+      <note default-x="776.86" default-y="-30.00">
+        <chord/>
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="stop"/>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <tied type="stop"/>
+          </notations>
+        </note>
+      <backup>
+        <duration>8</duration>
+        </backup>
+      <note default-x="106.75" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <notations>
+          <technical>
+            <fingering placement="below">1</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="224.28" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <tie type="start"/>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <notations>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <note default-x="341.82" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <tie type="stop"/>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <notations>
+          <tied type="stop"/>
+          </notations>
+        </note>
+      <note default-x="459.36" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <notations>
+          <technical>
+            <fingering placement="below">2</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="576.90" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="start"/>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        <notations>
+          <tied type="start"/>
+          </notations>
+        </note>
+      <note default-x="764.96" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <tie type="stop"/>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        <notations>
+          <tied type="stop"/>
+          </notations>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

BIN
test/data/test_tuplets_starting_with_rests_layout.mxl