Przeglądaj źródła

big 1.01 update: add pedal and trill lines, fix voice alignment (+ shared stems), flatten slurs, etc

add pedal symbols and lines
add trill lines, also fixed for tabs
fix simultaneous voices not aligned, not sharing stem when possible (fix double noteheads)
improve slur arcs by adjusting control points etc

fix a tie direction bug
fix barline incorrectly shown as endline
fix connecting repeat line (StaveConnector) incomplete
fix triplet number position
Wide improvements to wedges (cresc./decresc.): fix lengths, placement, etc

Discussion issue: #24
sschmid 3 lat temu
rodzic
commit
f408f46b56
30 zmienionych plików z 4918 dodań i 52 usunięć
  1. 3 0
      demo/index.js
  2. 1 1
      package.json
  3. 3 0
      src/MusicalScore/Graphical/EngravingRules.ts
  4. 30 0
      src/MusicalScore/Graphical/GraphicalPedal.ts
  5. 30 8
      src/MusicalScore/Graphical/GraphicalSlur.ts
  6. 13 0
      src/MusicalScore/Graphical/GraphicalWavyLine.ts
  7. 80 2
      src/MusicalScore/Graphical/MusicSheetCalculator.ts
  8. 8 0
      src/MusicalScore/Graphical/MusicSheetDrawer.ts
  9. 4 1
      src/MusicalScore/Graphical/MusicSymbol.ts
  10. 5 0
      src/MusicalScore/Graphical/StaffLine.ts
  11. 0 8
      src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts
  12. 465 11
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts
  13. 32 0
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts
  14. 109 0
      src/MusicalScore/Graphical/VexFlow/VexFlowPedal.ts
  15. 86 0
      src/MusicalScore/Graphical/VexFlow/VexflowVibratoBracket.ts
  16. 24 9
      src/MusicalScore/ScoreIO/InstrumentReader.ts
  17. 79 0
      src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts
  18. 4 4
      src/MusicalScore/ScoreIO/VoiceGenerator.ts
  19. 21 0
      src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/Pedal.ts
  20. 12 0
      src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/WavyLine.ts
  21. 6 0
      src/MusicalScore/VoiceData/Expressions/MultiExpression.ts
  22. 1 8
      src/MusicalScore/VoiceData/TabNote.ts
  23. 10 0
      src/VexFlowPatch/readme.txt
  24. 298 0
      src/VexFlowPatch/src/pedalmarking.js
  25. 1210 0
      src/VexFlowPatch/src/stavenote.js
  26. 222 0
      src/VexFlowPatch/src/stemmablenote.js
  27. 91 0
      src/VexFlowPatch/src/vibratobracket.js
  28. 855 0
      test/data/OSMD_Function_Test_Pedals.musicxml
  29. 655 0
      test/data/OSMD_Function_Test_Voice_Alignment.musicxml
  30. 561 0
      test/data/OSMD_Trill_Line_Function_Test.musicxml

+ 3 - 0
demo/index.js

@@ -48,6 +48,7 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
             "OSMD Function Test - Invisible Notes": "OSMD_function_test_invisible_notes.musicxml",
             "OSMD Function Test - Notehead Shapes": "OSMD_function_test_noteheadShapes.musicxml",
             "OSMD Function Test - Ornaments": "OSMD_function_test_Ornaments.xml",
+            "OSMD Function Test - Pedals": "OSMD_Function_Test_Pedals.musicxml",
             "OSMD Function Test - Selecting Measures To Draw": "OSMD_function_test_measuresToDraw_Beethoven_AnDieFerneGeliebte.xml",
             "OSMD Function Test - System and Page Breaks": "OSMD_Function_Test_System_and_Page_Breaks_4_pages.mxl",
             "OSMD Function Test - Tabulature": "OSMD_Function_Test_Tabulature_hayden_study_1.mxl",
@@ -60,6 +61,8 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
             "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",
+            "OSMD Function Test - Trill Lines": "OSMD_Trill_Line_Function_Test.musicxml",
+            "OSMD Function Test - Voice Alignment": "OSMD_Function_Test_Voice_Alignment.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",

+ 1 - 1
package.json

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

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

@@ -190,6 +190,7 @@ export class EngravingRules {
     public SlurHeightFlattenLongSlursCutoffAngle: number;
     public SlurHeightFlattenLongSlursCutoffWidth: number;
     public SlursStartingAtSameStaffEntryYOffset: number;
+    public SlurMaximumYControlPointDistance: number;
     public InstantaneousTempoTextHeight: number;
     public ContinuousDynamicTextHeight: number;
     public MoodTextHeight: number;
@@ -490,6 +491,8 @@ export class EngravingRules {
         this.SlurHeightFlattenLongSlursCutoffAngle = 47;
         this.SlurHeightFlattenLongSlursCutoffWidth = 16; // 15 ~ slur between measure's first notes in 4/4. 14 -> problem with test_slurs_highNotes
         this.SlursStartingAtSameStaffEntryYOffset = 0.8;
+        //Maximum y difference between control points. Forces slurs to have less 'weight' either way in the x direction
+        this.SlurMaximumYControlPointDistance = undefined;
 
         // Repetitions
         this.RepetitionEndingLabelHeight = 2.0;

+ 30 - 0
src/MusicalScore/Graphical/GraphicalPedal.ts

@@ -0,0 +1,30 @@
+import {GraphicalObject} from "./GraphicalObject";
+import {BoundingBox} from "./BoundingBox";
+import {MusicSymbol} from "./MusicSymbol";
+import { Pedal } from "../VoiceData/Expressions/ContinuousExpressions/Pedal";
+
+/**
+ * The graphical counterpart of an [[Pedal]]
+ */
+export class GraphicalPedal extends GraphicalObject {
+
+    constructor(pedal: Pedal, parent: BoundingBox) {
+        super();
+        this.getPedal = pedal;
+        this.setSymbol();
+        this.PositionAndShape = new BoundingBox(this, parent);
+    }
+
+    public getPedal: Pedal;
+    public pedalSymbol: MusicSymbol;
+
+    private setSymbol(): void {
+        if (!this.getPedal.IsLine && this.getPedal.IsSign) {
+            this.pedalSymbol = MusicSymbol.PEDAL_SYMBOL;
+        } else if (this.getPedal.IsLine && this.getPedal.IsSign){
+            this.pedalSymbol = MusicSymbol.PEDAL_MIXED;
+        } else {//Bracket is default
+            this.pedalSymbol = MusicSymbol.PEDAL_BRACKET;
+        }
+    }
+}

+ 30 - 8
src/MusicalScore/Graphical/GraphicalSlur.ts

@@ -220,7 +220,7 @@ export class GraphicalSlur extends GraphicalCurve {
 
             // calculate Curve's Control Points
             const controlPoints: {startControlPoint: PointF2D, endControlPoint: PointF2D} =
-                this.calculateControlPoints(end2.x, startAngle, endAngle, transformedPoints, heightWidthRatio);
+                this.calculateControlPoints(end2.x, startAngle, endAngle, transformedPoints, heightWidthRatio, startY, endY);
 
             let startControlPoint: PointF2D = controlPoints.startControlPoint;
             let endControlPoint: PointF2D = controlPoints.endControlPoint;
@@ -389,7 +389,7 @@ export class GraphicalSlur extends GraphicalCurve {
 
             // calculate Curve's Control Points
             const controlPoints: {startControlPoint: PointF2D, endControlPoint: PointF2D} =
-                this.calculateControlPoints(end2.x, startAngle, endAngle, transformedPoints, heightWidthRatio);
+                this.calculateControlPoints(end2.x, startAngle, endAngle, transformedPoints, heightWidthRatio, startY, endY);
             let startControlPoint: PointF2D = controlPoints.startControlPoint;
             let endControlPoint: PointF2D = controlPoints.endControlPoint;
 
@@ -503,7 +503,7 @@ export class GraphicalSlur extends GraphicalCurve {
             //     }
             // }
         } else {
-            startX = staffLine.Measures[0].beginInstructionsWidth;
+            startX = 0;
         }
 
         if (slurEndNote) {
@@ -562,14 +562,22 @@ export class GraphicalSlur extends GraphicalCurve {
 
         // if GraphicalSlur breaks over System, then the end/start of the curve is at the corresponding height with the known start/end
         if (!slurStartNote && !slurEndNote) {
-            startY = 0;
-            endY = 0;
+            startY = -1.5;
+            endY = -1.5;
         }
         if (!slurStartNote) {
-            startY = endY;
+            if (this.placement === PlacementEnum.Above) {
+                startY = endY - 1;
+            } else {
+                startY = endY + 1;
+            }
         }
         if (!slurEndNote) {
-            endY = startY;
+            if (this.placement === PlacementEnum.Above) {
+                endY = startY - 1;
+            } else {
+                endY = startY + 1;
+            }
         }
 
         // if two slurs start/end at the same GraphicalNote, then the second gets an offset
@@ -870,7 +878,8 @@ export class GraphicalSlur extends GraphicalCurve {
      * @param points
      */
     private calculateControlPoints(endX: number, startAngle: number, endAngle: number,
-                                   points: PointF2D[], heightWidthRatio: number
+                                   points: PointF2D[], heightWidthRatio: number,
+                                   startY: number, endY: number
     ): { startControlPoint: PointF2D, endControlPoint: PointF2D } {
         let heightFactor: number = this.rules.SlurHeightFactor;
         let widthFlattenFactor: number = 1;
@@ -911,6 +920,19 @@ export class GraphicalSlur extends GraphicalCurve {
         const endControlPoint: PointF2D = new PointF2D();
         endControlPoint.x = endX - (endX * factorEnd * Math.cos(endAngle * GraphicalSlur.degreesToRadiansFactor));
         endControlPoint.y = -(endX * factorEnd * Math.sin(endAngle * GraphicalSlur.degreesToRadiansFactor));
+        //Soften the slur in a "brute-force" way
+        let controlPointYDiff: number = startControlPoint.y - endControlPoint.y;
+        while (this.rules.SlurMaximumYControlPointDistance &&
+               Math.abs(controlPointYDiff) > this.rules.SlurMaximumYControlPointDistance) {
+            if (controlPointYDiff < 0) {
+                startControlPoint.y += 1;
+                endControlPoint.y -= 1;
+            } else {
+                startControlPoint.y -= 1;
+                endControlPoint.y += 1;
+            }
+            controlPointYDiff = startControlPoint.y - endControlPoint.y;
+        }
         return {startControlPoint: startControlPoint, endControlPoint: endControlPoint};
     }
 

+ 13 - 0
src/MusicalScore/Graphical/GraphicalWavyLine.ts

@@ -0,0 +1,13 @@
+import { BoundingBox } from "./BoundingBox";
+import { WavyLine } from "../VoiceData/Expressions/ContinuousExpressions/WavyLine";
+import { GraphicalObject } from "./GraphicalObject";
+
+export class GraphicalWavyLine extends GraphicalObject {
+    constructor(wavyLine: WavyLine, parent: BoundingBox) {
+        super();
+        this.getWavyLine = wavyLine;
+        this.PositionAndShape = new BoundingBox(this, parent);
+    }
+
+    public getWavyLine: WavyLine;
+}

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

@@ -513,10 +513,21 @@ export abstract class MusicSheetCalculator {
         relativeY = Math.min(0, relativeY);
 
         graphicalLabel.PositionAndShape.RelativePosition = new PointF2D(relativeX, relativeY);
-
-        skyBottomLineCalculator.updateSkyLineInRange(start, end, relativeY + graphicalLabel.PositionAndShape.BorderMarginTop);
         musicSystem.MeasureNumberLabels.push(graphicalLabel);
     }
+    //So we can apply slurs first, then do these
+    private calculateMeasureNumberSkyline(musicSystem: MusicSystem): void {
+        const staffLine: StaffLine = musicSystem.StaffLines[0];
+        for(const measureNumberLabel of musicSystem.MeasureNumberLabels) {
+            // and the corresponding SkyLine indices
+            let start: number = measureNumberLabel.PositionAndShape.RelativePosition.x;
+            let end: number = start - measureNumberLabel.PositionAndShape.BorderLeft + measureNumberLabel.PositionAndShape.BorderRight;
+            start -= staffLine.PositionAndShape.RelativePosition.x;
+            end -= staffLine.PositionAndShape.RelativePosition.x;
+            staffLine.SkyBottomLineCalculator.updateSkyLineInRange(start, end,
+                measureNumberLabel.PositionAndShape.RelativePosition.y + measureNumberLabel.PositionAndShape.BorderMarginTop);
+        }
+    }
 
     /**
      * Calculate the shape (Bézier curve) for this tie.
@@ -647,6 +658,26 @@ export abstract class MusicSheetCalculator {
     }
 
     /**
+     * Calculate a single Pedal for a [[MultiExpression]].
+     * @param sourceMeasure
+     * @param multiExpression
+     * @param measureIndex
+     * @param staffIndex
+     */
+    protected abstract calculateSinglePedal(sourceMeasure: SourceMeasure, multiExpression: MultiExpression,
+        measureIndex: number, staffIndex: number): void;
+
+    /**
+     * Calculate a single Wavy Line for a [[MultiExpression]].
+     * @param sourceMeasure
+     * @param multiExpression
+     * @param measureIndex
+     * @param staffIndex
+     */
+     protected abstract calculateSingleWavyLine(sourceMeasure: SourceMeasure, multiExpression: MultiExpression,
+        measureIndex: number, staffIndex: number): void;
+
+    /**
      * Calculate all the textual [[RepetitionInstruction]]s (e.g. dal segno) for a single [[SourceMeasure]].
      * @param repetitionInstruction
      * @param measureIndex
@@ -831,6 +862,13 @@ export abstract class MusicSheetCalculator {
         if (!this.leadSheet && this.rules.RenderSlurs) {
             this.calculateSlurs();
         }
+        //Calculate measure number skyline AFTER slurs
+        if (this.rules.RenderMeasureNumbers) {
+            for (let idx: number = 0, len: number = this.musicSystems.length; idx < len; ++idx) {
+                const musicSystem: MusicSystem = this.musicSystems[idx];
+                this.calculateMeasureNumberSkyline(musicSystem);
+            }
+        }
         // calculate StaffEntry Ornaments
         // (must come after Slurs)
         if (!this.leadSheet) {
@@ -847,6 +885,10 @@ export abstract class MusicSheetCalculator {
             this.calculateExpressionAlignements();
             // calculate all OctaveShifts
             this.calculateOctaveShifts();
+            // calculate all Pedal Expressions
+            this.calculatePedals();
+            //calculate all wavy lines (vibrato, trill marks)
+            this.calculateWavyLines();
             // calcualte RepetitionInstructions (Dal Segno, Coda, etc)
             this.calculateWordRepetitionInstructions();
         }
@@ -2946,6 +2988,42 @@ export abstract class MusicSheetCalculator {
         }
     }
 
+    private calculatePedals(): void {
+        for (let i: number = 0; i < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; i++) {
+            const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
+            for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
+                if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
+                    continue;
+                }
+                if (this.graphicalMusicSheet.MeasureList[i][j].ParentStaff.ParentInstrument.Visible) {
+                    for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
+                        if ((sourceMeasure.StaffLinkedExpressions[j][k].PedalStart)) {
+                            this.calculateSinglePedal(sourceMeasure, sourceMeasure.StaffLinkedExpressions[j][k], i, j);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private calculateWavyLines(): void {
+        for (let i: number = 0; i < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; i++) {
+            const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
+            for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
+                if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
+                    continue;
+                }
+                if (this.graphicalMusicSheet.MeasureList[i][j].ParentStaff.ParentInstrument.Visible) {
+                    for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
+                        if ((sourceMeasure.StaffLinkedExpressions[j][k].WavyLineStart)) {
+                            this.calculateSingleWavyLine(sourceMeasure, sourceMeasure.StaffLinkedExpressions[j][k], i, j);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     private getFirstLeftNotNullStaffEntryFromContainer(horizontalIndex: number, verticalIndex: number, multiStaffInstrument: boolean): GraphicalStaffEntry {
         if (this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[horizontalIndex].StaffEntries[verticalIndex]) {
             return this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[horizontalIndex].StaffEntries[verticalIndex];

+ 8 - 0
src/MusicalScore/Graphical/MusicSheetDrawer.ts

@@ -371,6 +371,10 @@ export abstract class MusicSheetDrawer {
         }
         this.drawOctaveShifts(staffLine);
 
+        this.drawPedals(staffLine);
+
+        this.drawWavyLines(staffLine);
+
         this.drawExpressions(staffLine);
 
         if (this.skyLineVisible) {
@@ -430,6 +434,10 @@ export abstract class MusicSheetDrawer {
         return;
     }
 
+    protected abstract drawPedals(staffLine: StaffLine): void;
+
+    protected abstract drawWavyLines(staffLine: StaffLine): void;
+
     protected drawStaffLines(staffLine: StaffLine): void {
         if (staffLine.StaffLines) {
             const position: PointF2D = staffLine.PositionAndShape.AbsolutePosition;

+ 4 - 1
src/MusicalScore/Graphical/MusicSymbol.ts

@@ -83,5 +83,8 @@ export enum MusicSymbol {
     MIC,
     SNAP_PIZZICATO,
     NATURAL_HARMONIC,
-    EditPen
+    EditPen,
+    PEDAL_BRACKET,
+    PEDAL_MIXED,
+    PEDAL_SYMBOL
 }

+ 5 - 0
src/MusicalScore/Graphical/StaffLine.ts

@@ -14,6 +14,8 @@ import { GraphicalOctaveShift } from "./GraphicalOctaveShift";
 import { GraphicalSlur } from "./GraphicalSlur";
 import { AbstractGraphicalExpression } from "./AbstractGraphicalExpression";
 import { MusicSheetCalculator } from "./MusicSheetCalculator";
+import { GraphicalPedal } from "./GraphicalPedal";
+import { GraphicalWavyLine } from "./GraphicalWavyLine";
 
 /**
  * A StaffLine contains the [[Measure]]s in one line of the music sheet
@@ -173,6 +175,9 @@ export abstract class StaffLine extends GraphicalObject {
         this.octaveShifts = value;
     }
 
+    public Pedals: GraphicalPedal[] = [];
+    public WavyLines: GraphicalWavyLine[] = [];
+
     public get StaffHeight(): number {
         return this.staffHeight;
     }

+ 0 - 8
src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts

@@ -650,7 +650,6 @@ export class VexFlowConverter {
         const isTuplet: boolean = gve.notes[0].sourceNote.NoteTuplet !== undefined;
         let duration: string = VexFlowConverter.duration(frac, isTuplet);
         let numDots: number = 0;
-        let tabVibrato: boolean = false;
         for (const note of gve.notes) {
             const tabNote: TabNote = note.sourceNote as TabNote;
             const tabPosition: {str: number, fret: number} = {str: tabNote.StringNumberTab, fret: tabNote.FretNumber};
@@ -674,10 +673,6 @@ export class VexFlowConverter {
                 });
             }
 
-            if (tabNote.VibratoStroke) {
-                tabVibrato = true;
-            }
-
             if (numDots < note.numberOfDots) {
                 numDots = note.numberOfDots;
             }
@@ -702,9 +697,6 @@ export class VexFlowConverter {
                 vfnote.addModifier (new Vex.Flow.Bend(phrase.text, true));
             }
         });
-        if (tabVibrato) {
-            vfnote.addModifier(new Vex.Flow.Vibrato());
-        }
 
         return vfnote;
     }

+ 465 - 11
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -15,7 +15,7 @@ import { ClefInstruction } from "../../VoiceData/Instructions/ClefInstruction";
 import { OctaveEnum, OctaveShift } from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
 import { Fraction } from "../../../Common/DataObjects/Fraction";
 import { LyricWord } from "../../VoiceData/Lyrics/LyricsWord";
-import { OrnamentContainer } from "../../VoiceData/OrnamentContainer";
+import { OrnamentContainer, OrnamentEnum } from "../../VoiceData/OrnamentContainer";
 import { Articulation } from "../../VoiceData/Articulation";
 import { Tuplet } from "../../VoiceData/Tuplet";
 import { VexFlowMeasure } from "./VexFlowMeasure";
@@ -56,6 +56,12 @@ import { TabNote } from "../../VoiceData/TabNote";
 import { PlacementEnum } from "../../VoiceData/Expressions";
 import { GraphicalChordSymbolContainer } from "../GraphicalChordSymbolContainer";
 import { RehearsalExpression } from "../../VoiceData/Expressions/RehearsalExpression";
+import { Pedal } from "../../VoiceData/Expressions/ContinuousExpressions/Pedal";
+import { VexFlowPedal } from "./VexFlowPedal";
+import { MusicSymbol } from "../MusicSymbol";
+import { VexFlowVoiceEntry } from "./VexFlowVoiceEntry";
+import { WavyLine } from "../../VoiceData/Expressions/ContinuousExpressions/WavyLine";
+import { VexflowVibratoBracket } from "./VexflowVibratoBracket";
 
 export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
   /** space needed for a dash for lyrics spacing, calculated once */
@@ -186,11 +192,10 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
 
     let minStaffEntriesWidth: number = 12; // a typical measure has roughly a length of 3*StaffHeight (3*4 = 12)
     const parentSourceMeasure: SourceMeasure = measures[0].parentSourceMeasure;
+    // the voicing space bonus addition makes the voicing more relaxed. With a bonus of 0 the notes are basically completely squeezed together.
+    const staffEntryFactor: number = 0.3;
 
     if (allVoices.length > 0) {
-      // the voicing space bonus addition makes the voicing more relaxed. With a bonus of 0 the notes are basically completely squeezed together.
-      const staffEntryFactor: number = 0.3;
-
       minStaffEntriesWidth = formatter.preCalculateMinTotalWidth(allVoices) / unitInPixels
       * this.rules.VoiceSpacingMultiplierVexflow
       + this.rules.VoiceSpacingAddendVexflow
@@ -293,6 +298,29 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         (<VexFlowStaffEntry>staffEntry).calculateXPosition();
       }
     }
+    //Can't quite figure out why, but this is the calculation that needs redone to have consistent rendering.
+    //The first render of a sheet vs. subsequent renders are calculated differently by vexflow without this re-joining of the voices
+    for (const measure of measures) {
+      if (!measure) {
+        continue;
+      }
+      const mvoices: { [voiceID: number]: Vex.Flow.Voice } = (measure as VexFlowMeasure).vfVoices;
+      const voices: Vex.Flow.Voice[] = [];
+      for (const voiceID in mvoices) {
+        if (mvoices.hasOwnProperty(voiceID)) {
+          voices.push(mvoices[voiceID]);
+        }
+      }
+
+      if (voices.length === 0) {
+        log.debug("Found a measure with no voices. Continuing anyway.", mvoices);
+        // no need to log this, measures with no voices/notes are fine. see OSMDOptions.fillEmptyMeasuresWithWholeRest
+        continue;
+      }
+      // all voices that belong to one stave are collectively added to create a common context in VexFlow.
+      formatter.joinVoices(voices);
+    }
+
     // calculateMeasureWidthFromLyrics() will be called from MusicSheetCalculator after this
     return minStaffEntriesWidth;
   }
@@ -812,7 +840,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     const maxMeasureToDrawIndex: number = this.rules.MaxMeasureToDrawIndex;
 
     let startStaffLine: StaffLine = this.graphicalMusicSheet.MeasureList[measureIndex][staffIndex].ParentStaffLine;
-    if (!startStaffLine) { // fix for rendering range set. all of these can probably done cleaner.
+    if (!startStaffLine) { // fix for rendering range set. all of these can probably be done cleaner.
       startStaffLine = this.graphicalMusicSheet.MeasureList[minMeasureToDrawIndex][staffIndex].ParentStaffLine;
     }
 
@@ -837,11 +865,11 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
       startMeasure = this.graphicalMusicSheet.MeasureList[minMeasureToDrawIndex][staffIndex]; // first rendered measure
     }
 
-    if (startMeasure.MeasureNumber < minMeasureToDrawIndex + 1 ||
-        startMeasure.MeasureNumber > maxMeasureToDrawIndex + 1 ||
-        endMeasure.MeasureNumber < minMeasureToDrawIndex + 1 ||
-        endMeasure.MeasureNumber > maxMeasureToDrawIndex + 1) {
-      // octave shift completely out of drawing range, don't draw anything
+    if (startMeasure.parentSourceMeasure.measureListIndex < minMeasureToDrawIndex ||
+        startMeasure.parentSourceMeasure.measureListIndex > maxMeasureToDrawIndex ||
+        endMeasure.parentSourceMeasure.measureListIndex < minMeasureToDrawIndex ||
+        endMeasure.parentSourceMeasure.measureListIndex > maxMeasureToDrawIndex) {
+      // completely out of drawing range, don't draw anything
       return;
     }
 
@@ -922,7 +950,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
             const firstNote: GraphicalStaffEntry = nextShiftFirstMeasure.staffEntries[0];
             let lastNote: GraphicalStaffEntry = nextShiftLastMeasure.staffEntries[nextShiftLastMeasure.staffEntries.length - 1];
 
-            //If the is the ending staffline, this endMeasure is the end of the shift
+            //If the end measure's staffline is the ending staffline, this endMeasure is the end of the shift
             if (endMeasure.ParentStaffLine === nextShiftStaffline) {
               nextShiftLastMeasure = endMeasure;
               lastNote = endStaffEntry;
@@ -946,6 +974,429 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
   }
 
+  protected calculateSinglePedal(sourceMeasure: SourceMeasure, multiExpression: MultiExpression, measureIndex: number, staffIndex: number): void {
+    // calculate absolute Timestamp and startStaffLine (and EndStaffLine if needed)
+    const pedal: Pedal = multiExpression.PedalStart;
+
+    const startTimeStamp: Fraction = pedal.ParentStartMultiExpression.Timestamp;
+    const endTimeStamp: Fraction = pedal.ParentEndMultiExpression?.Timestamp;
+
+    const minMeasureToDrawIndex: number = this.rules.MinMeasureToDrawIndex;
+    const maxMeasureToDrawIndex: number = this.rules.MaxMeasureToDrawIndex;
+
+    let startStaffLine: StaffLine = this.graphicalMusicSheet.MeasureList[measureIndex][staffIndex].ParentStaffLine;
+    if (!startStaffLine) { // fix for rendering range set. all of these can probably be done cleaner.
+      startStaffLine = this.graphicalMusicSheet.MeasureList[minMeasureToDrawIndex][staffIndex].ParentStaffLine;
+    }
+    let endMeasure: GraphicalMeasure = undefined;
+    if (pedal.ParentEndMultiExpression) {
+      endMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(pedal.ParentEndMultiExpression.SourceMeasureParent,
+                                                                                          staffIndex);
+    } else {
+      endMeasure = this.graphicalMusicSheet.getLastGraphicalMeasureFromIndex(staffIndex, true); // get last rendered measure
+    }
+    if (endMeasure.MeasureNumber > maxMeasureToDrawIndex + 1) { //  ends in measure not rendered
+      endMeasure = this.graphicalMusicSheet.getLastGraphicalMeasureFromIndex(staffIndex, true);
+    }
+    let startMeasure: GraphicalMeasure = undefined;
+    if (pedal.ParentEndMultiExpression) {
+      startMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(pedal.ParentStartMultiExpression.SourceMeasureParent,
+                                                                                            staffIndex);
+    } else {
+      startMeasure = this.graphicalMusicSheet.MeasureList[minMeasureToDrawIndex][staffIndex]; // first rendered measure
+    }
+    if (startMeasure.MeasureNumber < minMeasureToDrawIndex + 1) { //  starts before range of measures selected to render
+      startMeasure = this.graphicalMusicSheet.MeasureList[minMeasureToDrawIndex][staffIndex]; // first rendered measure
+    }
+
+    if (startMeasure.parentSourceMeasure.measureListIndex < minMeasureToDrawIndex ||
+        startMeasure.parentSourceMeasure.measureListIndex > maxMeasureToDrawIndex ||
+        endMeasure.parentSourceMeasure.measureListIndex < minMeasureToDrawIndex ||
+        endMeasure.parentSourceMeasure.measureListIndex > maxMeasureToDrawIndex) {
+      // completely out of drawing range, don't draw anything
+      return;
+    }
+
+    let endStaffLine: StaffLine = endMeasure.ParentStaffLine;
+    if (!endStaffLine) {
+      endStaffLine = startStaffLine;
+    }
+    if (endMeasure && startStaffLine && endStaffLine) {
+      let openEnd: boolean = false;
+      if (startStaffLine !== endStaffLine) {
+        openEnd = true;
+      }
+      // calculate GraphicalPedal and RelativePositions
+      const graphicalPedal: VexFlowPedal = new VexFlowPedal(pedal, startStaffLine.PositionAndShape, false, openEnd);
+      // calculate RelativePosition
+      let startStaffEntry: GraphicalStaffEntry = startMeasure.findGraphicalStaffEntryFromTimestamp(startTimeStamp);
+      if (!startStaffEntry) { // fix for rendering range set
+        startStaffEntry = startMeasure.staffEntries[0];
+      }
+      let endStaffEntry: GraphicalStaffEntry = endMeasure.findGraphicalStaffEntryFromTimestamp(endTimeStamp);
+      if (!endStaffEntry) { // fix for rendering range set
+        endStaffEntry = endMeasure.staffEntries[endMeasure.staffEntries.length - 1];
+      }
+      graphicalPedal.setStartNote(startStaffEntry);
+
+      if (endStaffLine !== startStaffLine) {
+        if(graphicalPedal.pedalSymbol === MusicSymbol.PEDAL_SYMBOL){
+          graphicalPedal.setEndNote(endStaffEntry);
+          graphicalPedal.ReleaseText = " ";
+          graphicalPedal.CalculateBoundingBox();
+          this.calculatePedalSkyBottomLine(graphicalPedal.startVfVoiceEntry, graphicalPedal.endVfVoiceEntry, graphicalPedal, startStaffLine);
+
+          const nextPedalFirstMeasure: GraphicalMeasure = endStaffLine.Measures[0];
+          // pedal starts on the first measure
+          const nextPedal: VexFlowPedal = new VexFlowPedal(pedal, nextPedalFirstMeasure.PositionAndShape);
+          const firstNote: GraphicalStaffEntry = nextPedalFirstMeasure.staffEntries[0];
+          nextPedal.setStartNote(firstNote);
+          nextPedal.setEndNote(endStaffEntry);
+          endStaffLine.Pedals.push(nextPedal);
+          nextPedal.CalculateBoundingBox();
+          nextPedal.DepressText = " ";
+          this.calculatePedalSkyBottomLine(nextPedal.startVfVoiceEntry, nextPedal.endVfVoiceEntry, nextPedal, endStaffLine);
+        } else {
+          let lastMeasureOfFirstShift: GraphicalMeasure = startStaffLine.Measures[startStaffLine.Measures.length - 1];
+          if (lastMeasureOfFirstShift === undefined) { // TODO handle this case correctly (when drawUpToMeasureNumber etc set)
+            lastMeasureOfFirstShift = endMeasure;
+          }
+          const lastNoteOfFirstShift: GraphicalStaffEntry = lastMeasureOfFirstShift.staffEntries[lastMeasureOfFirstShift.staffEntries.length - 1];
+          graphicalPedal.setEndNote(lastNoteOfFirstShift);
+
+          const systemsInBetweenCount: number = endStaffLine.ParentMusicSystem.Id - startStaffLine.ParentMusicSystem.Id;
+          if (systemsInBetweenCount > 0) {
+            //Loop through the stafflines in between to the end
+            let currentCount: number = 1;
+            for (let i: number = startStaffLine.ParentMusicSystem.Id; i < endStaffLine.ParentMusicSystem.Id; i++) {
+              const nextPedalMusicSystem: MusicSystem = this.musicSystems[i + 1];
+              const nextPedalStaffline: StaffLine = nextPedalMusicSystem.StaffLines[staffIndex];
+              const nextPedalFirstMeasure: GraphicalMeasure = nextPedalStaffline.Measures[0];
+              let nextOpenEnd: boolean = false;
+              if (currentCount < systemsInBetweenCount) {
+                nextOpenEnd = true;
+              }
+              currentCount++;
+              // pedal starts on the first measure
+              const nextPedal: VexFlowPedal = new VexFlowPedal(pedal, nextPedalFirstMeasure.PositionAndShape, true, nextOpenEnd);
+
+              let nextPedalLastMeasure: GraphicalMeasure = nextPedalStaffline.Measures[nextPedalStaffline.Measures.length - 1];
+              const firstNote: GraphicalStaffEntry = nextPedalFirstMeasure.staffEntries[0];
+              let lastNote: GraphicalStaffEntry = nextPedalLastMeasure.staffEntries[nextPedalLastMeasure.staffEntries.length - 1];
+
+              //If the end measure's staffline is the ending staffline, this endMeasure is the end of the pedal
+              if (endMeasure.ParentStaffLine === nextPedalStaffline) {
+                nextPedalLastMeasure = endMeasure;
+                lastNote = endStaffEntry;
+              }
+
+              nextPedal.setStartNote(firstNote);
+              nextPedal.setEndNote(lastNote);
+              nextPedalStaffline.Pedals.push(nextPedal);
+              nextPedal.CalculateBoundingBox();
+              this.calculatePedalSkyBottomLine(nextPedal.startVfVoiceEntry, nextPedal.endVfVoiceEntry, nextPedal, nextPedalStaffline);
+            }
+          }
+          graphicalPedal.CalculateBoundingBox();
+          this.calculatePedalSkyBottomLine(graphicalPedal.startVfVoiceEntry, graphicalPedal.endVfVoiceEntry, graphicalPedal, startStaffLine);
+        }
+      } else {
+        graphicalPedal.setEndNote(endStaffEntry);
+        graphicalPedal.CalculateBoundingBox();
+        this.calculatePedalSkyBottomLine(graphicalPedal.startVfVoiceEntry, graphicalPedal.endVfVoiceEntry, graphicalPedal, startStaffLine);
+      }
+      startStaffLine.Pedals.push(graphicalPedal);
+    } else {
+      log.warn("End measure or staffLines for pedal are undefined! This should not happen!");
+    }
+  }
+
+  protected calculateSingleWavyLine(sourceMeasure: SourceMeasure, multiExpression: MultiExpression, measureIndex: number, staffIndex: number): void {
+    // calculate absolute Timestamp and startStaffLine (and EndStaffLine if needed)
+    const wavyLine: WavyLine = multiExpression.WavyLineStart;
+
+    const startTimeStamp: Fraction = wavyLine.ParentStartMultiExpression.Timestamp;
+    const endTimeStamp: Fraction = wavyLine.ParentEndMultiExpression?.Timestamp;
+
+    const minMeasureToDrawIndex: number = this.rules.MinMeasureToDrawIndex;
+    const maxMeasureToDrawIndex: number = this.rules.MaxMeasureToDrawIndex;
+
+    let startStaffLine: StaffLine = this.graphicalMusicSheet.MeasureList[measureIndex][staffIndex].ParentStaffLine;
+    if (!startStaffLine) { // fix for rendering range set. all of these can probably be done cleaner.
+      startStaffLine = this.graphicalMusicSheet.MeasureList[minMeasureToDrawIndex][staffIndex].ParentStaffLine;
+    }
+    let endMeasure: GraphicalMeasure = undefined;
+    if (wavyLine.ParentEndMultiExpression) {
+      endMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(wavyLine.ParentEndMultiExpression.SourceMeasureParent,
+                                                                                          staffIndex);
+    } else {
+      endMeasure = this.graphicalMusicSheet.getLastGraphicalMeasureFromIndex(staffIndex, true); // get last rendered measure
+    }
+    if (endMeasure.MeasureNumber > maxMeasureToDrawIndex + 1) { //  ends in measure not rendered
+      endMeasure = this.graphicalMusicSheet.getLastGraphicalMeasureFromIndex(staffIndex, true);
+    }
+    let startMeasure: GraphicalMeasure = undefined;
+    if (wavyLine.ParentEndMultiExpression) {
+      startMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(wavyLine.ParentStartMultiExpression.SourceMeasureParent,
+                                                                                            staffIndex);
+    } else {
+      startMeasure = this.graphicalMusicSheet.MeasureList[minMeasureToDrawIndex][staffIndex]; // first rendered measure
+    }
+    if (startMeasure.MeasureNumber < minMeasureToDrawIndex + 1) { //  starts before range of measures selected to render
+      startMeasure = this.graphicalMusicSheet.MeasureList[minMeasureToDrawIndex][staffIndex]; // first rendered measure
+    }
+
+    if (startMeasure.parentSourceMeasure.measureListIndex < minMeasureToDrawIndex ||
+        startMeasure.parentSourceMeasure.measureListIndex > maxMeasureToDrawIndex ||
+        endMeasure.parentSourceMeasure.measureListIndex < minMeasureToDrawIndex ||
+        endMeasure.parentSourceMeasure.measureListIndex > maxMeasureToDrawIndex) {
+      // completely out of drawing range, don't draw anything
+      return;
+    }
+
+    let endStaffLine: StaffLine = endMeasure.ParentStaffLine;
+    if (!endStaffLine) {
+      endStaffLine = startStaffLine;
+    }
+    if (endMeasure && startStaffLine && endStaffLine) {
+      const graphicalWavyLine: VexflowVibratoBracket = new VexflowVibratoBracket(wavyLine, startStaffLine.PositionAndShape, startMeasure.ParentStaff.isTab);
+      // calculate RelativePosition
+      let startStaffEntry: GraphicalStaffEntry = startMeasure.findGraphicalStaffEntryFromTimestamp(startTimeStamp);
+      if (!startStaffEntry) { // fix for rendering range set
+        startStaffEntry = startMeasure.staffEntries[0];
+      }
+      let endStaffEntry: GraphicalStaffEntry = endMeasure.findGraphicalStaffEntryFromTimestamp(endTimeStamp);
+      if (!endStaffEntry) { // fix for rendering range set
+        endStaffEntry = endMeasure.staffEntries[endMeasure.staffEntries.length - 1];
+      }
+      graphicalWavyLine.setStartNote(startStaffEntry);
+
+      if (endStaffLine !== startStaffLine) {
+          let lastMeasureOfFirstShift: GraphicalMeasure = startStaffLine.Measures[startStaffLine.Measures.length - 1];
+          if (lastMeasureOfFirstShift === undefined) { // TODO handle this case correctly (when drawUpToMeasureNumber etc set)
+            lastMeasureOfFirstShift = endMeasure;
+          }
+          const lastNoteOfFirstShift: GraphicalStaffEntry = lastMeasureOfFirstShift.staffEntries[lastMeasureOfFirstShift.staffEntries.length - 1];
+          graphicalWavyLine.setEndNote(lastNoteOfFirstShift);
+
+          const systemsInBetweenCount: number = endStaffLine.ParentMusicSystem.Id - startStaffLine.ParentMusicSystem.Id;
+          if (systemsInBetweenCount > 0) {
+            for (let i: number = startStaffLine.ParentMusicSystem.Id; i < endStaffLine.ParentMusicSystem.Id; i++) {
+              const nextWavyLineMusicSystem: MusicSystem = this.musicSystems[i + 1];
+              const nextWavyLineStaffline: StaffLine = nextWavyLineMusicSystem.StaffLines[staffIndex];
+              const nextWavyLineFirstMeasure: GraphicalMeasure = nextWavyLineStaffline.Measures[0];
+              // vibrato starts on the first measure
+              const nextWavyLine: VexflowVibratoBracket = new VexflowVibratoBracket(wavyLine, nextWavyLineFirstMeasure.PositionAndShape,
+                nextWavyLineStaffline.ParentStaff.isTab);
+              let nextWavyLineLastMeasure: GraphicalMeasure = nextWavyLineStaffline.Measures[nextWavyLineStaffline.Measures.length - 1];
+              const firstNote: GraphicalStaffEntry = nextWavyLineFirstMeasure.staffEntries[0];
+              let lastNote: GraphicalStaffEntry = nextWavyLineLastMeasure.staffEntries[nextWavyLineLastMeasure.staffEntries.length - 1];
+              //If the end measure's is the ending staffline, this endMeasure is the end of the wavy line
+              if (endMeasure.ParentStaffLine === nextWavyLineStaffline) {
+                nextWavyLineLastMeasure = endMeasure;
+                lastNote = endStaffEntry;
+              }
+
+              nextWavyLine.setStartNote(firstNote);
+              nextWavyLine.setEndNote(lastNote);
+              nextWavyLineStaffline.WavyLines.push(nextWavyLine);
+              nextWavyLine.CalculateBoundingBox();
+              this.calculateWavyLineSkyBottomLine(nextWavyLine.startVfVoiceEntry, nextWavyLine.endVfVoiceEntry, nextWavyLine, nextWavyLineStaffline);
+            }
+          }
+          graphicalWavyLine.CalculateBoundingBox();
+          this.calculateWavyLineSkyBottomLine(graphicalWavyLine.startVfVoiceEntry, graphicalWavyLine.endVfVoiceEntry, graphicalWavyLine, startStaffLine);
+      } else {
+        graphicalWavyLine.setEndNote(endStaffEntry);
+        graphicalWavyLine.CalculateBoundingBox();
+        this.calculateWavyLineSkyBottomLine(graphicalWavyLine.startVfVoiceEntry, graphicalWavyLine.endVfVoiceEntry, graphicalWavyLine, startStaffLine);
+      }
+      startStaffLine.WavyLines.push(graphicalWavyLine);
+    } else {
+      log.warn("End measure or staffLines for wavy line are undefined! This should not happen!");
+    }
+  }
+
+  private calculateWavyLineSkyBottomLine(startVfVoiceEntry: VexFlowVoiceEntry, endVfVoiceEntry: VexFlowVoiceEntry,
+    vfVibratoBracket: VexflowVibratoBracket, parentStaffline: StaffLine): void {
+    const startStave: Vex.Flow.Stave = vfVibratoBracket.startNote.getStave();
+    const endStave: Vex.Flow.Stave = vfVibratoBracket.endNote.getStave();
+    //In VF Line positions, need to negate for our units
+    const highestVFTopTextPosition: number = Math.max(
+      startStave.options.top_text_position,
+      endStave.options.top_text_position
+    );
+
+    //Whichever is higher, set the other to match
+    startStave.options.top_text_position = highestVFTopTextPosition;
+    endStave.options.top_text_position = highestVFTopTextPosition;
+    let headroom: number = -highestVFTopTextPosition;
+    let trillStartX: number = 0;
+    let trillEndX: number = 0;
+    let trillSkyline: number = Infinity;
+    let trillWavyLineBottom: number = Infinity;
+    const TRILL_HEIGHT: number = 1.85;
+
+    let startX: number = startVfVoiceEntry.PositionAndShape.AbsolutePosition.x + startVfVoiceEntry.PositionAndShape.BorderLeft;
+    if (startVfVoiceEntry.parentVoiceEntry?.OrnamentContainer?.GetOrnament === OrnamentEnum.Trill) {
+      trillStartX = startX;
+      //Width of trill mark
+      startX += 2;
+      trillEndX = startX;
+      //Since the trill mark is not managed or calculated by our bounding boxes, we have to get the location this way
+      //Also at this point the skyline has already been updated with the trill mark. So we can't determine if it should go lower
+      //Need to trust Vexflow later on, unless the wavy line must be rendered higher
+      trillSkyline = parentStaffline.SkyBottomLineCalculator.getSkyLineMinInRange(trillStartX, trillEndX);
+      //height of the trill mark
+      trillWavyLineBottom = trillSkyline + TRILL_HEIGHT;
+    }
+
+    let stopX: number = undefined;
+    //If the end of the line is the last note in the measure, go all the way to the end of the stave
+    if(vfVibratoBracket.ToEndOfStopStave) {
+      //vexflow backs off by 1 unit (10 pixels) from stave edge
+      stopX = endVfVoiceEntry.parentStaffEntry.parentMeasure.PositionAndShape.AbsolutePosition.x +
+        endVfVoiceEntry.parentStaffEntry.parentMeasure.PositionAndShape.BorderRight - 1;
+    } else {
+      stopX = endVfVoiceEntry.PositionAndShape.AbsolutePosition.x + endVfVoiceEntry.PositionAndShape.BorderRight;
+      //Take into account in-staff clefs associated with the staff entry (they modify the bounding box position)
+      const vfClefBefore: Vex.Flow.ClefNote = (endVfVoiceEntry.parentStaffEntry as VexFlowStaffEntry).vfClefBefore;
+      if (vfClefBefore) {
+        const clefWidth: number = vfClefBefore.getWidth() / 10;
+        stopX += clefWidth;
+      }
+    }
+
+    headroom = parentStaffline.SkyBottomLineCalculator.getSkyLineMinInRange(startX, stopX);
+    if (headroom === Infinity) { // will cause Vexflow error
+      return;
+    }
+    //If somewhere in our wavy line path we have to render higher than where the trill mark is set...
+    if (headroom < trillSkyline) {
+      startStave.options.top_text_position = -headroom;
+      endStave.options.top_text_position = -headroom;
+      //A decent enough approximation. Better than recalculating via Canvas or SVG sampling
+      parentStaffline.SkyBottomLineCalculator.updateSkyLineInRange(trillStartX, trillEndX, headroom - TRILL_HEIGHT);
+    } else { //Else just render where Vexflow has set the trill mark
+      vfVibratoBracket.line = -trillWavyLineBottom;
+      headroom = trillWavyLineBottom;
+    }
+    //Update skyline to include height of the wavy line
+    headroom -= vfVibratoBracket.PositionAndShape.Size.height;
+    parentStaffline.SkyBottomLineCalculator.updateSkyLineInRange(startX, stopX, headroom);
+  }
+
+  private calculatePedalSkyBottomLine(startVfVoiceEntry: VexFlowVoiceEntry, endVfVoiceEntry: VexFlowVoiceEntry,
+    vfPedal: VexFlowPedal, parentStaffline: StaffLine): void {
+      //Just for shorthand. Easier readability below
+      const PEDAL_STYLES_ENUM: any = Vex.Flow.PedalMarking.Styles;
+      const pedalMarking: any = vfPedal.getPedalMarking();
+      //VF adds 3 to the line that is set for rendering pedal
+      const bottomLineYOffset: number = (pedalMarking.line + 3);
+      //VF Uses a margin offset for rendering. Take this into account
+      const pedalMarkingMarginXOffset: number = pedalMarking.render_options.text_margin_right / 10;
+      //TODO: Most of this should be in the bounding box calculation
+      let startX: number = startVfVoiceEntry.PositionAndShape.AbsolutePosition.x - pedalMarkingMarginXOffset;
+
+      if (pedalMarking.style === PEDAL_STYLES_ENUM.MIXED ||
+          pedalMarking.style === PEDAL_STYLES_ENUM.MIXED_OPEN_END ||
+          pedalMarking.style === PEDAL_STYLES_ENUM.TEXT) {
+        //Accomodate the Ped. sign
+        startX -= 1;
+      }
+      let stopX: number = undefined;
+      //We have the two seperate symbols, with two bounding boxes
+      if (vfPedal.EndSymbolPositionAndShape) {
+        //Width of the Ped. symbol
+        stopX = startX + 3.4;
+        const startX2: number = endVfVoiceEntry.PositionAndShape.AbsolutePosition.x - pedalMarkingMarginXOffset;
+        //Width of * symbol
+        const stopX2: number = startX2 + 1.5;
+
+        let footroom: number = parentStaffline.SkyBottomLineCalculator.getBottomLineMaxInRange(startX, stopX);
+        footroom = Math.max((vfPedal.startNote.getStave().options as any).bottom_text_position, footroom);
+        const footroom2: number = parentStaffline.SkyBottomLineCalculator.getBottomLineMaxInRange(startX2, stopX2);
+        //If Depress text is set, means we are not rendering the begin label (we are just rendering the end one)
+        if (!vfPedal.DepressText) {
+          footroom = Math.max(footroom, footroom2);
+        }
+        if (startVfVoiceEntry.parentStaffEntry.parentMeasure !== endVfVoiceEntry.parentStaffEntry.parentMeasure) {
+          //TODO: look into using the vexflow setLine instead of modifying the whole stave if possible
+          footroom = Math.max((vfPedal.endNote.getStave().options as any).bottom_text_position, footroom);
+          (vfPedal.endNote.getStave().options as any).bottom_text_position = footroom;
+        }
+        (vfPedal.startNote.getStave().options as any).bottom_text_position = footroom;
+        parentStaffline.SkyBottomLineCalculator.updateBottomLineInRange(startX, stopX, footroom + bottomLineYOffset);
+        parentStaffline.SkyBottomLineCalculator.updateBottomLineInRange(startX2, stopX2, footroom + bottomLineYOffset);
+      } else {
+        switch (pedalMarking.style) {
+          case PEDAL_STYLES_ENUM.BRACKET_OPEN_END:
+          case PEDAL_STYLES_ENUM.BRACKET_OPEN_BOTH:
+          case PEDAL_STYLES_ENUM.MIXED_OPEN_END:
+            stopX = endVfVoiceEntry.PositionAndShape.AbsolutePosition.x + endVfVoiceEntry.PositionAndShape.BorderRight - pedalMarkingMarginXOffset;
+          break;
+          default:
+            stopX = endVfVoiceEntry.PositionAndShape.AbsolutePosition.x + endVfVoiceEntry.PositionAndShape.BorderLeft - pedalMarkingMarginXOffset;
+          break;
+        }
+        //Take into account in-staff clefs associated with the staff entry (they modify the bounding box position)
+        const vfClefBefore: Vex.Flow.ClefNote = (endVfVoiceEntry.parentStaffEntry as VexFlowStaffEntry).vfClefBefore;
+        if (vfClefBefore) {
+          const clefWidth: number = vfClefBefore.getWidth() / 10;
+          stopX += clefWidth;
+        }
+
+        let footroom: number = parentStaffline.SkyBottomLineCalculator.getBottomLineMaxInRange(startX, stopX);
+        if (footroom === Infinity) { // will cause Vexflow error
+          return;
+        }
+        //Whatever is currently lower - the set render height of the begin vf stave, the set render height of the end vf stave,
+        //or the bottom line. Use that as the render height of both staves
+        footroom = Math.max(footroom, (vfPedal.startNote.getStave().options as any).bottom_text_position);
+        if (startVfVoiceEntry.parentStaffEntry.parentMeasure !== endVfVoiceEntry.parentStaffEntry.parentMeasure) {
+          footroom = Math.max(footroom, (vfPedal.endNote.getStave().options as any).bottom_text_position);
+          (vfPedal.endNote.getStave().options as any).bottom_text_position = footroom;
+        }
+        (vfPedal.startNote.getStave().options as any).bottom_text_position = footroom;
+        parentStaffline.SkyBottomLineCalculator.updateBottomLineInRange(startX, stopX, footroom + bottomLineYOffset);
+        //This list will contain only previously processed Pedals - aka, those previous in the sheet music
+        for (const otherPedal of parentStaffline.Pedals) {
+          const vfOtherPedal: VexFlowPedal = otherPedal as VexFlowPedal;
+          //If there is a pedal that ends on our begin measure, we need to update its begin measure to render at the new height
+          //So we don't get lopsided pedal brackets
+          if (vfOtherPedal.endVfVoiceEntry.parentStaffEntry.parentMeasure === startVfVoiceEntry.parentStaffEntry.parentMeasure) {
+            //Since we've already checked for max height for this stave above, we are certain that this pedal will not render higher than it should
+            //(e.g. colliding with stuff)
+            (vfOtherPedal.startNote.getStave().options as any).bottom_text_position = footroom;
+            const otherPedalMarking: any = vfOtherPedal.getPedalMarking();
+            const otherPedalMarkingMarginXOffset: number = otherPedalMarking.render_options.text_margin_right / 10;
+            const otherPedalStartX: number = vfOtherPedal.startVfVoiceEntry.PositionAndShape.AbsolutePosition.x - otherPedalMarkingMarginXOffset;
+            let otherPedalStopX: number = undefined;
+            switch (otherPedalMarking.style) {
+              case PEDAL_STYLES_ENUM.BRACKET_OPEN_END:
+              case PEDAL_STYLES_ENUM.BRACKET_OPEN_BOTH:
+              case PEDAL_STYLES_ENUM.MIXED_OPEN_END:
+                otherPedalStopX = vfOtherPedal.endVfVoiceEntry.PositionAndShape.AbsolutePosition.x +
+                                  vfOtherPedal.endVfVoiceEntry.PositionAndShape.BorderRight - otherPedalMarkingMarginXOffset;
+              break;
+              default:
+                otherPedalStopX = vfOtherPedal.endVfVoiceEntry.PositionAndShape.AbsolutePosition.x +
+                vfOtherPedal.endVfVoiceEntry.PositionAndShape.BorderLeft - otherPedalMarkingMarginXOffset;
+              break;
+            }
+            //Take into account in-staff clefs associated with the staff entry (they modify the bounding box position)
+            const otherPedalVfClefBefore: Vex.Flow.ClefNote = (vfOtherPedal.endVfVoiceEntry.parentStaffEntry as VexFlowStaffEntry).vfClefBefore;
+            if (otherPedalVfClefBefore) {
+              const clefWidth: number = otherPedalVfClefBefore.getWidth() / 10;
+              stopX += clefWidth;
+            }
+            const otherPedalBottomLineYOffset: number = (pedalMarking.line + 3);
+            parentStaffline.SkyBottomLineCalculator.updateBottomLineInRange(otherPedalStartX, otherPedalStopX, footroom + otherPedalBottomLineYOffset);
+          }
+        }
+      }
+  }
+
   private calculateOctaveShiftSkyBottomLine(startStaffEntry: GraphicalStaffEntry, endStaffEntry: GraphicalStaffEntry,
                                             vfOctaveShift: VexFlowOctaveShift, parentStaffline: StaffLine): void {
 
@@ -987,6 +1438,9 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         return;
       }
       (textBracket.start.getStave().options as any).bottom_text_position = footroom;
+      if (startStaffEntry.parentMeasure !== endStaffEntry.parentMeasure) {
+        (textBracket.stop.getStave().options as any).bottom_text_position = footroom;
+      }
       //Vexflow positions top vs. bottom text in a slightly inconsistent way it seems
       parentStaffline.SkyBottomLineCalculator.updateBottomLineInRange(startX, stopX, footroom + fontSize * 1.5);
     }

+ 32 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts

@@ -28,6 +28,8 @@ import { DrawingParameters } from "../DrawingParameters";
 import { GraphicalMusicPage } from "../GraphicalMusicPage";
 import { GraphicalMusicSheet } from "../GraphicalMusicSheet";
 import { GraphicalUnknownExpression } from "../GraphicalUnknownExpression";
+import { VexFlowPedal } from "./VexFlowPedal";
+import { VexflowVibratoBracket } from "./VexflowVibratoBracket";
 
 /**
  * This is a global constant which denotes the height in pixels of the space between two lines of the stave
@@ -152,6 +154,12 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
         curvePointsInPixels.push(this.applyScreenTransformation(p2));
         curvePointsInPixels.push(this.applyScreenTransformation(p3));
         curvePointsInPixels.push(this.applyScreenTransformation(p4));
+        //DEBUG: Render control points
+        /*
+        for (const point of curvePointsInPixels) {
+            const pointRect: RectangleF2D = new RectangleF2D(point.x - 2, point.y - 2, 4, 4);
+            this.backend.renderRectangle(pointRect, 3, "#000000", 1);
+        }*/
 
         // 2) create second outer curve to create a thickness for the curve:
         if (graphicalSlur.placement === PlacementEnum.Above) {
@@ -359,6 +367,30 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
         }
     }
 
+    protected drawPedals(staffLine: StaffLine): void {
+        for (const graphicalPedal of staffLine.Pedals) {
+            if (graphicalPedal) {
+                const vexFlowPedal: VexFlowPedal = graphicalPedal as VexFlowPedal;
+                const ctx: Vex.IRenderContext = this.backend.getContext();
+                const pedalMarking: Vex.Flow.PedalMarking = vexFlowPedal.getPedalMarking();
+                pedalMarking.setContext(ctx);
+                pedalMarking.draw();
+            }
+        }
+    }
+
+    protected drawWavyLines(staffLine: StaffLine): void {
+        for (const graphicalWavyLine of staffLine.WavyLines) {
+            if (graphicalWavyLine) {
+                const vexFlowVibratoBracket: VexflowVibratoBracket = graphicalWavyLine as VexflowVibratoBracket;
+                const ctx: Vex.IRenderContext = this.backend.getContext();
+                const vfVibratoBracket: Vex.Flow.VibratoBracket = vexFlowVibratoBracket.getVibratoBracket();
+                (vfVibratoBracket as any).setContext(ctx);
+                vfVibratoBracket.draw();
+            }
+        }
+    }
+
     protected drawExpressions(staffline: StaffLine): void {
         // Draw all Expressions
         for (const abstractGraphicalExpression of staffline.AbstractExpressions) {

+ 109 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowPedal.ts

@@ -0,0 +1,109 @@
+import Vex from "vexflow";
+import { BoundingBox } from "../BoundingBox";
+import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
+import { VexFlowVoiceEntry } from "./VexFlowVoiceEntry";
+import { GraphicalPedal } from "../GraphicalPedal";
+import { Pedal } from "../../VoiceData/Expressions/ContinuousExpressions/Pedal";
+import { MusicSymbol } from "../MusicSymbol";
+
+/**
+ * The vexflow adaptation of a pedal marking
+ */
+export class VexFlowPedal extends GraphicalPedal {
+    /** Defines the note where the pedal starts */
+    public startNote: Vex.Flow.StemmableNote;
+    /** Defines the note where the pedal ends */
+    public endNote: Vex.Flow.StemmableNote;
+    private vfStyle: Vex.Flow.PedalMarking.Styles = Vex.Flow.PedalMarking.Styles.BRACKET;
+    public DepressText: string;
+    public ReleaseText: string;
+    public startVfVoiceEntry: VexFlowVoiceEntry;
+    public endVfVoiceEntry: VexFlowVoiceEntry;
+
+    public EndSymbolPositionAndShape: BoundingBox = undefined;
+    /**
+     * Create a new vexflow pedal marking
+     * @param pedal the object read by the ExpressionReader
+     * @param parent the bounding box of the parent
+     */
+    constructor(pedal: Pedal, parent: BoundingBox, openBegin: boolean = false, openEnd: boolean = false) {
+        super(pedal, parent);
+        switch (this.pedalSymbol) {
+            case MusicSymbol.PEDAL_SYMBOL:
+                //This renders the pedal symbols in VF.
+                this.vfStyle = Vex.Flow.PedalMarking.Styles.TEXT;
+                this.EndSymbolPositionAndShape = new BoundingBox(this, parent);
+            break;
+            case MusicSymbol.PEDAL_MIXED:
+                if (openBegin && openEnd) {
+                    this.vfStyle = (Vex.Flow.PedalMarking.Styles as any).BRACKET_OPEN_BOTH;
+                } else if (openBegin) {
+                    this.vfStyle = (Vex.Flow.PedalMarking.Styles as any).BRACKET_OPEN_BEGIN;
+                } else if (openEnd) {
+                    this.vfStyle = (Vex.Flow.PedalMarking.Styles as any).MIXED_OPEN_END;
+                } else {
+                    this.vfStyle = Vex.Flow.PedalMarking.Styles.MIXED;
+                }
+            break;
+            case MusicSymbol.PEDAL_BRACKET:
+            default:
+                if (openBegin && openEnd) {
+                    this.vfStyle = (Vex.Flow.PedalMarking.Styles as any).BRACKET_OPEN_BOTH;
+                } else if (openBegin) {
+                    this.vfStyle = (Vex.Flow.PedalMarking.Styles as any).BRACKET_OPEN_BEGIN;
+                } else if (openEnd) {
+                    this.vfStyle = (Vex.Flow.PedalMarking.Styles as any).BRACKET_OPEN_END;
+                } else {
+                    this.vfStyle = Vex.Flow.PedalMarking.Styles.BRACKET;
+                }
+            break;
+        }
+    }
+
+    /**
+     * Set a start note using a staff entry
+     * @param graphicalStaffEntry the staff entry that holds the start note
+     */
+    public setStartNote(graphicalStaffEntry: GraphicalStaffEntry): boolean {
+        for (const gve of graphicalStaffEntry.graphicalVoiceEntries) {
+            const vve: VexFlowVoiceEntry = (gve as VexFlowVoiceEntry);
+            if (vve?.vfStaveNote) {
+                this.startNote = vve.vfStaveNote;
+                this.startVfVoiceEntry = vve;
+                return true;
+            }
+        }
+        return false; // couldn't find a startNote
+    }
+
+    /**
+     * Set an end note using a staff entry
+     * @param graphicalStaffEntry the staff entry that holds the end note
+     */
+    public setEndNote(graphicalStaffEntry: GraphicalStaffEntry): boolean {
+        // this is duplicate code from setStartNote, but if we make one general method, we add a lot of branching.
+        for (const gve of graphicalStaffEntry.graphicalVoiceEntries) {
+            const vve: VexFlowVoiceEntry = (gve as VexFlowVoiceEntry);
+            if (vve?.vfStaveNote) {
+                this.endNote = vve.vfStaveNote;
+                this.endVfVoiceEntry = vve;
+                return true;
+            }
+        }
+        return false; // couldn't find an endNote
+    }
+
+    public CalculateBoundingBox(): void {
+        //TODO?
+    }
+    /**
+     * Get the actual vexflow Pedal Marking used for drawing
+     */
+    public getPedalMarking(): Vex.Flow.PedalMarking {
+        const pedalMarking: Vex.Flow.PedalMarking = new Vex.Flow.PedalMarking([this.startNote, this.endNote]);
+        pedalMarking.setStyle(this.vfStyle);
+        pedalMarking.setLine(-1);
+        pedalMarking.setCustomText(this.DepressText, this.ReleaseText);
+        return pedalMarking;
+    }
+}

+ 86 - 0
src/MusicalScore/Graphical/VexFlow/VexflowVibratoBracket.ts

@@ -0,0 +1,86 @@
+import { WavyLine } from "../../VoiceData/Expressions/ContinuousExpressions/WavyLine";
+import { BoundingBox } from "../BoundingBox";
+import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
+import { GraphicalWavyLine } from "../GraphicalWavyLine";
+import { VexFlowVoiceEntry } from "./VexFlowVoiceEntry";
+import Vex from "vexflow";
+
+export class VexflowVibratoBracket extends GraphicalWavyLine {
+    /** Defines the note where the pedal starts */
+    public startNote: Vex.Flow.StemmableNote;
+    /** Defines the note where the pedal ends */
+    public endNote: Vex.Flow.StemmableNote;
+    public startVfVoiceEntry: VexFlowVoiceEntry;
+    public endVfVoiceEntry: VexFlowVoiceEntry;
+    //Line where vexflow renders the bracket. VF default is 1
+    public line: number = 1;
+    private isVibrato: boolean = false;
+    private toEndOfStopStave: boolean = false;
+    public get ToEndOfStopStave(): boolean {
+        return this.toEndOfStopStave;
+    }
+
+    constructor(wavyLine: WavyLine, parentBBox: BoundingBox, tabVibrato: boolean = false) {
+        super(wavyLine, parentBBox);
+        this.isVibrato = tabVibrato;
+    }
+
+    /**
+     * Set a start note using a staff entry
+     * @param graphicalStaffEntry the staff entry that holds the start note
+     */
+     public setStartNote(graphicalStaffEntry: GraphicalStaffEntry): boolean {
+        for (const gve of graphicalStaffEntry.graphicalVoiceEntries) {
+            const vve: VexFlowVoiceEntry = (gve as VexFlowVoiceEntry);
+            if (vve?.vfStaveNote) {
+                this.startNote = vve.vfStaveNote;
+                this.startVfVoiceEntry = vve;
+                return true;
+            }
+        }
+        return false; // couldn't find a startNote
+    }
+
+    /**
+     * Set an end note using a staff entry
+     * @param graphicalStaffEntry the staff entry that holds the end note
+     */
+    public setEndNote(graphicalStaffEntry: GraphicalStaffEntry): boolean {
+        // this is duplicate code from setStartNote, but if we make one general method, we add a lot of branching.
+        for (const gve of graphicalStaffEntry.graphicalVoiceEntries) {
+            const vve: VexFlowVoiceEntry = (gve as VexFlowVoiceEntry);
+            if (vve?.vfStaveNote) {
+                this.endNote = vve.vfStaveNote;
+                this.endVfVoiceEntry = vve;
+                const parentMeasureStaffEntries: GraphicalStaffEntry[] = this.endVfVoiceEntry.parentStaffEntry.parentMeasure.staffEntries;
+                const lastStaffEntry: GraphicalStaffEntry = parentMeasureStaffEntries[parentMeasureStaffEntries.length - 1];
+                //If this is the last staff entry of the stave (measure), render line to end of measure
+                this.toEndOfStopStave = (lastStaffEntry === this.endVfVoiceEntry.parentStaffEntry);
+                return true;
+            }
+        }
+        return false; // couldn't find an endNote
+    }
+
+    public CalculateBoundingBox(): void {
+        const vfBracket: any = this.getVibratoBracket();
+        //Double the height of the wave, coverted to units
+        this.boundingBox.Size.height = vfBracket.render_options.wave_height * 0.2;
+    }
+
+    public getVibratoBracket(): Vex.Flow.VibratoBracket {
+		const bracket: Vex.Flow.VibratoBracket = new Vex.Flow.VibratoBracket({
+			start: this.startNote,
+			stop: this.endNote,
+            toEndOfStopStave: this.toEndOfStopStave
+		});
+        bracket.setLine(this.line);
+        if (this.isVibrato) {
+			//Render options for vibrato style
+			(bracket as any).render_options.vibrato_width = 20;
+		} else {
+			(bracket as any).render_options.wave_girth = 4;
+		}
+		return bracket;
+    }
+}

+ 24 - 9
src/MusicalScore/ScoreIO/InstrumentReader.ts

@@ -296,7 +296,6 @@ export class InstrumentReader {
 
           // check Tremolo
           let tremoloStrokes: number = 0;
-          let vibratoStrokes: boolean = false;
           if (notationsNode) {
             const ornamentsNode: IXmlElement = notationsNode.element("ornaments");
             if (ornamentsNode) {
@@ -311,12 +310,23 @@ export class InstrumentReader {
                 }
                 // TODO implement type "start". Vexflow doesn't have tremolo beams yet though (shorter than normal beams)
               }
-              const vibratoNode: IXmlElement = ornamentsNode.element("wavy-line");
-              if (vibratoNode !== undefined) {
-                const vibratoType: Attr = vibratoNode.attribute("type");
-                if (vibratoType && vibratoType.value === "start") {
-                  vibratoStrokes = true;
-                }
+              const wavyLineNodes: IXmlElement[] = ornamentsNode.elements("wavy-line");
+              if (wavyLineNodes !== undefined) {
+                /* As mentioned elsewhere, the wavy-line is technically an ornament element, but is specified and behaves
+                   very much like a continuous expression, so makes more sense to interpret as an expression in our model.
+                */
+               for (const wavyLineNode of wavyLineNodes) {
+                  const expressionReader: ExpressionReader = this.expressionReaders[this.readExpressionStaffNumber(xmlNode) - 1];
+                  if (expressionReader) {
+                    //Read placement from the wavy line node
+                  expressionReader.readExpressionParameters(
+                    wavyLineNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, false
+                  );
+                  expressionReader.addWavyLine(
+                    wavyLineNode, this.currentMeasure, currentFraction, previousFraction
+                  );
+                  }
+               }
               }
             }
           }
@@ -398,8 +408,7 @@ export class InstrumentReader {
             this.currentStaffEntry, this.currentMeasure,
             measureStartAbsoluteTimestamp,
             this.maxTieNoteFraction, isChord, octavePlusOne,
-            printObject, isCueNote, isGraceNote, stemDirectionXml, tremoloStrokes, stemColorXml, noteheadColorXml,
-            vibratoStrokes
+            printObject, isCueNote, isGraceNote, stemDirectionXml, tremoloStrokes, stemColorXml, noteheadColorXml
           );
 
           // notationsNode created further up for multiple checks
@@ -532,6 +541,12 @@ export class InstrumentReader {
                );
                expressionReader.addOctaveShift(xmlNode, this.currentMeasure, previousFraction.clone());
              }
+             if (directionTypeNode.element("pedal")) {
+              expressionReader.readExpressionParameters(
+                xmlNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, true
+              );
+              expressionReader.addPedalMarking(xmlNode, this.currentMeasure, previousFraction.clone());
+             }
              expressionReader.readExpressionParameters(
                xmlNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, false
              );

+ 79 - 0
src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts

@@ -18,6 +18,8 @@ import {ITextTranslation} from "../../Interfaces/ITextTranslation";
 import log from "loglevel";
 import { FontStyles } from "../../../Common/Enums/FontStyles";
 import { RehearsalExpression } from "../../VoiceData/Expressions/RehearsalExpression";
+import { Pedal } from "../../VoiceData/Expressions/ContinuousExpressions/Pedal";
+import { WavyLine } from "../../VoiceData/Expressions/ContinuousExpressions/WavyLine";
 
 export class ExpressionReader {
     private musicSheet: MusicSheet;
@@ -33,6 +35,8 @@ export class ExpressionReader {
     private openContinuousTempoExpression: ContinuousTempoExpression;
     private activeInstantaneousDynamic: InstantaneousDynamicExpression;
     private openOctaveShift: OctaveShift;
+    private openPedal: Pedal;
+    private openWavyLine: WavyLine;
     constructor(musicSheet: MusicSheet, instrument: Instrument, staffNumber: number) {
         this.musicSheet = musicSheet;
         this.staffNumber = staffNumber;
@@ -300,6 +304,81 @@ export class ExpressionReader {
             }
         }
     }
+    public addPedalMarking(directionNode: IXmlElement, currentMeasure: SourceMeasure, endTimestamp: Fraction): void {
+        const directionTypeNode: IXmlElement = directionNode.element("direction-type");
+        if (directionTypeNode) {
+            const pedalNode: IXmlElement = directionTypeNode.element("pedal");
+            if (pedalNode !== undefined && pedalNode.hasAttributes) {
+                let sign: boolean = false, line: boolean = false;
+                try {
+                    if (pedalNode.attribute("line")?.value === "yes") {
+                        line = true;
+                    }
+                    if (pedalNode.attribute("sign")?.value === "yes") {
+                        sign = true;
+                    }
+                    switch (pedalNode.attribute("type").value) {
+                        case "start":
+                            this.createNewMultiExpressionIfNeeded(currentMeasure);
+                            this.openPedal = new Pedal(line, sign);
+                            this.getMultiExpression.PedalStart = this.openPedal;
+                            this.openPedal.ParentStartMultiExpression = this.getMultiExpression;
+                        break;
+                        case "stop":
+                            if (this.openPedal) {
+                                this.createNewMultiExpressionIfNeeded(currentMeasure);
+                                this.getMultiExpression.PedalEnd = this.openPedal;
+                                this.openPedal.ParentEndMultiExpression = this.getMultiExpression;
+                                this.openPedal = undefined;
+                            }
+                        break;
+                        case "change":
+                            //VF doesn't seem to support this V marking
+                        break;
+                        case "continue":
+                        break;
+                        default:
+                        break;
+                    }
+                } catch (ex) {
+                    const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/PedalError", "Error while reading pedal.");
+                    this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                    log.debug("ExpressionReader.addPedalMarking", errorMsg, ex);
+                }
+            }
+        }
+    }
+    public addWavyLine(wavyLineNode: IXmlElement, currentMeasure: SourceMeasure, currentTimestamp: Fraction, previousTimestamp: Fraction): void {
+        if (wavyLineNode && wavyLineNode.hasAttributes) {
+            try {
+                switch (wavyLineNode.attribute("type").value) {
+                    case "start":
+                        this.createNewMultiExpressionIfNeeded(currentMeasure);
+                        this.openWavyLine = new WavyLine(this.placement);
+                        this.getMultiExpression.WavyLineStart = this.openWavyLine;
+                        this.openWavyLine.ParentStartMultiExpression = this.getMultiExpression;
+                    break;
+                    case "stop":
+                        if (this.openWavyLine) {
+                            this.createNewMultiExpressionIfNeeded(currentMeasure, currentTimestamp);
+                            this.getMultiExpression.WavyLineEnd = this.openWavyLine;
+                            this.openWavyLine.ParentEndMultiExpression = this.getMultiExpression;
+                            this.openWavyLine = undefined;
+                        }
+                    break;
+                    case "continue":
+                        //Seems we can ignore this for now. TODO: Look into when this is a barline child
+                    break;
+                    default:
+                    break;
+                }
+            } catch (ex) {
+                const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/WavyLineError", "Error while reading wavy-line.");
+                this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                log.debug("ExpressionReader.addWavyLine", errorMsg, ex);
+            }
+        }
+    }
     private initialize(): void {
         this.placement = PlacementEnum.NotYetDefined;
         this.soundTempo = 0;

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

@@ -154,7 +154,7 @@ export class VoiceGenerator {
               parentStaffEntry: SourceStaffEntry, parentMeasure: SourceMeasure,
               measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, chord: boolean, octavePlusOne: boolean,
               printObject: boolean, isCueNote: boolean, isGraceNote: boolean, stemDirectionXml: StemDirectionType, tremoloStrokes: number,
-              stemColorXml: string, noteheadColorXml: string, vibratoStrokes: boolean): Note {
+              stemColorXml: string, noteheadColorXml: string): Note {
     this.currentStaffEntry = parentStaffEntry;
     this.currentMeasure = parentMeasure;
     //log.debug("read called:", restNote);
@@ -163,7 +163,7 @@ export class VoiceGenerator {
       this.currentNote = restNote
         ? this.addRestNote(noteNode.element("rest"), noteDuration, noteTypeXml, normalNotes, printObject, isCueNote, noteheadColorXml)
         : this.addSingleNote(noteNode, noteDuration, noteTypeXml, typeDuration, normalNotes, chord, octavePlusOne,
-                             printObject, isCueNote, isGraceNote, stemDirectionXml, tremoloStrokes, stemColorXml, noteheadColorXml, vibratoStrokes);
+                             printObject, isCueNote, isGraceNote, stemDirectionXml, tremoloStrokes, stemColorXml, noteheadColorXml);
       // read lyrics
       const lyricElements: IXmlElement[] = noteNode.elements("lyric");
       if (this.lyricsReader !== undefined && lyricElements) {
@@ -364,7 +364,7 @@ export class VoiceGenerator {
   private addSingleNote(node: IXmlElement, noteDuration: Fraction, noteTypeXml: NoteType, typeDuration: Fraction,
                         normalNotes: number, chord: boolean, octavePlusOne: boolean,
                         printObject: boolean, isCueNote: boolean, isGraceNote: boolean, stemDirectionXml: StemDirectionType, tremoloStrokes: number,
-                        stemColorXml: string, noteheadColorXml: string, vibratoStrokes: boolean): Note {
+                        stemColorXml: string, noteheadColorXml: string): Note {
     //log.debug("addSingleNote called");
     let noteAlter: number = 0;
     let noteAccidental: AccidentalEnum = AccidentalEnum.NONE;
@@ -504,7 +504,7 @@ export class VoiceGenerator {
     } else {
       // create TabNote
       note = new TabNote(this.currentVoiceEntry, this.currentStaffEntry, noteLength, pitch, this.currentMeasure,
-                         stringNumber, fretNumber, bends, vibratoStrokes);
+                         stringNumber, fretNumber, bends);
     }
 
     this.addNoteInfo(note, noteTypeXml, printObject, isCueNote, normalNotes,

+ 21 - 0
src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/Pedal.ts

@@ -0,0 +1,21 @@
+import { MultiExpression } from "../MultiExpression";
+
+export class Pedal {
+    constructor(line: boolean = false, sign: boolean = true) {
+        this.line = line;
+        this.sign = sign;
+    }
+
+    private line: boolean;
+    private sign: boolean;
+    public StaffNumber: number;
+    public ParentStartMultiExpression: MultiExpression;
+    public ParentEndMultiExpression: MultiExpression;
+
+    public get IsLine(): boolean {
+        return this.line;
+    }
+    public get IsSign(): boolean {
+        return this.sign;
+    }
+}

+ 12 - 0
src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/WavyLine.ts

@@ -0,0 +1,12 @@
+import { AbstractExpression, PlacementEnum } from "../AbstractExpression";
+import { MultiExpression } from "../MultiExpression";
+//Represents the wavy-line element in musicxml
+//Technically not an expression, but an ornament... But behaves very much like an expression line.
+export class WavyLine extends AbstractExpression {
+    constructor(placement: PlacementEnum) {
+        super(placement);
+    }
+
+    public ParentStartMultiExpression: MultiExpression;
+    public ParentEndMultiExpression: MultiExpression;
+}

+ 6 - 0
src/MusicalScore/VoiceData/Expressions/MultiExpression.ts

@@ -8,6 +8,8 @@ import {UnknownExpression} from "./UnknownExpression";
 import {AbstractExpression} from "./AbstractExpression";
 import {PlacementEnum} from "./AbstractExpression";
 import { FontStyles } from "../../../Common/Enums/FontStyles";
+import { Pedal } from "./ContinuousExpressions/Pedal";
+import { WavyLine } from "./ContinuousExpressions/WavyLine";
 
 export class MultiExpression {
 
@@ -28,6 +30,10 @@ export class MultiExpression {
     private combinedExpressionsText: string;
     private octaveShiftStart: OctaveShift;
     private octaveShiftEnd: OctaveShift;
+    public PedalStart: Pedal;
+    public PedalEnd: Pedal;
+    public WavyLineStart: WavyLine;
+    public WavyLineEnd: WavyLine;
 
     public get SourceMeasureParent(): SourceMeasure {
         return this.sourceMeasure;

+ 1 - 8
src/MusicalScore/VoiceData/TabNote.ts

@@ -7,19 +7,16 @@ import { SourceMeasure } from "./SourceMeasure";
 
 export class TabNote extends Note {
     constructor(voiceEntry: VoiceEntry, parentStaffEntry: SourceStaffEntry, length: Fraction, pitch: Pitch, sourceMeasure: SourceMeasure,
-                stringNumber: number, fretNumber: number, bendArray: { bendalter: number, direction: string }[],
-                vibratoStroke: boolean) {
+                stringNumber: number, fretNumber: number, bendArray: { bendalter: number, direction: string }[]) {
         super(voiceEntry, parentStaffEntry, length, pitch, sourceMeasure);
         this.stringNumberTab = stringNumber;
         this.fretNumber = fretNumber;
         this.bendArray = bendArray;
-        this.vibratoStroke = vibratoStroke;
     }
 
     private stringNumberTab: number; // there can also be string numbers for e.g. violin in treble clef.
     private fretNumber: number;
     private bendArray: { bendalter: number, direction: string }[];
-    private vibratoStroke: boolean;
 
     /** Returns the string number the note should be played on. Note there can also be violin string numbers in treble clef. */
     public get StringNumberTab(): number {
@@ -33,8 +30,4 @@ export class TabNote extends Note {
     public get BendArray(): { bendalter: number, direction: string }[] {
         return this.bendArray;
     }
-
-    public get VibratoStroke(): boolean {
-        return this.vibratoStroke;
-    }
 }

+ 10 - 0
src/VexFlowPatch/readme.txt

@@ -13,11 +13,18 @@ Each .js has comments like "// VexFlowPatch: [explanation]" to indicate what was
 beam.js (custom addition):
 add flat_beams, flat_beam_offset, flat_beam_offset_per_beam render_option
 
+pedalmarking.js (custom addition):
+Add rendering options for pedals that break across systems.
+
 stave.js (custom addition):
 prevent a bug where a modifier width is NaN, leading to a VexFlow error
 stave.setSection(section, y, xOffset = 0, fontSize = 12):
 add xOffset, fontSize arguments (see stavesection.js)
 
+stavenote.js (custom addition):
+Fix stem/flag formatting. Instead of shifting notes by default, update the stem/flag rendering to render different voices aligned.
+Only offset if a note is the same voice, same note.
+
 staverepetition.js (custom addition):
 add TO_CODA enum to type() and draw()
 fix x-positioning for TO_CODA and DS_AL_CODA in drawSymbolText()
@@ -31,6 +38,9 @@ stavevolta.js (merged Vexflow 3.x):
 Fix the length of voltas for first measures in a system
 (whose lengths were wrongly extended by the width of the clef, key signature, etc. (beginInstructions) in Vexflow 1.2.93)
 
+stemmablenote.js (custom addition):
+Add manual flag rendering variable so we can choose not to render flags if notes are sharing a stem.
+
 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")
 

+ 298 - 0
src/VexFlowPatch/src/pedalmarking.js

@@ -0,0 +1,298 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+//
+// ## Description
+//
+// This file implements different types of pedal markings. These notation
+// elements indicate to the performer when to depress and release the a pedal.
+//
+// In order to create "Sostenuto", and "una corda" markings, you must set
+// custom text for the release/depress pedal markings.
+
+import { Vex } from './vex';
+import { Element } from './element';
+import { Glyph } from './glyph';
+
+// To enable logging for this class. Set `Vex.Flow.PedalMarking.DEBUG` to `true`.
+function L(...args) { if (PedalMarking.DEBUG) Vex.L('Vex.Flow.PedalMarking', args); }
+
+// Draws a pedal glyph with the provided `name` on a rendering `context`
+// at the coordinates `x` and `y. Takes into account the glyph data
+// coordinate shifts.
+function drawPedalGlyph(name, context, x, y, point) {
+  const glyph_data = PedalMarking.GLYPHS[name];
+  const glyph = new Glyph(glyph_data.code, point);
+  glyph.render(context, x + glyph_data.x_shift, y + glyph_data.y_shift);
+}
+
+export class PedalMarking extends Element {
+  // Glyph data
+  static get GLYPHS() {
+    return {
+      'pedal_depress': {
+        code: 'v36',
+        x_shift: -10,
+        y_shift: 0,
+      },
+      'pedal_release': {
+        code: 'v5d',
+        x_shift: -2,
+        y_shift: 3,
+      },
+    };
+  }
+
+  static get Styles() {
+    return {
+      TEXT: 1,
+      BRACKET: 2,
+      MIXED: 3,
+      MIXED_OPEN_END: 4,
+      BRACKET_OPEN_BEGIN: 5,
+      BRACKET_OPEN_END: 6,
+      BRACKET_OPEN_BOTH: 7
+    };
+  }
+
+  static get StylesString() {
+    return {
+      text: PedalMarking.Styles.TEXT,
+      bracket: PedalMarking.Styles.BRACKET,
+      mixed: PedalMarking.Styles.MIXED,
+      mixed_open_end: PedalMarking.Styles.MIXED_OPEN_END,
+      bracket_open_begin: PedalMarking.Styles.BRACKET_OPEN_BEGIN,
+      bracket_open_end: PedalMarking.Styles.BRACKET_OPEN_END,
+      bracket_open_both: PedalMarking.Styles.BRACKET_OPEN_BOTH
+    };
+  }
+
+  // Create a sustain pedal marking. Returns the defaults PedalMarking.
+  // Which uses the traditional "Ped" and "*"" markings.
+  static createSustain(notes) {
+    const pedal = new PedalMarking(notes);
+    return pedal;
+  }
+
+  // Create a sostenuto pedal marking
+  static createSostenuto(notes) {
+    const pedal = new PedalMarking(notes);
+    pedal.setStyle(PedalMarking.Styles.MIXED);
+    pedal.setCustomText('Sost. Ped.');
+    return pedal;
+  }
+
+  // Create an una corda pedal marking
+  static createUnaCorda(notes) {
+    const pedal = new PedalMarking(notes);
+    pedal.setStyle(PedalMarking.Styles.TEXT);
+    pedal.setCustomText('una corda', 'tre corda');
+    return pedal;
+  }
+
+  // ## Prototype Methods
+  constructor(notes) {
+    super();
+    this.setAttribute('type', 'PedalMarking');
+
+    this.notes = notes;
+    this.style = PedalMarking.TEXT;
+    this.line = 0;
+
+    // Custom text for the release/depress markings
+    this.custom_depress_text = '';
+    this.custom_release_text = '';
+
+    this.font = {
+      family: 'Times New Roman',
+      size: 12,
+      weight: 'italic bold',
+    };
+
+    this.render_options = {
+      bracket_height: 10,
+      text_margin_right: 6,
+      bracket_line_width: 1,
+      glyph_point_size: 40,
+      color: 'black',
+    };
+  }
+
+  // Set custom text for the `depress`/`release` pedal markings. No text is
+  // set if the parameter is falsy.
+  setCustomText(depress, release) {
+    this.custom_depress_text = depress || '';
+    this.custom_release_text = release || '';
+    return this;
+  }
+
+  // Set the pedal marking style
+  setStyle(style) {
+    if (style < 1 && style > 3)  {
+      throw new Vex.RERR('InvalidParameter', 'The style must be one found in PedalMarking.Styles');
+    }
+
+    this.style = style;
+    return this;
+  }
+
+  // Set the staff line to render the markings on
+  setLine(line) { this.line = line; return this; }
+
+  // Draw the bracket based pedal markings
+  drawBracketed() {
+    const ctx = this.context;
+    let is_pedal_depressed = false;
+    let prev_x;
+    let prev_y;
+    const pedal = this;
+
+    // Iterate through each note
+    this.notes.forEach((note, index, notes) => {
+      // Each note triggers the opposite pedal action
+      is_pedal_depressed = !is_pedal_depressed;
+      // Get the initial coordinates for the note
+      let x = note.getNoteHeadBeginX();
+      if (!is_pedal_depressed) {
+        switch(pedal.style) {
+          case PedalMarking.Styles.BRACKET_OPEN_END:
+          case PedalMarking.Styles.BRACKET_OPEN_BOTH:
+          case PedalMarking.Styles.MIXED_OPEN_END:
+            x = note.getNoteHeadEndX();
+          break;
+          default:
+            x -= pedal.render_options.text_margin_right;
+          break;
+        }
+      } else {
+        switch(pedal.style) {
+          case PedalMarking.Styles.BRACKET_OPEN_BEGIN:
+          case PedalMarking.Styles.BRACKET_OPEN_BOTH:
+            x -= pedal.render_options.text_margin_right;
+          break;
+        }
+      }
+
+      const y = note.getStave().getYForBottomText(pedal.line + 3);
+
+      // Throw if current note is positioned before the previous note
+      if (x < prev_x) {
+        throw new Vex.RERR(
+          'InvalidConfiguration', 'The notes provided must be in order of ascending x positions'
+        );
+      }
+
+      // Determine if the previous or next note are the same
+      // as the current note. We need to keep track of this for
+      // when adjustments are made for the release+depress action
+      const next_is_same = notes[index + 1] === note;
+      const prev_is_same = notes[index - 1] === note;
+
+      let x_shift = 0;
+      if (is_pedal_depressed) {
+        // Adjustment for release+depress
+        x_shift =  prev_is_same ? 5 : 0;
+
+        if ((pedal.style === PedalMarking.Styles.MIXED || pedal.style === PedalMarking.Styles.MIXED_OPEN_END) && !prev_is_same) {
+          // For MIXED style, start with text instead of bracket
+          if (pedal.custom_depress_text) {
+            // If we have custom text, use instead of the default "Ped" glyph
+            const text_width = ctx.measureText(pedal.custom_depress_text).width;
+            ctx.fillText(pedal.custom_depress_text, x - (text_width / 2), y);
+            x_shift = (text_width / 2) + pedal.render_options.text_margin_right;
+          } else {
+            // Render the Ped glyph in position
+            drawPedalGlyph('pedal_depress', ctx, x, y, pedal.render_options.glyph_point_size);
+            x_shift = 20 + pedal.render_options.text_margin_right;
+          }
+        } else {
+          // Draw start bracket
+          ctx.beginPath();
+          if (pedal.style === PedalMarking.Styles.BRACKET_OPEN_BEGIN || pedal.style === PedalMarking.Styles.BRACKET_OPEN_BOTH) {
+            ctx.moveTo(x + x_shift, y);
+          } else {
+            ctx.moveTo(x, y - pedal.render_options.bracket_height);
+            ctx.lineTo(x + x_shift, y);
+          }
+          ctx.stroke();
+          ctx.closePath();
+        }
+      } else {
+        // Adjustment for release+depress
+        x_shift = next_is_same ? -5 : 0;
+
+        // Draw end bracket
+        ctx.beginPath();
+        ctx.moveTo(prev_x, prev_y);
+        ctx.lineTo(x + x_shift, y);
+        if (pedal.style !== PedalMarking.Styles.BRACKET_OPEN_END && pedal.style !== PedalMarking.Styles.MIXED_OPEN_END &&
+            pedal.style !== PedalMarking.Styles.BRACKET_OPEN_BOTH) {
+            ctx.lineTo(x, y - pedal.render_options.bracket_height);
+        }
+        ctx.stroke();
+        ctx.closePath();
+      }
+
+      // Store previous coordinates
+      prev_x = x + x_shift;
+      prev_y = y;
+    });
+  }
+
+  // Draw the text based pedal markings. This defaults to the traditional
+  // "Ped" and "*"" symbols if no custom text has been provided.
+  drawText() {
+    const ctx = this.context;
+    let is_pedal_depressed = false;
+    const pedal = this;
+
+    // The glyph point size
+    const point = pedal.render_options.glyph_point_size;
+
+    // Iterate through each note, placing glyphs or custom text accordingly
+    this.notes.forEach(note => {
+      is_pedal_depressed = !is_pedal_depressed;
+      const stave = note.getStave();
+      const x = note.getAbsoluteX();
+      const y = stave.getYForBottomText(pedal.line + 3);
+
+      let text_width = 0;
+      if (is_pedal_depressed) {
+        if (pedal.custom_depress_text) {
+          text_width = ctx.measureText(pedal.custom_depress_text).width;
+          ctx.fillText(pedal.custom_depress_text, x - (text_width / 2), y);
+        } else {
+          drawPedalGlyph('pedal_depress', ctx, x, y, point);
+        }
+      } else {
+        if (pedal.custom_release_text) {
+          text_width = ctx.measureText(pedal.custom_release_text).width;
+          ctx.fillText(pedal.custom_release_text, x - (text_width / 2), y);
+        } else {
+          drawPedalGlyph('pedal_release', ctx, x, y, point);
+        }
+      }
+    });
+  }
+
+  // Render the pedal marking in position on the rendering context
+  draw() {
+    const ctx = this.checkContext();
+    this.setRendered();
+
+    ctx.save();
+    ctx.setStrokeStyle(this.render_options.color);
+    ctx.setFillStyle(this.render_options.color);
+    ctx.setFont(this.font.family, this.font.size, this.font.weight);
+
+    L('Rendering Pedal Marking');
+
+    if (this.style === PedalMarking.Styles.BRACKET || this.style === PedalMarking.Styles.MIXED || this.style === PedalMarking.Styles.MIXED_OPEN_END ||
+        this.style === PedalMarking.Styles.BRACKET_OPEN_BEGIN || this.style === PedalMarking.Styles.BRACKET_OPEN_END || this.style === PedalMarking.Styles.BRACKET_OPEN_BOTH) {
+      ctx.setLineWidth(this.render_options.bracket_line_width);
+      this.drawBracketed();
+    } else if (this.style === PedalMarking.Styles.TEXT) {
+      this.drawText();
+    }
+
+    ctx.restore();
+  }
+}

+ 1210 - 0
src/VexFlowPatch/src/stavenote.js

@@ -0,0 +1,1210 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+//
+// ## Description
+// This file implements notes for standard notation. This consists of one or
+// more `NoteHeads`, an optional stem, and an optional flag.
+//
+// *Throughout these comments, a "note" refers to the entire `StaveNote`,
+// and a "key" refers to a specific pitch/notehead within a note.*
+//
+// See `tests/stavenote_tests.js` for usage examples.
+
+import { Vex } from './vex';
+import { Flow } from './tables';
+import { BoundingBox } from './boundingbox';
+import { Stem } from './stem';
+import { NoteHead } from './notehead';
+import { StemmableNote } from './stemmablenote';
+import { Modifier } from './modifier';
+import { Dot } from './dot';
+
+// To enable logging for this class. Set `Vex.Flow.StaveNote.DEBUG` to `true`.
+function L(...args) { if (StaveNote.DEBUG) Vex.L('Vex.Flow.StaveNote', args); }
+
+const getStemAdjustment = (note) => Stem.WIDTH / (2 * -note.getStemDirection());
+
+const isInnerNoteIndex = (note, index) =>
+  index === (note.getStemDirection() === Stem.UP ? note.keyProps.length - 1 : 0);
+
+// Helper methods for rest positioning in ModifierContext.
+function shiftRestVertical(rest, note, dir) {
+  const delta = (note.isrest ? 0.0 : 1.0) * dir;
+
+  rest.line += delta;
+  rest.maxLine += delta;
+  rest.minLine += delta;
+  rest.note.setKeyLine(0, rest.note.getKeyLine(0) + (delta));
+}
+
+// Called from formatNotes :: center a rest between two notes
+function centerRest(rest, noteU, noteL) {
+  const delta = rest.line - Vex.MidLine(noteU.minLine, noteL.maxLine);
+  rest.note.setKeyLine(0, rest.note.getKeyLine(0) - delta);
+  rest.line -= delta;
+  rest.maxLine -= delta;
+  rest.minLine -= delta;
+}
+
+export class StaveNote extends StemmableNote {
+  static get CATEGORY() { return 'stavenotes'; }
+  static get STEM_UP() { return Stem.UP; }
+  static get STEM_DOWN() { return Stem.DOWN; }
+  static get DEFAULT_LEDGER_LINE_OFFSET() { return 3; }
+
+  // ## Static Methods
+  //
+  // Format notes inside a ModifierContext.
+  static format(notes, state) {
+    if (!notes || notes.length < 2) return false;
+
+    // FIXME: VexFlow will soon require that a stave be set before formatting.
+    // Which, according to the below condition, means that following branch will
+    // always be taken and the rest of this function is dead code.
+    //
+    // Problematically, `Formatter#formatByY` was not designed to work for more
+    // than 2 voices (although, doesn't throw on this condition, just tries
+    // to power through).
+    //
+    // Based on the above:
+    //   * 2 voices can be formatted *with or without* a stave being set but
+    //     the output will be different
+    //   * 3 voices can only be formatted *without* a stave
+    if (notes[0].getStave()) {
+      return StaveNote.formatByY(notes, state);
+    }
+
+    const notesList = [];
+
+    for (let i = 0; i < notes.length; i++) {
+      const props = notes[i].getKeyProps();
+      const line = props[0].line;
+      let minL = props[props.length - 1].line;
+      const stemDirection = notes[i].getStemDirection();
+      const stemMax = notes[i].getStemLength() / 10;
+      const stemMin = notes[i].getStemMinumumLength() / 10;
+
+      let maxL;
+      if (notes[i].isRest()) {
+        maxL = line + notes[i].glyph.line_above;
+        minL = line - notes[i].glyph.line_below;
+      } else {
+        maxL = stemDirection === 1
+          ? props[props.length - 1].line + stemMax
+          : props[props.length - 1].line;
+
+        minL = stemDirection === 1
+          ? props[0].line
+          : props[0].line - stemMax;
+      }
+
+      notesList.push({
+        line: props[0].line, // note/rest base line
+        maxLine: maxL, // note/rest upper bounds line
+        minLine: minL, // note/rest lower bounds line
+        isrest: notes[i].isRest(),
+        stemDirection,
+        stemMax, // Maximum (default) note stem length;
+        stemMin, // minimum note stem length
+        voice_shift: notes[i].getVoiceShiftWidth(),
+        is_displaced: notes[i].isDisplaced(), // note manually displaced
+        note: notes[i],
+      });
+    }
+
+    const voices = notesList.length;
+
+    let noteU = notesList[0];
+    const noteM = voices > 2 ? notesList[1] : null;
+    let noteL = voices > 2 ? notesList[2] : notesList[1];
+
+    // for two voice backward compatibility, ensure upper voice is stems up
+    // for three voices, the voices must be in order (upper, middle, lower)
+    if (voices === 2 && noteU.stemDirection === -1 && noteL.stemDirection === 1) {
+      noteU = notesList[1];
+      noteL = notesList[0];
+    }
+
+    const voiceXShift = Math.max(noteU.voice_shift, noteL.voice_shift);
+    let xShift = 0;
+    let stemDelta;
+    // Test for two voice note intersection
+    if (voices === 2) {
+      const lineSpacing = noteU.stemDirection === noteL.stemDirection ? 0.0 : 0.5;
+      // if top voice is a middle voice, check stem intersection with lower voice
+      if (noteU.stemDirection === noteL.stemDirection &&
+        noteU.minLine <= noteL.maxLine) {
+        if (!noteU.isrest) {
+          stemDelta = Math.abs(noteU.line - (noteL.maxLine + 0.5));
+          stemDelta = Math.max(stemDelta, noteU.stemMin);
+          noteU.minLine = noteU.line - stemDelta;
+          noteU.note.setStemLength(stemDelta * 10);
+        }
+      }
+      if (noteU.minLine <= noteL.maxLine + lineSpacing) {
+        if (noteU.isrest) {
+          // shift rest up
+          shiftRestVertical(noteU, noteL, 1);
+        } else if (noteL.isrest) {
+          // shift rest down
+          shiftRestVertical(noteL, noteU, -1);
+        } else {
+          xShift = voiceXShift;
+          //Vexflowpatch: Instead of shifting notes, remove the appropriate flag.
+          //If we are sharing a line, switch one notes stem direction.
+          //If we are sharing a line and in the same voice, only then offset one note
+          const lineDiff = Math.abs(noteU.line - noteL.line);
+          if (noteU.note.glyph.stem && noteL.note.glyph.stem) {
+            //If we have different dot values, must offset
+            //Or If we have a non-filled in mixed with a filled in notehead, must offset
+            if ((noteU.note.duration === "h" && noteL.note.duration !== "h") || 
+                (noteU.note.duration !== "h" && noteL.note.duration === "h") ||
+                 noteU.note.dots !== noteL.note.dots) {
+                noteL.note.setXShift(xShift);
+                if (noteU.note.dots > 0) {
+                  let foundDots = 0;
+                  for (const modifier of noteU.note.modifiers) {
+                    if (modifier instanceof Dot) {
+                      foundDots++;
+                      //offset dot(s) above the shifted note
+                      //lines + 1 to negative pixels
+                      modifier.setYShift(-10 * (noteL.maxLine - noteU.line + 1));
+                      if (foundDots === noteU.note.dots) {
+                        break;
+                      }
+                    }
+                  }
+                }
+            } else if (lineDiff < 1 && lineDiff > 0) {//if the notes are quite close but not on the same line, shift
+              noteL.note.setXShift(xShift);
+            } else if (noteU.note.voice !== noteL.note.voice) {//If we are not in the same voice
+              if (noteU.stemDirection === noteL.stemDirection) {
+                if (noteU.line > noteL.line) {
+                  //noteU is above noteL
+                  if (noteU.stemDirection === 1) {
+                    noteL.note.renderFlag = false;
+                  } else {
+                    noteU.note.renderFlag = false;
+                  }
+                } else if (noteL.line > noteU.line) {
+                  //note L is above noteU
+                  if (noteL.stemDirection === 1) {
+                    noteU.note.renderFlag = false;
+                  } else {
+                    noteL.note.renderFlag = false;
+                  }
+                } else {
+                  //same line, swap stem direction for one note
+                  if (noteL.stemDirection === 1) {
+                    noteL.stemDirection = -1;
+                    noteL.note.setStemDirection(-1);
+                  }
+                }
+              }
+            } //Very close whole notes
+          } else if ((!noteU.note.glyph.stem && !noteL.note.glyph.stem && lineDiff < 1.5)) {
+            noteL.note.setXShift(xShift);
+          }
+        }
+      }
+
+      // format complete
+      return true;
+    }
+
+    // Check middle voice stem intersection with lower voice
+    if (noteM !== null && noteM.minLine < noteL.maxLine + 0.5) {
+      if (!noteM.isrest) {
+        stemDelta = Math.abs(noteM.line - (noteL.maxLine + 0.5));
+        stemDelta = Math.max(stemDelta, noteM.stemMin);
+        noteM.minLine = noteM.line - stemDelta;
+        noteM.note.setStemLength(stemDelta * 10);
+      }
+    }
+
+    // For three voices, test if rests can be repositioned
+    //
+    // Special case 1 :: middle voice rest between two notes
+    //
+    if (noteM.isrest && !noteU.isrest && !noteL.isrest) {
+      if (noteU.minLine <= noteM.maxLine || noteM.minLine <= noteL.maxLine) {
+        const restHeight = noteM.maxLine - noteM.minLine;
+        const space = noteU.minLine - noteL.maxLine;
+        if (restHeight < space) {
+          // center middle voice rest between the upper and lower voices
+          centerRest(noteM, noteU, noteL);
+        } else {
+          xShift = voiceXShift + 3;    // shift middle rest right
+          noteM.note.setXShift(xShift);
+        }
+        // format complete
+        return true;
+      }
+    }
+
+    // Special case 2 :: all voices are rests
+    if (noteU.isrest && noteM.isrest && noteL.isrest) {
+      // Shift upper voice rest up
+      shiftRestVertical(noteU, noteM, 1);
+      // Shift lower voice rest down
+      shiftRestVertical(noteL, noteM, -1);
+      // format complete
+      return true;
+    }
+
+    // Test if any other rests can be repositioned
+    if (noteM.isrest && noteU.isrest && noteM.minLine <= noteL.maxLine) {
+      // Shift middle voice rest up
+      shiftRestVertical(noteM, noteL, 1);
+    }
+    if (noteM.isrest && noteL.isrest && noteU.minLine <= noteM.maxLine) {
+      // Shift middle voice rest down
+      shiftRestVertical(noteM, noteU, -1);
+    }
+    if (noteU.isrest && noteU.minLine <= noteM.maxLine) {
+      // shift upper voice rest up;
+      shiftRestVertical(noteU, noteM, 1);
+    }
+    if (noteL.isrest && noteM.minLine <= noteL.maxLine) {
+      // shift lower voice rest down
+      shiftRestVertical(noteL, noteM, -1);
+    }
+
+    // If middle voice intersects upper or lower voice
+    if ((!noteU.isrest && !noteM.isrest && noteU.minLine <= noteM.maxLine + 0.5) ||
+      (!noteM.isrest && !noteL.isrest && noteM.minLine <= noteL.maxLine)) {
+      xShift = voiceXShift + 3;      // shift middle note right
+      noteM.note.setXShift(xShift);
+    }
+
+    return true;
+  }
+
+  static formatByY(notes, state) {
+    // NOTE: this function does not support more than two voices per stave
+    // use with care.
+    let hasStave = true;
+
+    for (let i = 0; i < notes.length; i++) {
+      hasStave = hasStave && notes[i].getStave() != null;
+    }
+
+    if (!hasStave) {
+      throw new Vex.RERR(
+        'Stave Missing',
+        'All notes must have a stave - Vex.Flow.ModifierContext.formatMultiVoice!'
+      );
+    }
+
+    let xShift = 0;
+
+    for (let i = 0; i < notes.length - 1; i++) {
+      let topNote = notes[i];
+      let bottomNote = notes[i + 1];
+
+      //Vexflowpatch: The stem direction doesn't really determine which note is on top.
+      //Pick the actual note that is on top via the line number
+      if (topNote.maxLine < bottomNote.maxLine) {
+        topNote = notes[i + 1];
+        bottomNote = notes[i];
+      }
+
+      const topKeys = topNote.getKeyProps();
+      const bottomKeys = bottomNote.getKeyProps();
+
+      const HALF_NOTEHEAD_HEIGHT = 0.5;
+
+      // `keyProps` and `stave.getYForLine` have different notions of a `line`
+      // so we have to convert the keyProps value by subtracting 5.
+      // See https://github.com/0xfe/vexflow/wiki/Development-Gotchas
+      //
+      // We also extend the y for each note by a half notehead because the
+      // notehead's origin is centered
+      const topNoteBottomY = topNote
+        .getStave()
+        .getYForLine(5 - topKeys[0].line + HALF_NOTEHEAD_HEIGHT);
+
+      const bottomNoteTopY = bottomNote
+        .getStave()
+        .getYForLine(5 - bottomKeys[bottomKeys.length - 1].line - HALF_NOTEHEAD_HEIGHT);
+
+      const areNotesColliding = bottomNoteTopY - topNoteBottomY < 0;
+      if (areNotesColliding) {
+        //Vexflowpatch: Only shift if we are in the same voice. This is mostly taken care of format() above.
+        if (topNote.voice === bottomNote.voice) {
+          xShift = topNote.getVoiceShiftWidth() + 2;
+          bottomNote.setXShift(xShift);
+        }
+      }
+    }
+
+    state.right_shift += xShift;
+  }
+
+  static postFormat(notes) {
+    if (!notes) return false;
+
+    notes.forEach(note => note.postFormat());
+
+    return true;
+  }
+
+  constructor(noteStruct) {
+    super(noteStruct);
+    this.setAttribute('type', 'StaveNote');
+
+    this.keys = noteStruct.keys;
+    this.clef = noteStruct.clef;
+    this.octave_shift = noteStruct.octave_shift;
+    this.beam = null;
+
+    // Pull note rendering properties
+    this.glyph = Flow.getGlyphProps(this.duration, this.noteType);
+
+    if (!this.glyph) {
+      throw new Vex.RuntimeError(
+        'BadArguments',
+        `Invalid note initialization data (No glyph found): ${JSON.stringify(noteStruct)}`
+      );
+    }
+
+    // if true, displace note to right
+    this.displaced = false;
+    this.dot_shiftY = 0;
+    //VexflowPatch: We seem to init with a dot count, and also call addDot, so this is to count the added dots vs. the inited dots.
+    this.addDotsCount = 0;
+    // per-pitch properties
+    this.keyProps = [];
+    // for displaced ledger lines
+    this.use_default_head_x = false;
+
+    // Drawing
+    this.note_heads = [];
+    this.modifiers = [];
+
+    Vex.Merge(this.render_options, {
+      // font size for note heads and rests
+      glyph_font_scale: noteStruct.glyph_font_scale || Flow.DEFAULT_NOTATION_FONT_SCALE,
+      // number of stroke px to the left and right of head
+      stroke_px: noteStruct.stroke_px || StaveNote.DEFAULT_LEDGER_LINE_OFFSET,
+    });
+
+    this.calculateKeyProps();
+    this.buildStem();
+
+    // Set the stem direction
+    if (noteStruct.auto_stem) {
+      this.autoStem();
+    } else {
+      this.setStemDirection(noteStruct.stem_direction);
+    }
+    this.reset();
+    this.buildFlag();
+  }
+
+  reset() {
+    super.reset();
+
+    // Save prior noteHead styles & reapply them after making new noteheads.
+    const noteHeadStyles = this.note_heads.map(noteHead => noteHead.getStyle());
+    this.buildNoteHeads();
+    this.note_heads.forEach((noteHead, index) => noteHead.setStyle(noteHeadStyles[index]));
+
+    if (this.stave) {
+      this.note_heads.forEach(head => head.setStave(this.stave));
+    }
+    this.calcExtraPx();
+  }
+
+  setBeam(beam) {
+    this.beam = beam;
+    this.calcExtraPx();
+    return this;
+  }
+
+  getCategory() { return StaveNote.CATEGORY; }
+
+  // Builds a `Stem` for the note
+  buildStem() {
+    this.setStem(new Stem({ hide: !!this.isRest(), }));
+  }
+
+  // Builds a `NoteHead` for each key in the note
+  buildNoteHeads() {
+    this.note_heads = [];
+    const stemDirection = this.getStemDirection();
+    const keys = this.getKeys();
+
+    let lastLine = null;
+    let lineDiff = null;
+    let displaced = false;
+
+    // Draw notes from bottom to top.
+
+    // For down-stem notes, we draw from top to bottom.
+    let start;
+    let end;
+    let step;
+    if (stemDirection === Stem.UP) {
+      start = 0;
+      end = keys.length;
+      step = 1;
+    } else if (stemDirection === Stem.DOWN) {
+      start = keys.length - 1;
+      end = -1;
+      step = -1;
+    }
+
+    for (let i = start; i !== end; i += step) {
+      const noteProps = this.keyProps[i];
+      const line = noteProps.line;
+
+      // Keep track of last line with a note head, so that consecutive heads
+      // are correctly displaced.
+      if (lastLine === null) {
+        lastLine = line;
+      } else {
+        lineDiff = Math.abs(lastLine - line);
+        if (lineDiff === 0 || lineDiff === 0.5) {
+          displaced = !displaced;
+        } else {
+          displaced = false;
+          this.use_default_head_x = true;
+        }
+      }
+      lastLine = line;
+
+      const notehead = new NoteHead({
+        duration: this.duration,
+        note_type: this.noteType,
+        displaced,
+        stem_direction: stemDirection,
+        custom_glyph_code: noteProps.code,
+        glyph_font_scale: this.render_options.glyph_font_scale,
+        x_shift: noteProps.shift_right,
+        stem_up_x_offset: noteProps.stem_up_x_offset,
+        stem_down_x_offset: noteProps.stem_down_x_offset,
+        line: noteProps.line,
+      });
+
+      this.note_heads[i] = notehead;
+    }
+  }
+
+  // Automatically sets the stem direction based on the keys in the note
+  autoStem() {
+    // Figure out optimal stem direction based on given notes
+    this.minLine = this.keyProps[0].line;
+    this.maxLine = this.keyProps[this.keyProps.length - 1].line;
+
+    const MIDDLE_LINE = 3;
+    const decider = (this.minLine + this.maxLine) / 2;
+    const stemDirection = decider < MIDDLE_LINE ? Stem.UP : Stem.DOWN;
+
+    this.setStemDirection(stemDirection);
+  }
+
+  // Calculates and stores the properties for each key in the note
+  calculateKeyProps() {
+    let lastLine = null;
+    for (let i = 0; i < this.keys.length; ++i) {
+      const key = this.keys[i];
+
+      // All rests use the same position on the line.
+      // if (this.glyph.rest) key = this.glyph.position;
+      if (this.glyph.rest) this.glyph.position = key;
+
+      const options = { octave_shift: this.octave_shift || 0 };
+      const props = Flow.keyProperties(key, this.clef, options);
+
+      if (!props) {
+        throw new Vex.RuntimeError('BadArguments', `Invalid key for note properties: ${key}`);
+      }
+
+      // Override line placement for default rests
+      if (props.key === 'R') {
+        if (this.duration === '1' || this.duration === 'w') {
+          props.line = 4;
+        } else {
+          props.line = 3;
+        }
+      }
+
+      // Calculate displacement of this note
+      const line = props.line;
+      if (lastLine === null) {
+        lastLine = line;
+      } else {
+        if (Math.abs(lastLine - line) === 0.5) {
+          this.displaced = true;
+          props.displaced = true;
+
+          // Have to mark the previous note as
+          // displaced as well, for modifier placement
+          if (this.keyProps.length > 0) {
+            this.keyProps[i - 1].displaced = true;
+          }
+        }
+      }
+
+      lastLine = line;
+      this.keyProps.push(props);
+    }
+
+    // Sort the notes from lowest line to highest line
+    lastLine = -Infinity;
+    this.keyProps.forEach(key => {
+      if (key.line < lastLine) {
+        Vex.W(
+          'Unsorted keys in note will be sorted. ' +
+          'See https://github.com/0xfe/vexflow/issues/104 for details.'
+        );
+      }
+      lastLine = key.line;
+    });
+    this.keyProps.sort((a, b) => a.line - b.line);
+  }
+
+  // Get the `BoundingBox` for the entire note
+  getBoundingBox() {
+    if (!this.preFormatted) {
+      throw new Vex.RERR('UnformattedNote', "Can't call getBoundingBox on an unformatted note.");
+    }
+
+    const { width: w, modLeftPx, extraLeftPx } = this.getMetrics();
+    const x = this.getAbsoluteX() - modLeftPx - extraLeftPx;
+
+    let minY = 0;
+    let maxY = 0;
+    const halfLineSpacing = this.getStave().getSpacingBetweenLines() / 2;
+    const lineSpacing = halfLineSpacing * 2;
+    if (this.isRest()) {
+      const y = this.ys[0];
+      const frac = Flow.durationToFraction(this.duration);
+      if (frac.equals(1) || frac.equals(2)) {
+        minY = y - halfLineSpacing;
+        maxY = y + halfLineSpacing;
+      } else {
+        minY = y - (this.glyph.line_above * lineSpacing);
+        maxY = y + (this.glyph.line_below * lineSpacing);
+      }
+    } else if (this.glyph.stem) {
+      const ys = this.getStemExtents();
+      ys.baseY += halfLineSpacing * this.stem_direction;
+      minY = Math.min(ys.topY, ys.baseY);
+      maxY = Math.max(ys.topY, ys.baseY);
+    } else {
+      minY = null;
+      maxY = null;
+
+      for (let i = 0; i < this.ys.length; ++i) {
+        const yy = this.ys[i];
+        if (i === 0) {
+          minY = yy;
+          maxY = yy;
+        } else {
+          minY = Math.min(yy, minY);
+          maxY = Math.max(yy, maxY);
+        }
+      }
+      minY -= halfLineSpacing;
+      maxY += halfLineSpacing;
+    }
+
+    return new BoundingBox(x, minY, w, maxY - minY);
+  }
+
+  // Gets the line number of the top or bottom note in the chord.
+  // If `isTopNote` is `true` then get the top note
+  getLineNumber(isTopNote) {
+    if (!this.keyProps.length) {
+      throw new Vex.RERR(
+        'NoKeyProps', "Can't get bottom note line, because note is not initialized properly."
+      );
+    }
+
+    let resultLine = this.keyProps[0].line;
+
+    // No precondition assumed for sortedness of keyProps array
+    for (let i = 0; i < this.keyProps.length; i++) {
+      const thisLine = this.keyProps[i].line;
+      if (isTopNote) {
+        if (thisLine > resultLine) resultLine = thisLine;
+      } else {
+        if (thisLine < resultLine) resultLine = thisLine;
+      }
+    }
+
+    return resultLine;
+  }
+
+  // Determine if current note is a rest
+  isRest() { return this.glyph.rest; }
+
+  // Determine if the current note is a chord
+  isChord() { return !this.isRest() && this.keys.length > 1; }
+
+  // Determine if the `StaveNote` has a stem
+  hasStem() { return this.glyph.stem; }
+
+  hasFlag() {
+    return super.hasFlag() && !this.isRest() && this.renderFlag;
+  }
+
+  getStemX() {
+    if (this.noteType === 'r') {
+      return this.getCenterGlyphX();
+    } else {
+      // We adjust the origin of the stem because we want the stem left-aligned
+      // with the notehead if stemmed-down, and right-aligned if stemmed-up
+      return super.getStemX() + getStemAdjustment(this);
+    }
+  }
+
+  // Get the `y` coordinate for text placed on the top/bottom of a
+  // note at a desired `text_line`
+  getYForTopText(textLine) {
+    const extents = this.getStemExtents();
+    return Math.min(
+      this.stave.getYForTopText(textLine),
+      extents.topY - (this.render_options.annotation_spacing * (textLine + 1))
+    );
+  }
+  getYForBottomText(textLine) {
+    const extents = this.getStemExtents();
+    return Math.max(
+      this.stave.getYForTopText(textLine),
+      extents.baseY + (this.render_options.annotation_spacing * (textLine))
+    );
+  }
+
+  // Sets the current note to the provided `stave`. This applies
+  // `y` values to the `NoteHeads`.
+  setStave(stave) {
+    super.setStave(stave);
+
+    const ys = this.note_heads.map(notehead => {
+      notehead.setStave(stave);
+      return notehead.getY();
+    });
+
+    this.setYs(ys);
+
+    if (this.stem) {
+      const { y_top, y_bottom } = this.getNoteHeadBounds();
+      this.stem.setYBounds(y_top, y_bottom);
+    }
+
+    return this;
+  }
+
+  // Get the pitches in the note
+  getKeys() { return this.keys; }
+
+  // Get the properties for all the keys in the note
+  getKeyProps() {
+    return this.keyProps;
+  }
+
+  // Check if note is shifted to the right
+  isDisplaced() {
+    return this.displaced;
+  }
+
+  // Sets whether shift note to the right. `displaced` is a `boolean`
+  setNoteDisplaced(displaced) {
+    this.displaced = displaced;
+    return this;
+  }
+
+  // Get the starting `x` coordinate for a `StaveTie`
+  getTieRightX() {
+    let tieStartX = this.getAbsoluteX();
+    tieStartX += this.getGlyphWidth() + this.x_shift + this.extraRightPx;
+    if (this.modifierContext) tieStartX += this.modifierContext.getExtraRightPx();
+    return tieStartX;
+  }
+
+  // Get the ending `x` coordinate for a `StaveTie`
+  getTieLeftX() {
+    let tieEndX = this.getAbsoluteX();
+    tieEndX += this.x_shift - this.extraLeftPx;
+    return tieEndX;
+  }
+
+  // Get the stave line on which to place a rest
+  getLineForRest() {
+    let restLine = this.keyProps[0].line;
+    if (this.keyProps.length > 1) {
+      const lastLine = this.keyProps[this.keyProps.length - 1].line;
+      const top = Math.max(restLine, lastLine);
+      const bot = Math.min(restLine, lastLine);
+      restLine = Vex.MidLine(top, bot);
+    }
+
+    return restLine;
+  }
+
+  // Get the default `x` and `y` coordinates for the provided `position`
+  // and key `index`
+  getModifierStartXY(position, index, options) {
+    options = options || {};
+    if (!this.preFormatted) {
+      throw new Vex.RERR('UnformattedNote', "Can't call GetModifierStartXY on an unformatted note");
+    }
+
+    if (this.ys.length === 0) {
+      throw new Vex.RERR('NoYValues', 'No Y-Values calculated for this note.');
+    }
+
+    const { ABOVE, BELOW, LEFT, RIGHT } = Modifier.Position;
+    let x = 0;
+    if (position === LEFT) {
+      // extra_left_px
+      // FIXME: What are these magic numbers?
+      x = -1 * 2;
+    } else if (position === RIGHT) {
+      // extra_right_px
+      // FIXME: What is this magical +2?
+      x = this.getGlyphWidth() + this.x_shift + 2;
+
+      if (this.stem_direction === Stem.UP && this.hasFlag() &&
+        (options.forceFlagRight || isInnerNoteIndex(this, index))) {
+        x += this.flag.getMetrics().width;
+      }
+    } else if (position === BELOW || position === ABOVE) {
+      x = this.getGlyphWidth() / 2;
+    }
+
+    return {
+      x: this.getAbsoluteX() + x,
+      y: this.ys[index],
+    };
+  }
+
+  // Sets the style of the complete StaveNote, including all keys
+  // and the stem.
+  setStyle(style) {
+    super.setStyle(style);
+    this.note_heads.forEach(notehead => notehead.setStyle(style));
+    if (this.stem){
+      this.stem.setStyle(style);
+    } 
+  }
+
+  setStemStyle(style) {
+    if (this.stem){
+      const stem = this.getStem();
+      stem.setStyle(style);
+    }
+  }
+  getStemStyle() { return this.stem.getStyle(); }
+
+  setLedgerLineStyle(style) { this.ledgerLineStyle = style; }
+  getLedgerLineStyle() { return this.ledgerLineStyle; }
+
+  setFlagStyle(style) { this.flagStyle = style; }
+  getFlagStyle() { return this.flagStyle; }
+
+  // Sets the notehead at `index` to the provided coloring `style`.
+  //
+  // `style` is an `object` with the following properties: `shadowColor`,
+  // `shadowBlur`, `fillStyle`, `strokeStyle`
+  setKeyStyle(index, style) {
+    this.note_heads[index].setStyle(style);
+    return this;
+  }
+
+  setKeyLine(index, line) {
+    this.keyProps[index].line = line;
+    this.reset();
+    return this;
+  }
+
+  getKeyLine(index) {
+    return this.keyProps[index].line;
+  }
+
+  // Add self to modifier context. `mContext` is the `ModifierContext`
+  // to be added to.
+  addToModifierContext(mContext) {
+    this.setModifierContext(mContext);
+    for (let i = 0; i < this.modifiers.length; ++i) {
+      this.modifierContext.addModifier(this.modifiers[i]);
+    }
+    this.modifierContext.addModifier(this);
+    this.setPreFormatted(false);
+    return this;
+  }
+
+  // Generic function to add modifiers to a note
+  //
+  // Parameters:
+  // * `index`: The index of the key that we're modifying
+  // * `modifier`: The modifier to add
+  addModifier(index, modifier) {
+    modifier.setNote(this);
+    modifier.setIndex(index);
+    this.modifiers.push(modifier);
+    this.setPreFormatted(false);
+    return this;
+  }
+
+  // Helper function to add an accidental to a key
+  addAccidental(index, accidental) {
+    return this.addModifier(index, accidental);
+  }
+
+  // Helper function to add an articulation to a key
+  addArticulation(index, articulation) {
+    return this.addModifier(index, articulation);
+  }
+
+  // Helper function to add an annotation to a key
+  addAnnotation(index, annotation) {
+    return this.addModifier(index, annotation);
+  }
+
+  // Helper function to add a dot on a specific key
+  addDot(index) {
+    const dot = new Dot();
+    dot.setDotShiftY(this.glyph.dot_shiftY);
+    this.addDotsCount++;
+    return this.addModifier(index, dot);
+  }
+
+  // Convenience method to add dot to all keys in note
+  addDotToAll() {
+    for (let i = 0; i < this.keys.length; ++i) {
+      this.addDot(i);
+    }
+    return this;
+  }
+
+  // Get all accidentals in the `ModifierContext`
+  getAccidentals() {
+    return this.modifierContext.getModifiers('accidentals');
+  }
+
+  // Get all dots in the `ModifierContext`
+  getDots() {
+    return this.modifierContext.getModifiers('dots');
+  }
+
+  // Get the width of the note if it is displaced. Used for `Voice`
+  // formatting
+  getVoiceShiftWidth() {
+    // TODO: may need to accomodate for dot here.
+    return this.getGlyphWidth() * (this.displaced ? 2 : 1);
+  }
+
+  // Calculates and sets the extra pixels to the left or right
+  // if the note is displaced.
+  calcExtraPx() {
+    this.setExtraLeftPx(
+      this.displaced && this.stem_direction === Stem.DOWN
+        ? this.getGlyphWidth()
+        : 0
+    );
+
+    // For upstems with flags, the extra space is unnecessary, since it's taken
+    // up by the flag.
+    this.setExtraRightPx(
+      !this.hasFlag() && this.displaced && this.stem_direction === Stem.UP
+        ? this.getGlyphWidth()
+        : 0
+    );
+  }
+
+  // Pre-render formatting
+  preFormat() {
+    if (this.preFormatted) return;
+    if (this.modifierContext) this.modifierContext.preFormat();
+
+    let width = this.getGlyphWidth() + this.extraLeftPx + this.extraRightPx;
+
+    // For upward flagged notes, the width of the flag needs to be added
+    if (this.renderFlag && this.glyph.flag && this.beam === null && this.stem_direction === Stem.UP) {
+      width += this.getGlyphWidth();
+    }
+
+    this.setWidth(width);
+    this.setPreFormatted(true);
+  }
+
+  /**
+   * @typedef {Object} noteHeadBounds
+   * @property {number} y_top the highest notehead bound
+   * @property {number} y_bottom the lowest notehead bound
+   * @property {number|Null} displaced_x the starting x for displaced noteheads
+   * @property {number|Null} non_displaced_x the starting x for non-displaced noteheads
+   * @property {number} highest_line the highest notehead line in traditional music line
+   *  numbering (bottom line = 1, top line = 5)
+   * @property {number} lowest_line the lowest notehead line
+   * @property {number|false} highest_displaced_line the highest staff line number
+   *   for a displaced notehead
+   * @property {number|false} lowest_displaced_line
+   * @property {number} highest_non_displaced_line
+   * @property {number} lowest_non_displaced_line
+   */
+
+  /**
+   * Get the staff line and y value for the highest & lowest noteheads
+   * @returns {noteHeadBounds}
+   */
+  getNoteHeadBounds() {
+    // Top and bottom Y values for stem.
+    let yTop = null;
+    let yBottom = null;
+    let nonDisplacedX = null;
+    let displacedX = null;
+
+    let highestLine = this.stave.getNumLines();
+    let lowestLine = 1;
+    let highestDisplacedLine = false;
+    let lowestDisplacedLine = false;
+    let highestNonDisplacedLine = highestLine;
+    let lowestNonDisplacedLine = lowestLine;
+
+    this.note_heads.forEach(notehead => {
+      const line = notehead.getLine();
+      const y = notehead.getY();
+
+      if (yTop === null || y < yTop) {
+        yTop = y;
+      }
+
+      if (yBottom === null || y > yBottom) {
+        yBottom = y;
+      }
+
+      if (displacedX === null && notehead.isDisplaced()) {
+        displacedX = notehead.getAbsoluteX();
+      }
+
+      if (nonDisplacedX === null && !notehead.isDisplaced()) {
+        nonDisplacedX = notehead.getAbsoluteX();
+      }
+
+      highestLine = line > highestLine ? line : highestLine;
+      lowestLine = line < lowestLine ? line : lowestLine;
+
+      if (notehead.isDisplaced()) {
+        highestDisplacedLine = (highestDisplacedLine === false) ?
+          line : Math.max(line, highestDisplacedLine);
+        lowestDisplacedLine = (lowestDisplacedLine === false) ?
+          line : Math.min(line, lowestDisplacedLine);
+      } else {
+        highestNonDisplacedLine = Math.max(line, highestNonDisplacedLine);
+        lowestNonDisplacedLine = Math.min(line, lowestNonDisplacedLine);
+      }
+    }, this);
+
+    return {
+      y_top: yTop,
+      y_bottom: yBottom,
+      displaced_x: displacedX,
+      non_displaced_x: nonDisplacedX,
+      highest_line: highestLine,
+      lowest_line: lowestLine,
+      highest_displaced_line: highestDisplacedLine,
+      lowest_displaced_line: lowestDisplacedLine,
+      highest_non_displaced_line: highestNonDisplacedLine,
+      lowest_non_displaced_line: lowestNonDisplacedLine,
+    };
+  }
+
+  // Get the starting `x` coordinate for the noteheads
+  getNoteHeadBeginX() {
+    return this.getAbsoluteX() + this.x_shift;
+  }
+
+  // Get the ending `x` coordinate for the noteheads
+  getNoteHeadEndX() {
+    const xBegin = this.getNoteHeadBeginX();
+    return xBegin + this.getGlyphWidth();
+  }
+
+  // Draw the ledger lines between the stave and the highest/lowest keys
+  drawLedgerLines() {
+    const {
+      stave, glyph,
+      render_options: { stroke_px },
+      context: ctx,
+    } = this;
+
+    const width = glyph.getWidth() + (stroke_px * 2);
+    const doubleWidth = 2 * (glyph.getWidth() + stroke_px) - (Stem.WIDTH / 2);
+
+    if (this.isRest()) return;
+    if (!ctx) {
+      throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
+    }
+
+    const {
+      highest_line,
+      lowest_line,
+      highest_displaced_line,
+      highest_non_displaced_line,
+      lowest_displaced_line,
+      lowest_non_displaced_line,
+      displaced_x,
+      non_displaced_x,
+    } = this.getNoteHeadBounds();
+
+    const min_x = Math.min(displaced_x, non_displaced_x);
+
+    const drawLedgerLine = (y, normal, displaced) => {
+      let x;
+      if (displaced && normal) x = min_x - stroke_px;
+      else if (normal) x = non_displaced_x - stroke_px;
+      else x = displaced_x - stroke_px;
+      const ledgerWidth = (normal && displaced) ? doubleWidth : width;
+
+      ctx.beginPath();
+      ctx.moveTo(x, y);
+      ctx.lineTo(x + ledgerWidth, y);
+      ctx.stroke();
+    };
+
+    const style = { ...stave.getStyle() || {}, ...this.getLedgerLineStyle() || {} };
+    this.applyStyle(ctx, style);
+
+    // Draw ledger lines below the staff:
+    for (let line = 6; line <= highest_line; ++line) {
+      const normal = (non_displaced_x !== null) && (line <= highest_non_displaced_line);
+      const displaced = (displaced_x !== null) && (line <= highest_displaced_line);
+      drawLedgerLine(stave.getYForNote(line), normal, displaced);
+    }
+
+    // Draw ledger lines above the staff:
+    for (let line = 0; line >= lowest_line; --line) {
+      const normal = (non_displaced_x !== null) && (line >= lowest_non_displaced_line);
+      const displaced = (displaced_x !== null) && (line >= lowest_displaced_line);
+      drawLedgerLine(stave.getYForNote(line), normal, displaced);
+    }
+
+    this.restoreStyle(ctx, style);
+  }
+
+  // Draw all key modifiers
+  drawModifiers() {
+    if (!this.context) {
+      throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
+    }
+
+    const ctx = this.context;
+    ctx.openGroup('modifiers');
+    for (let i = 0; i < this.modifiers.length; i++) {
+      const modifier = this.modifiers[i];
+      const notehead = this.note_heads[modifier.getIndex()];
+      const noteheadStyle = notehead.getStyle();
+      notehead.applyStyle(ctx, noteheadStyle);
+      modifier.setContext(ctx);
+      modifier.drawWithStyle();
+      notehead.restoreStyle(ctx, noteheadStyle);
+    }
+    ctx.closeGroup();
+  }
+
+  // Draw the flag for the note
+  drawFlag() {
+    const { stem, beam, context: ctx } = this;
+
+    if (!ctx) {
+      throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
+    }
+
+    const shouldRenderFlag = beam === null && this.renderFlag;
+    const glyph = this.getGlyph();
+
+    if (glyph.flag && shouldRenderFlag) {
+      const { y_top, y_bottom } = this.getNoteHeadBounds();
+      const noteStemHeight = stem.getHeight();
+      const flagX = this.getStemX();
+      // FIXME: What's with the magic +/- 2
+      const flagY = this.getStemDirection() === Stem.DOWN
+        // Down stems have flags on the left
+        ? y_top - noteStemHeight + 2
+        // Up stems have flags on the eft.
+        : y_bottom - noteStemHeight - 2;
+
+      // Draw the Flag
+      ctx.openGroup('flag', null, { pointerBBox: true });
+      this.applyStyle(ctx, this.getFlagStyle() || false);
+      this.flag.render(ctx, flagX, flagY);
+      this.restoreStyle(ctx, this.getFlagStyle() || false);
+      ctx.closeGroup();
+    }
+  }
+
+  // Draw the NoteHeads
+  drawNoteHeads() {
+    this.note_heads.forEach(notehead => {
+      this.context.openGroup('notehead', null, { pointerBBox: true });
+      notehead.setContext(this.context).draw();
+      this.context.closeGroup();
+    });
+  }
+
+  drawStem(stemStruct) {
+    // GCR TODO: I can't find any context in which this is called with the stemStruct
+    // argument in the codebase or tests. Nor can I find a case where super.drawStem
+    // is called at all. Perhaps these should be removed?
+    if (!this.context) {
+      throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
+    }
+
+    if (stemStruct) {
+      this.setStem(new Stem(stemStruct));
+    }
+
+    if (this.stem) {
+      this.context.openGroup('stem', null, { pointerBBox: true });
+      this.stem.setContext(this.context).draw();
+      this.context.closeGroup();
+    }
+  }
+
+  // Draws all the `StaveNote` parts. This is the main drawing method.
+  draw() {
+    if (!this.context) {
+      throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
+    }
+    if (!this.stave) {
+      throw new Vex.RERR('NoStave', "Can't draw without a stave.");
+    }
+    if (this.ys.length === 0) {
+      throw new Vex.RERR('NoYValues', "Can't draw note without Y values.");
+    }
+
+    const xBegin = this.getNoteHeadBeginX();
+    const shouldRenderStem = this.hasStem() && !this.beam;
+
+    // Format note head x positions
+    this.note_heads.forEach(notehead => notehead.setX(xBegin));
+
+    if(this.stem) {
+      // Format stem x positions
+      const stemX = this.getStemX();
+      this.stem.setNoteHeadXBounds(stemX, stemX);
+    }
+
+    L('Rendering ', this.isChord() ? 'chord :' : 'note :', this.keys);
+
+    // Draw each part of the note
+    this.drawLedgerLines();
+
+    // Apply the overall style -- may be contradicted by local settings:
+    this.applyStyle();
+    this.setAttribute('el', this.context.openGroup('stavenote', this.getAttribute('id')));
+    this.context.openGroup('note', null, { pointerBBox: true });
+    if (shouldRenderStem) this.drawStem();
+    this.drawNoteHeads();
+    this.drawFlag();
+    this.context.closeGroup();
+    this.drawModifiers();
+    this.context.closeGroup();
+    this.restoreStyle();
+    this.setRendered();
+  }
+}

+ 222 - 0
src/VexFlowPatch/src/stemmablenote.js

@@ -0,0 +1,222 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+//
+// ## Description
+// `StemmableNote` is an abstract interface for notes with optional stems.
+// Examples of stemmable notes are `StaveNote` and `TabNote`
+
+import { Vex } from './vex';
+import { Flow } from './tables';
+import { Stem } from './stem';
+import { Glyph } from './glyph';
+import { Note } from './note';
+
+export class StemmableNote extends Note {
+  constructor(note_struct) {
+    super(note_struct);
+    this.setAttribute('type', 'StemmableNote');
+    this.stem = null;
+    this.stemExtensionOverride = null;
+    this.beam = null;
+    this.renderFlag = true;
+  }
+
+  // Get and set the note's `Stem`
+  getStem() { return this.stem; }
+  setStem(stem) { this.stem = stem; return this; }
+
+  // Builds and sets a new stem
+  buildStem() {
+    const stem = new Stem();
+    this.setStem(stem);
+    return this;
+  }
+
+  buildFlag() {
+    const { glyph, beam } = this;
+    const shouldRenderFlag = beam === null && this.renderFlag;
+
+    if (glyph && glyph.flag && shouldRenderFlag) {
+      const flagCode = this.getStemDirection() === Stem.DOWN
+        ? glyph.code_flag_downstem
+        : glyph.code_flag_upstem;
+
+      this.flag = new Glyph(flagCode, this.render_options.glyph_font_scale);
+    }
+  }
+
+  // Get the glyph associated with the top key of this note
+  getTopGlyph() {
+    if (this.getStemDirection() === Stem.DOWN) {
+      return this.customGlyphs[this.customGlyphs.length - 1];
+    } else {
+      return this.customGlyphs[0];
+    }
+  }
+
+  // Get the full length of stem
+  getStemLength() {
+    return Stem.HEIGHT + this.getStemExtension();
+  }
+
+  // Get the number of beams for this duration
+  getBeamCount() {
+    const glyph = this.getGlyph();
+
+    if (glyph) {
+      return glyph.beam_count;
+    } else {
+      return 0;
+    }
+  }
+
+  // Get the minimum length of stem
+  getStemMinumumLength() {
+    const frac = Flow.durationToFraction(this.duration);
+    let length = frac.value() <= 1 ? 0 : 20;
+    // if note is flagged, cannot shorten beam
+    switch (this.duration) {
+      case '8':
+        if (this.beam == null) length = 35;
+        break;
+      case '16':
+        length = this.beam == null ? 35 : 25;
+        break;
+      case '32':
+        length = this.beam == null ? 45 : 35;
+        break;
+      case '64':
+        length = this.beam == null ? 50 : 40;
+        break;
+      case '128':
+        length = this.beam == null ? 55 : 45;
+        break;
+      default:
+        break;
+    }
+    return length;
+  }
+
+  // Get/set the direction of the stem
+  getStemDirection() { return this.stem_direction; }
+  setStemDirection(direction) {
+    if (!direction) direction = Stem.UP;
+    if (direction !== Stem.UP && direction !== Stem.DOWN) {
+      throw new Vex.RERR('BadArgument', `Invalid stem direction: ${direction}`);
+    }
+
+    this.stem_direction = direction;
+    if (this.stem) {
+      this.stem.setDirection(direction);
+      this.stem.setExtension(this.getStemExtension());
+      const glyph = this.getTopGlyph() || this.getGlyph();
+      this.stem.setOptions({
+        stem_up_y_offset: glyph.stem_up_y_offset,
+        stem_down_y_offset: glyph.stem_down_y_offset
+      });
+    }
+
+    this.reset();
+    if (this.flag && this.renderFlag) {
+      this.buildFlag();
+    }
+
+    this.beam = null;
+    if (this.preFormatted) {
+      this.preFormat();
+    }
+
+    return this;
+  }
+
+  // Get the `x` coordinate of the stem
+  getStemX() {
+    const x_begin = this.getAbsoluteX() + this.x_shift;
+    const x_end = this.getAbsoluteX() + this.x_shift + this.getGlyphWidth();
+    const stem_x = this.stem_direction === Stem.DOWN ? x_begin : x_end;
+    return stem_x;
+  }
+
+  // Get the `x` coordinate for the center of the glyph.
+  // Used for `TabNote` stems and stemlets over rests
+  getCenterGlyphX() {
+    return this.getAbsoluteX() + this.x_shift + (this.getGlyphWidth() / 2);
+  }
+
+  // Get the stem extension for the current duration
+  getStemExtension() {
+    const glyph = this.getGlyph();
+
+    if (this.stemExtensionOverride != null) {
+      return this.stemExtensionOverride;
+    }
+
+    if (glyph) {
+      return this.getStemDirection() === 1
+        ? glyph.stem_up_extension
+        : glyph.stem_down_extension;
+    }
+
+    return 0;
+  }
+
+  // Set the stem length to a specific. Will override the default length.
+  setStemLength(height) {
+    this.stemExtensionOverride = (height - Stem.HEIGHT);
+    return this;
+  }
+
+  // Get the top and bottom `y` values of the stem.
+  getStemExtents() {
+    return this.stem.getExtents();
+  }
+
+  // Sets the current note's beam
+  setBeam(beam) { this.beam = beam; return this; }
+
+  // Get the `y` value for the top/bottom modifiers at a specific `textLine`
+  getYForTopText(textLine) {
+    const extents = this.getStemExtents();
+    if (this.hasStem()) {
+      return Math.min(
+        this.stave.getYForTopText(textLine),
+        extents.topY - (this.render_options.annotation_spacing * (textLine + 1))
+      );
+    } else {
+      return this.stave.getYForTopText(textLine);
+    }
+  }
+
+  getYForBottomText(textLine) {
+    const extents = this.getStemExtents();
+    if (this.hasStem()) {
+      return Math.max(
+        this.stave.getYForTopText(textLine),
+        extents.baseY + (this.render_options.annotation_spacing * (textLine))
+      );
+    } else {
+      return this.stave.getYForBottomText(textLine);
+    }
+  }
+
+  hasFlag() {
+    return this.renderFlag && Flow.getGlyphProps(this.duration).flag && !this.beam;
+  }
+
+  // Post format the note
+  postFormat() {
+    if (this.beam) this.beam.postFormat();
+
+    this.postFormatted = true;
+
+    return this;
+  }
+
+  // Render the stem onto the canvas
+  drawStem(stem_struct) {
+    this.checkContext();
+    this.setRendered();
+
+    this.setStem(new Stem(stem_struct));
+    this.stem.setContext(this.context).draw();
+  }
+}

+ 91 - 0
src/VexFlowPatch/src/vibratobracket.js

@@ -0,0 +1,91 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+// Author: Balazs Forian-Szabo
+//
+// ## Description
+//
+// This file implements `VibratoBrackets`
+// that renders vibrato effect between two notes.
+
+import { Vex } from './vex';
+import { Element } from './element';
+import { Vibrato } from './vibrato';
+
+// To enable logging for this class. Set `Vex.Flow.VibratoBracket.DEBUG` to `true`.
+function L(...args) { if (VibratoBracket.DEBUG) Vex.L('Vex.Flow.VibratoBracket', args); }
+
+export class VibratoBracket extends Element {
+  // bracket_data = {
+  //   start: Vex.Flow.Note (optional)
+  //   stop: Vex.Flow.Note (optional)
+  // };
+  // Either the stop or start note must be set, or both of them.
+  // A null value for the start or stop note indicates that the vibrato
+  // is drawn from the beginning or until the end of the stave accordingly.
+  constructor(bracket_data) {
+    super();
+    this.setAttribute('type', 'VibratoBracket');
+
+    this.start = bracket_data.start;
+    this.stop = bracket_data.stop;
+    //VexFlowPatch: Needed an option to render to the end of the stop note stave vs. the stop note itself
+    this.toEndOfStopStave = bracket_data.toEndOfStopStave;
+
+    this.line = 1;
+
+    this.render_options = {
+      harsh: false,
+      wave_height: 6,
+      wave_width: 4,
+      wave_girth: 2,
+    };
+  }
+
+  // Set line position of the vibrato bracket
+  setLine(line) { this.line = line; return this; }
+  setHarsh(harsh) { this.render_options.harsh = harsh; return this; }
+
+  // Draw the vibrato bracket on the rendering context
+  draw() {
+    const ctx = this.context;
+    this.setRendered();
+    const y = (this.start)
+      ? this.start.getStave().getYForTopText(this.line)
+      : this.stop.getStave().getYForTopText(this.line);
+
+    // If start note is not set then vibrato will be drawn
+    // from the beginning of the stave
+    let start_x = 0;
+    if(this.start) {
+        let trillOffset = 0;
+        // VexFlowPatch: If we have a trill mark accompanying, need to allow space for it
+        for(const modifier of this.start.modifiers) {
+          if(modifier && modifier.type === "tr") {
+            trillOffset = modifier.glyph.bbox.w;
+            break;
+          }
+        }
+        start_x = this.start.getNoteHeadBeginX ? this.start.getNoteHeadBeginX() : this.start.getAbsoluteX();
+        start_x += trillOffset;
+    } else {
+        start_x = this.stop.getStave().getTieStartX();
+    }
+    // If stop note is not set then vibrato will be drawn
+    // until the end of the stave
+    let stop_x = 0;
+
+    if(this.stop) {
+      stop_x = (this.toEndOfStopStave) ?
+        this.stop.getStave().getTieEndX() - 10 :
+        // VexFlowPatch: Render to the end of the stop note, instead of before it
+        this.stop.getAbsoluteX() + this.stop.getWidth()
+    } else {
+      stop_x = this.start.getStave().getTieEndX() - 10;
+    }
+
+    this.render_options.vibrato_width = stop_x - start_x;
+
+    L('Rendering VibratoBracket: start_x:', start_x, 'stop_x:', stop_x, 'y:', y);
+
+    Vibrato.renderVibrato(ctx, start_x, y, this.render_options);
+  }
+}

+ 855 - 0
test/data/OSMD_Function_Test_Pedals.musicxml

@@ -0,0 +1,855 @@
+<?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: Pedals</work-title>
+    </work>
+  <identification>
+    <creator type="composer">OSMD</creator>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2021-05-25</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.09" justify="center" valign="top" font-size="22">Title</credit-words>
+    </credit>
+  <credit page="1">
+    <credit-type>composer</credit-type>
+    <credit-words default-x="1148.14" default-y="1411.09" justify="right" valign="top">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="298.65">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>-0.00</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>
+      <direction placement="below">
+        <direction-type>
+          <pedal type="start" sign="yes" default-y="-65.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="73.72" default-y="-5.00">
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="129.50" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="185.28" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="241.07" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="2" width="237.92">
+      <note default-x="13.00" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="68.78" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="124.56" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="180.34" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      </measure>
+    <measure number="3" width="237.92">
+      <note default-x="13.00" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="68.78" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="124.56" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="180.34" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="4" width="237.92">
+      <note default-x="13.00" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="68.78" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="124.56" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="180.34" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="5" width="318.18">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>-0.00</left-margin>
+            <right-margin>-0.00</right-margin>
+            </system-margins>
+          <system-distance>150.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="62.09" default-y="-55.00">
+        <pitch>
+          <step>B</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="62.09" default-y="-40.00">
+        <chord/>
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="125.66" default-y="-55.00">
+        <pitch>
+          <step>B</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="125.66" default-y="-30.00">
+        <chord/>
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="189.23" default-y="-70.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="189.23" default-y="-35.00">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="252.80" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="6" width="248.08">
+      <note default-x="13.00" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="71.32" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="129.64" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="187.96" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="7" width="248.08">
+      <note default-x="13.00" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="71.32" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="129.64" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="187.96" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      </measure>
+    <measure number="8" width="248.08">
+      <note default-x="13.00" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <pedal type="stop" line="yes"/>
+          </direction-type>
+        </direction>
+      <note default-x="129.64" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="187.96" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="187.96" default-y="0.00">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      </measure>
+    <measure number="9" width="299.80">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>-0.00</left-margin>
+            <right-margin>-0.00</right-margin>
+            </system-margins>
+          <system-distance>150.00</system-distance>
+          </system-layout>
+        </print>
+      <direction placement="below">
+        <direction-type>
+          <pedal type="start" line="yes" sign="yes" default-y="-65.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="58.59" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="58.59" default-y="-10.00">
+        <chord/>
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="118.45" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="178.30" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="238.15" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      </measure>
+    <measure number="10" width="254.21">
+      <note default-x="13.00" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="72.85" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="132.70" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="192.55" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="11" width="254.21">
+      <note default-x="13.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>
+        </note>
+      <note default-x="72.85" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <pedal type="stop" line="yes"/>
+          </direction-type>
+        </direction>
+      <note default-x="132.70" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <pedal type="start" line="yes" default-y="-65.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="192.55" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      </measure>
+    <measure number="12" width="254.21">
+      <note default-x="13.00" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="72.85" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="132.70" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="192.55" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="13" width="381.59">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>-0.00</left-margin>
+            <right-margin>-0.00</right-margin>
+            </system-margins>
+          <system-distance>150.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="58.59" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="58.59" default-y="-20.00">
+        <chord/>
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="138.89" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="219.19" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="219.19" default-y="-20.00">
+        <chord/>
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="299.49" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="14" width="335.99">
+      <note default-x="13.00" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="93.30" default-y="5.00">
+        <pitch>
+          <step>G</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="173.60" default-y="0.00">
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="253.89" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      </measure>
+    <measure number="15" width="344.84">
+      <note default-x="13.00" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="93.30" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <pedal type="stop" line="yes"/>
+          </direction-type>
+        </direction>
+      <note default-x="173.60" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="253.89" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 655 - 0
test/data/OSMD_Function_Test_Voice_Alignment.musicxml

@@ -0,0 +1,655 @@
+<?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">
+  <identification>
+    <creator type="composer">OSMD Function Test - Voice Alignment</creator>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2021-06-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.35</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1870.99</page-height>
+      <page-width>1322</page-width>
+      <page-margins type="both">
+        <left-margin>80</left-margin>
+        <right-margin>80</right-margin>
+        <top-margin>80</top-margin>
+        <bottom-margin>80</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="7"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="661" default-y="1791.92" justify="center" valign="top" font-size="22">OSMD Function Test - Voice Alignment</credit-words>
+    </credit>
+  <credit page="1">
+    <credit-type>composer</credit-type>
+    <credit-words default-x="1242" default-y="1691.92" justify="right" valign="top">OSMD</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <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>69.2913</volume>
+        <pan>90</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="344.23">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>64.89</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>8</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time symbol="common">
+          <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="97.14" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="165.27" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="177.17" default-y="-20.00">
+        <chord/>
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="233.41" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="287.92" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note default-x="97.14" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <accidental>sharp</accidental>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+        </note>
+      <note default-x="165.27" default-y="-90.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="199.34" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <accidental>sharp</accidental>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>16</duration>
+        <voice>5</voice>
+        <type>half</type>
+        <staff>2</staff>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note default-x="109.03" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>7</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>7</voice>
+        <type>eighth</type>
+        <staff>2</staff>
+        </note>
+      <note default-x="165.27" default-y="-100.00">
+        <pitch>
+          <step>B</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>7</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="199.34" default-y="-120.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>7</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <forward>
+        <duration>16</duration>
+        </forward>
+      </measure>
+    <measure number="2" width="323.55">
+      <note default-x="36.96" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <dot/>
+        <accidental>sharp</accidental>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <staff>1</staff>
+        </note>
+      <note default-x="158.49" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>12</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <dot/>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="269.96" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note default-x="20.96" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>15</duration>
+        <voice>2</voice>
+        <type>quarter</type>
+        <dot/>
+        <dot/>
+        <dot/>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="20.96" default-y="-25.00">
+        <chord/>
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>15</duration>
+        <voice>2</voice>
+        <type>quarter</type>
+        <dot/>
+        <dot/>
+        <dot/>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <forward>
+        <duration>1</duration>
+        </forward>
+      <note default-x="158.49" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <dot/>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>2</voice>
+        <type>16th</type>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <staff>1</staff>
+        </note>
+      <note default-x="281.85" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <accidental>natural</accidental>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note default-x="20.96" default-y="-150.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>2</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <accidental>sharp</accidental>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="20.96" default-y="-130.00">
+        <chord/>
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="93.82" default-y="-150.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>2</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="158.49" default-y="-150.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>2</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="231.34" default-y="-150.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>2</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <backup>
+        <duration>24</duration>
+        </backup>
+      <note default-x="93.82" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>6</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="158.49" default-y="-125.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>6</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="231.34" default-y="-125.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>6</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      </measure>
+    <measure number="3" width="170.86">
+      <note default-x="26.88" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>32</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note default-x="13.96" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>32</duration>
+        <voice>2</voice>
+        <type>whole</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note default-x="13.96" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>32</duration>
+        <voice>5</voice>
+        <type>whole</type>
+        <staff>2</staff>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note default-x="31.88" default-y="-115.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>32</duration>
+        <voice>6</voice>
+        <type>whole</type>
+        <staff>2</staff>
+        </note>
+      </measure>
+    <measure number="4" width="258.47">
+      <note default-x="18.56" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="66.37" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <dot/>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="108.76" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">end</beam>
+        <beam number="2">backward hook</beam>
+        </note>
+      <note default-x="130.49" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="178.29" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="213.06" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <accidental>sharp</accidental>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">end</beam>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note>
+        <rest measure="yes"/>
+        <duration>32</duration>
+        <voice>2</voice>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note default-x="17.60" default-y="-110.00">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>32</duration>
+        <voice>5</voice>
+        <type>whole</type>
+        <staff>2</staff>
+        </note>
+      <backup>
+        <duration>32</duration>
+        </backup>
+      <note default-x="19.52" default-y="-90.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>16</duration>
+        <voice>6</voice>
+        <type>half</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="130.49" default-y="-90.00">
+        <pitch>
+          <step>D</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>16</duration>
+        <voice>6</voice>
+        <type>half</type>
+        <accidental>sharp</accidental>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 561 - 0
test/data/OSMD_Trill_Line_Function_Test.musicxml

@@ -0,0 +1,561 @@
+<?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 Trill Line Function Test</work-title>
+    </work>
+  <identification>
+    <creator type="composer">OSMD</creator>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2021-06-15</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.99912</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.725</left-margin>
+        <right-margin>85.725</right-margin>
+        <top-margin>85.725</top-margin>
+        <bottom-margin>85.725</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.725</left-margin>
+        <right-margin>85.725</right-margin>
+        <top-margin>85.725</top-margin>
+        <bottom-margin>85.725</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.934" default-y="1511.1" justify="center" valign="top" font-size="22">OSMD Trill Line Function Test</credit-words>
+    </credit>
+  <credit page="1">
+    <credit-type>composer</credit-type>
+    <credit-words default-x="1148.14" default-y="1411.1" justify="right" valign="top">OSMD</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="429.83">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>59.26</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>4</divisions>
+        <key>
+          <fifths>3</fifths>
+          </key>
+        <time symbol="common">
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <note default-x="114.87" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>5</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <notations>
+          <ornaments>
+            <trill-mark/>
+            <wavy-line type="start" number="1" default-y="13.05"/>
+            </ornaments>
+          </notations>
+        </note>
+      <note default-x="193.16" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <notations>
+          <ornaments>
+            <wavy-line type="stop" number="1"/>
+            </ornaments>
+          </notations>
+        </note>
+      <note default-x="271.45" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="349.74" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="2" width="281.36">
+      <note default-x="13.96" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>16</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        <notations>
+          <ornaments>
+            <trill-mark/>
+            <wavy-line type="start" number="1" default-y="5.55"/>
+            <wavy-line type="stop" number="1"/>
+            </ornaments>
+          </notations>
+        </note>
+      </measure>
+    <measure number="3" width="291.97">
+      <note default-x="13.00" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="151.58" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>up</stem>
+        <notations>
+          <ornaments>
+            <trill-mark/>
+            <wavy-line type="start" number="1" default-y="9.30"/>
+            <wavy-line type="stop" number="1"/>
+            </ornaments>
+          </notations>
+        </note>
+      </measure>
+    <measure number="4" width="334.16">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>9.26</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <system-distance>150.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="98.35" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <ornaments>
+            <trill-mark/>
+            <wavy-line type="start" number="1" default-y="71.06"/>
+            </ornaments>
+          </notations>
+        </note>
+      <note default-x="123.71" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        </note>
+      <note default-x="149.06" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="174.42" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">continue</beam>
+        <beam number="2">begin</beam>
+        </note>
+      <note default-x="190.26" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        </note>
+      <note default-x="206.11" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="206.11" default-y="35.00">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>6</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="231.47" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        </note>
+      <note default-x="256.82" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        <beam number="2">begin</beam>
+        </note>
+      <note default-x="272.67" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="288.51" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="304.36" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        </note>
+      </measure>
+    <measure number="5" width="76.05">
+      <note>
+        <rest measure="yes"/>
+        <duration>16</duration>
+        <voice>1</voice>
+        </note>
+      </measure>
+    <measure number="6" width="76.24">
+      <note default-x="13.96" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>5</octave>
+          </pitch>
+        <duration>16</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        <notations>
+          <ornaments>
+            <wavy-line type="stop" number="1"/>
+            </ornaments>
+          </notations>
+        </note>
+      </measure>
+    <measure number="7" width="207.37">
+      <note default-x="13.00" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <notations>
+          <ornaments>
+            <trill-mark/>
+            <wavy-line type="start" number="1" default-y="9.30"/>
+            </ornaments>
+          </notations>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      <note default-x="61.64" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="84.83" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <beam number="1">end</beam>
+        </note>
+      <note default-x="108.01" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <notations>
+          <ornaments>
+            <wavy-line type="stop" number="1"/>
+            </ornaments>
+          </notations>
+        </note>
+      <note default-x="154.39" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      </measure>
+    <measure number="8" width="140.83">
+      <note default-x="13.00" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        </note>
+      <note default-x="79.72" default-y="-5.00">
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="109.37" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      </measure>
+    <measure number="9" width="104.84">
+      <note default-x="13.00" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>half</type>
+        </note>
+      </measure>
+    <measure number="10" width="113.69">
+      <note default-x="13.00" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>half</type>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>