소스 검색

Merge branch 'develop' of https://github.com/opensheetmusicdisplay/opensheetmusicdisplay into fix/skyline/ottava

Justin Litten 5 년 전
부모
커밋
e45a22de41
36개의 변경된 파일1359개의 추가작업 그리고 259개의 파일을 삭제
  1. 3 1
      demo/index.js
  2. 2 2
      package.json
  3. 4 1
      src/MusicalScore/Graphical/AbstractGraphicalExpression.ts
  4. 38 14
      src/MusicalScore/Graphical/EngravingRules.ts
  5. 27 5
      src/MusicalScore/Graphical/GraphicalContinuousDynamicExpression.ts
  6. 1 1
      src/MusicalScore/Graphical/GraphicalInstantaneousDynamicExpression.ts
  7. 1 1
      src/MusicalScore/Graphical/GraphicalInstantaneousTempoExpression.ts
  8. 27 24
      src/MusicalScore/Graphical/GraphicalSlur.ts
  9. 40 0
      src/MusicalScore/Graphical/GraphicalUnknownExpression.ts
  10. 78 10
      src/MusicalScore/Graphical/MusicSheetCalculator.ts
  11. 2 2
      src/MusicalScore/Graphical/MusicSystemBuilder.ts
  12. 4 4
      src/MusicalScore/Graphical/SkyBottomLineCalculator.ts
  13. 51 2
      src/MusicalScore/Graphical/StaffLine.ts
  14. 66 5
      src/MusicalScore/Graphical/VexFlow/AlignmentManager.ts
  15. 4 2
      src/MusicalScore/Graphical/VexFlow/VexFlowContinuousDynamicExpression.ts
  16. 22 0
      src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts
  17. 64 1
      src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts
  18. 18 15
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts
  19. 5 7
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts
  20. 89 0
      src/MusicalScore/Graphical/VexFlow/VexflowStafflineNoteCalculator.ts
  21. 6 0
      src/MusicalScore/Interfaces/IStafflineNoteCalculator.ts
  22. 12 0
      src/MusicalScore/ScoreIO/InstrumentReader.ts
  23. 67 46
      src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts
  24. 2 0
      src/MusicalScore/VoiceData/ChordSymbolContainer.ts
  25. 3 0
      src/MusicalScore/VoiceData/Expressions/AbstractExpression.ts
  26. 5 1
      src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression.ts
  27. 4 1
      src/MusicalScore/VoiceData/Expressions/InstantaneousDynamicExpression.ts
  28. 15 13
      src/MusicalScore/VoiceData/Expressions/MultiExpression.ts
  29. 14 0
      src/MusicalScore/VoiceData/Expressions/MultiTempoExpression.ts
  30. 7 1
      src/MusicalScore/VoiceData/Staff.ts
  31. 45 0
      src/OpenSheetMusicDisplay/OSMDOptions.ts
  32. 11 3
      src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts
  33. 106 82
      test/Util/generateImages_browserless.js
  34. 516 0
      test/data/OSMD_Function_Test_Drums_one_line_snare_plus_piano.musicxml
  35. 0 10
      test/data/OSMD_function_test_Ornaments.xml
  36. 0 5
      test/data/OSMD_function_test_expressions.musicxml

+ 3 - 1
demo/index.js

@@ -29,6 +29,7 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
             "OSMD Function Test - Bar lines": "OSMD_function_test_bar_lines.musicxml",
             "OSMD Function Test - Color (from XML)": "OSMD_function_test_color.musicxml",
             "OSMD Function Test - Drumset": "OSMD_function_test_drumset.musicxml",
+            "OSMD Function Test - Drums on one Line": "OSMD_Function_Test_Drums_one_line_snare_plus_piano.musicxml", 
             "OSMD Function Test - Expressions": "OSMD_function_test_expressions.musicxml",
             "OSMD Function Test - Expressions Overlap": "OSMD_function_test_expressions_overlap.musicxml",
             "OSMD Function Test - Grace Notes": "OSMD_function_test_GraceNotes.xml",
@@ -382,12 +383,14 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         if (skylineDebug) {
             skylineDebug.onclick = function () {
                 openSheetMusicDisplay.DrawSkyLine = !openSheetMusicDisplay.DrawSkyLine;
+                openSheetMusicDisplay.render();
             }
         }
 
         if (bottomlineDebug) {
             bottomlineDebug.onclick = function () {
                 openSheetMusicDisplay.DrawBottomLine = !openSheetMusicDisplay.DrawBottomLine;
+                openSheetMusicDisplay.render();
             }
         }
 
@@ -436,7 +439,6 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
                 //groups: [[3,4], [1,1]],
                 maintain_stem_directions: false
             },
-
             pageFormat: pageFormat,
             pageBackgroundColor: pageBackgroundColor,
             renderSingleHorizontalStaffline: singleHorizontalStaffline

+ 2 - 2
package.json

@@ -26,7 +26,7 @@
     "generatePNG:paged:single": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 0 0 ^Beethoven",
     "generate:current": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 allSmall --osmdtesting",
     "generate:current:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 allSmall --debugosmdtesting",
-    "generate:current:singletest": "node test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 .*function_test_all.*",
+    "generate:current:singletest": "node test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 .*function_test_all.* --osmdtesting",
     "generate:blessed": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/blessed 0 0 allSmall --osmdtesting",
     "test:visual": "sh ./test/Util/visual_regression.sh ./visual_regression",
     "test:visual:singletest": "sh ./test/Util/visual_regression.sh ./visual_regression OSMD_function_test_all",
@@ -71,7 +71,7 @@
   "devDependencies": {
     "@types/chai": "^4.2.11",
     "@types/mocha": "^7.0.2",
-    "@types/node": "^14.0.5",
+    "@types/node": "^14.0.9",
     "canvas": "^2.6.1",
     "chai": "^4.1.0",
     "clean-webpack-plugin": "^3.0.0",

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

@@ -4,6 +4,7 @@ import { StaffLine } from "./StaffLine";
 import { BoundingBox } from "./BoundingBox";
 import { AbstractExpression, PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
 import { EngravingRules } from "./EngravingRules";
+import { SourceMeasure } from "../VoiceData";
 
 export abstract class AbstractGraphicalExpression extends GraphicalObject {
     protected label: GraphicalLabel;
@@ -12,10 +13,12 @@ export abstract class AbstractGraphicalExpression extends GraphicalObject {
     protected expression: AbstractExpression;
     /** EngravingRules for positioning */
     protected rules: EngravingRules;
+    protected parentMeasure: SourceMeasure;
 
-    constructor(parentStaffline: StaffLine, expression: AbstractExpression) {
+    constructor(parentStaffline: StaffLine, expression: AbstractExpression, measure: SourceMeasure) {
         super();
         this.expression = expression;
+        this.parentMeasure = measure; // could be undefined!
         this.boundingBox = new BoundingBox(this, parentStaffline.PositionAndShape);
         this.parentStaffLine = parentStaffline;
         this.parentStaffLine.AbstractExpressions.push(this);

+ 38 - 14
src/MusicalScore/Graphical/EngravingRules.ts

@@ -41,7 +41,7 @@ export class EngravingRules {
     private staffDistance: number;
     private betweenStaffDistance: number;
     private staffHeight: number;
-    private tabStaffHeight: number;
+    private tabStaffInterlineHeight: number;
     private betweenStaffLinesDistance: number;
     /** Whether to automatically beam notes that don't already have beams in XML. */
     private autoBeamNotes: boolean;
@@ -52,6 +52,8 @@ export class EngravingRules {
     private beamForwardLength: number;
     private clefLeftMargin: number;
     private clefRightMargin: number;
+    private percussionOneLineCutoff: number;
+    private percussionForceVoicesOneLineCutoff: number;
     private betweenKeySymbolsDistance: number;
     private keyRightMargin: number;
     private rhythmRightMargin: number;
@@ -91,7 +93,6 @@ export class EngravingRules {
     private chordSymbolTextHeight: number;
     private chordSymbolXSpacing: number;
     private chordSymbolYOffset: number;
-    private fingeringLabelFontHeight: number;
     private measureNumberLabelHeight: number;
     private measureNumberLabelOffset: number;
     /** Whether tuplets should display ratio (3:2 instead of 3 for triplet). Default false. */
@@ -218,6 +219,8 @@ export class EngravingRules {
     /** Position of fingering label in relation to corresponding note (left, right supported, above, below experimental) */
     private fingeringPosition: PlacementEnum;
     private fingeringInsideStafflines: boolean;
+    private fingeringLabelFontHeight: number;
+    private fingeringOffsetX: number;
     private newSystemAtXMLNewSystemAttribute: boolean;
     private newPageAtXMLNewPageAttribute: boolean;
     private pageFormat: PageFormat;
@@ -254,7 +257,7 @@ export class EngravingRules {
 
         // System Sizing and Label Variables
         this.staffHeight = 4.0;
-        this.tabStaffHeight = 6.67;
+        this.tabStaffInterlineHeight = 1.1111;
         this.betweenStaffLinesDistance = EngravingRules.unit;
         this.systemLeftMargin = 0.0;
         this.systemRightMargin = 0.0;
@@ -281,6 +284,8 @@ export class EngravingRules {
         // Beam Sizing Variables
         this.clefLeftMargin = 0.5;
         this.clefRightMargin = 0.75;
+        this.percussionOneLineCutoff = 4;
+        this.percussionForceVoicesOneLineCutoff = 3;
         this.betweenKeySymbolsDistance = 0.2;
         this.keyRightMargin = 0.75;
         this.rhythmRightMargin = 1.25;
@@ -333,7 +338,6 @@ export class EngravingRules {
         this.chordSymbolTextHeight = 2.0;
         this.chordSymbolXSpacing = 1.0;
         this.chordSymbolYOffset = 2.0;
-        this.fingeringLabelFontHeight = 1.7;
 
         // Tuplets, MeasureNumber and TupletNumber Labels
         this.measureNumberLabelHeight = 1.5 * EngravingRules.unit;
@@ -453,6 +457,8 @@ export class EngravingRules {
         this.renderLyrics = true;
         this.fingeringPosition = PlacementEnum.Left; // easier to get bounding box, and safer for vertical layout
         this.fingeringInsideStafflines = false;
+        this.fingeringLabelFontHeight = 1.7;
+        this.fingeringOffsetX = 0.0;
         this.newSystemAtXMLNewSystemAttribute = false;
         this.newPageAtXMLNewPageAttribute = false;
         this.restoreCursorAfterRerender = true;
@@ -642,11 +648,11 @@ export class EngravingRules {
     public set StaffHeight(value: number) {
         this.staffHeight = value;
     }
-    public get TabStaffHeight(): number {
-        return this.tabStaffHeight;
+    public get TabStaffInterlineHeight(): number {
+        return this.tabStaffInterlineHeight;
     }
-    public set TabStaffHeight(value: number) {
-        this.tabStaffHeight = value;
+    public set TabStaffInterlineHeight(value: number) {
+        this.tabStaffInterlineHeight = value;
     }
     public get BetweenStaffLinesDistance(): number {
         return this.betweenStaffLinesDistance;
@@ -702,6 +708,18 @@ export class EngravingRules {
     public set ClefRightMargin(value: number) {
         this.clefRightMargin = value;
     }
+    public get PercussionOneLineCutoff(): number {
+        return this.percussionOneLineCutoff;
+    }
+    public set PercussionOneLineCutoff(value: number) {
+        this.percussionOneLineCutoff = value;
+    }
+    public get PercussionForceVoicesOneLineCutoff(): number {
+        return this.percussionForceVoicesOneLineCutoff;
+    }
+    public set PercussionForceVoicesOneLineCutoff(value: number) {
+        this.percussionForceVoicesOneLineCutoff = value;
+    }
     public get KeyRightMargin(): number {
         return this.keyRightMargin;
     }
@@ -936,12 +954,6 @@ export class EngravingRules {
     public set ChordSymbolYOffset(value: number) {
         this.chordSymbolYOffset = value;
     }
-    public get FingeringLabelFontHeight(): number {
-        return this.fingeringLabelFontHeight;
-    }
-    public set FingeringLabelFontHeight(value: number) {
-        this.fingeringLabelFontHeight = value;
-    }
     public get MeasureNumberLabelHeight(): number {
         return this.measureNumberLabelHeight;
     }
@@ -1603,6 +1615,18 @@ export class EngravingRules {
     public set FingeringInsideStafflines(value: boolean) {
         this.fingeringInsideStafflines = value;
     }
+    public get FingeringLabelFontHeight(): number {
+        return this.fingeringLabelFontHeight;
+    }
+    public set FingeringLabelFontHeight(value: number) {
+        this.fingeringLabelFontHeight = value;
+    }
+    public get FingeringOffsetX(): number {
+        return this.fingeringOffsetX;
+    }
+    public set FingeringOffsetX(value: number) {
+        this.fingeringOffsetX = value;
+    }
     public get NewSystemAtXMLNewSystemAttribute(): boolean {
         return this.newSystemAtXMLNewSystemAttribute;
     }

+ 27 - 5
src/MusicalScore/Graphical/GraphicalContinuousDynamicExpression.ts

@@ -8,6 +8,7 @@ import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
 import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
 import { ISqueezable } from "./ISqueezable";
 import log from "loglevel";
+import { SourceMeasure } from "../VoiceData";
 
 /**
  * This class prepares the graphical elements for a continuous expression. It calculates the wedges and
@@ -26,10 +27,10 @@ export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpre
     /**
      * Create a new instance of the GraphicalContinuousDynamicExpression
      * @param continuousDynamic The continuous dynamic instruction read via ExpressionReader
-     * @param staffLine The staffline where the exoression is attached
+     * @param staffLine The staffline where the expression is attached
      */
-    constructor(continuousDynamic: ContinuousDynamicExpression, staffLine: StaffLine) {
-        super(staffLine, continuousDynamic);
+    constructor(continuousDynamic: ContinuousDynamicExpression, staffLine: StaffLine, measure: SourceMeasure) {
+        super(staffLine, continuousDynamic, measure);
 
         this.isSplittedPart = false;
         this.notToBeRemoved = false;
@@ -69,10 +70,22 @@ export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpre
         if (!this.IsVerbal && this.lines.length < 2) {
             log.warn("Not enough lines for SkyBottomLine calculation");
         }
+        if (!this.IsVerbal) {
+            if (this.ContinuousDynamic.DynamicType !== ContDynamicEnum.crescendo &&
+                this.ContinuousDynamic.DynamicType !== ContDynamicEnum.diminuendo) {
+                // for now there is only crescendo or decrescendo anyways, but this will catch errors when we add new types in the future
+                log.warn("GraphicalContinuousDynamicExpression.updateSkyBottomLine(): " +
+                    "unhandled continuous dynamic type. start measure: " + this.startMeasure?.MeasureNumber);
+            }
+        }
         switch (this.Placement) {
             case PlacementEnum.Above:
                 if (!this.IsVerbal) {
-                    skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[0].Start, this.lines[0].End);
+                    if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
+                        skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[0].Start, this.lines[0].End);
+                    } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
+                        skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[0].End, this.lines[0].Start);
+                    } // else covered with the log.warn above
                 } else {
                     const yValue: number = this.label.PositionAndShape.BorderMarginTop + this.label.PositionAndShape.RelativePosition.y;
                     skyBottomLineCalculator.updateSkyLineInRange(left, right, yValue);
@@ -80,7 +93,12 @@ export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpre
                 break;
             case PlacementEnum.Below:
                 if (!this.IsVerbal) {
-                    skyBottomLineCalculator.updateBottomLineWithWedge(this.lines[1].Start, this.lines[1].End);
+                    // console.log(`id: ${this.parentStaffLine.ParentStaff.Id}`);
+                    if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
+                        skyBottomLineCalculator.updateBottomLineWithWedge(this.lines[1].Start, this.lines[1].End);
+                    } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
+                        skyBottomLineCalculator.updateBottomLineWithWedge(this.lines[1].End, this.lines[1].Start);
+                    } // else covered with the log.warn above
                 } else {
                     const yValue: number = this.label.PositionAndShape.BorderMarginBottom + this.label.PositionAndShape.RelativePosition.y;
                     skyBottomLineCalculator.updateBottomLineInRange(left, right, yValue);
@@ -247,6 +265,10 @@ export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpre
         this.PositionAndShape.RelativePosition = this.lines[0].Start;
         this.PositionAndShape.BorderMarginTop = this.lines[0].End.y - this.lines[0].Start.y;
         this.PositionAndShape.BorderMarginBottom = this.lines[1].End.y - this.lines[1].Start.y;
+        this.PositionAndShape.Center.y = (this.PositionAndShape.BorderMarginTop + this.PositionAndShape.BorderMarginBottom) / 2;
+        // TODO is the center position correct? it wasn't set before, important for AlignmentManager.alignDynamicExpressions()
+        // console.log(`relative y, center y: ${this.PositionAndShape.RelativePosition.y},${this.PositionAndShape.Center.y})`);
+
 
         if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
             this.PositionAndShape.BorderMarginLeft = 0;

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

@@ -11,7 +11,7 @@ export class GraphicalInstantaneousDynamicExpression extends AbstractGraphicalEx
     protected mMeasure: GraphicalMeasure;
 
     constructor(instantaneousDynamic: InstantaneousDynamicExpression, staffLine: StaffLine, measure: GraphicalMeasure) {
-        super(staffLine, instantaneousDynamic);
+        super(staffLine, instantaneousDynamic, measure.parentSourceMeasure);
         this.mInstantaneousDynamicExpression = instantaneousDynamic;
         this.mMeasure = measure;
     }

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

@@ -7,7 +7,7 @@ import { AbstractGraphicalExpression } from "./AbstractGraphicalExpression";
 export class GraphicalInstantaneousTempoExpression extends AbstractGraphicalExpression {
 
     constructor(tempoExpresssion: AbstractTempoExpression, label: GraphicalLabel) {
-        super((label.PositionAndShape.Parent.DataObject as StaffLine), tempoExpresssion);
+        super((label.PositionAndShape.Parent.DataObject as StaffLine), tempoExpresssion, tempoExpresssion.parentMeasure);
         this.label = label;
     }
 

+ 27 - 24
src/MusicalScore/Graphical/GraphicalSlur.ts

@@ -566,32 +566,35 @@ export class GraphicalSlur extends GraphicalCurve {
         const startStaffEntry: GraphicalStaffEntry = this.staffEntries[0];
         const endStaffEntry: GraphicalStaffEntry = this.staffEntries[this.staffEntries.length - 1];
 
-        // Deactivated: single Voice, opposite to StemDirection
-        // if (startStaffEntry.hasStem() && endStaffEntry.hasStem() && startStaffEntry.getStemDirection() === endStaffEntry.getStemDirection()) {
-        //     this.placement = (startStaffEntry.getStemDirection() === StemDirectionType.Up) ? PlacementEnum.Below : PlacementEnum.Above;
-        // } else {
-
-        // Placement at the side with the minimum border
-        let sX: number = startStaffEntry.PositionAndShape.BorderLeft + startStaffEntry.PositionAndShape.RelativePosition.x
-                    + startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
-        let eX: number = endStaffEntry.PositionAndShape.BorderRight + endStaffEntry.PositionAndShape.RelativePosition.x
-                    + endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
-
-        if (this.graceStart) {
-            sX += endStaffEntry.PositionAndShape.RelativePosition.x;
-        }
-        if (this.graceEnd) {
-            eX += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
-        }
+        // single Voice, opposite to StemDirection
+        // here should only be one voiceEntry, so we can take graphicalVoiceEntries[0]:
+        const startStemDirection: StemDirectionType = startStaffEntry.graphicalVoiceEntries[0].parentVoiceEntry.StemDirection;
+        const endStemDirection: StemDirectionType = endStaffEntry.graphicalVoiceEntries[0].parentVoiceEntry.StemDirection;
+        if (startStemDirection  ===
+            endStemDirection) {
+            this.placement = (startStemDirection === StemDirectionType.Up) ? PlacementEnum.Below : PlacementEnum.Above;
+        } else {
+            // Placement at the side with the minimum border
+            let sX: number = startStaffEntry.PositionAndShape.BorderLeft + startStaffEntry.PositionAndShape.RelativePosition.x
+                        + startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
+            let eX: number = endStaffEntry.PositionAndShape.BorderRight + endStaffEntry.PositionAndShape.RelativePosition.x
+                        + endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
+
+            if (this.graceStart) {
+                sX += endStaffEntry.PositionAndShape.RelativePosition.x;
+            }
+            if (this.graceEnd) {
+                eX += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
+            }
 
-        // get SkyBottomLine borders
-        const minAbove: number = skyBottomLineCalculator.getSkyLineMinInRange(sX, eX) * -1;
-        const maxBelow: number = skyBottomLineCalculator.getBottomLineMaxInRange(sX, eX) - staffLine.StaffHeight;
+            // get SkyBottomLine borders
+            const minAbove: number = skyBottomLineCalculator.getSkyLineMinInRange(sX, eX) * -1;
+            const maxBelow: number = skyBottomLineCalculator.getBottomLineMaxInRange(sX, eX) - staffLine.StaffHeight;
 
-        if (maxBelow > minAbove) {
-            this.placement = PlacementEnum.Above;
-        } else { this.placement = PlacementEnum.Below; }
-        //}
+            if (maxBelow > minAbove) {
+                this.placement = PlacementEnum.Above;
+            } else { this.placement = PlacementEnum.Below; }
+        }
     }
 
     /**

+ 40 - 0
src/MusicalScore/Graphical/GraphicalUnknownExpression.ts

@@ -0,0 +1,40 @@
+
+import { StaffLine } from "./StaffLine";
+import { GraphicalLabel } from "./GraphicalLabel";
+import { AbstractGraphicalExpression } from "./AbstractGraphicalExpression";
+import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
+import { MultiExpression } from "../VoiceData/Expressions/MultiExpression";
+import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
+import log from "loglevel";
+import { SourceMeasure } from "../VoiceData/SourceMeasure";
+
+export class GraphicalUnknownExpression extends AbstractGraphicalExpression {
+    public sourceMultiExpression: MultiExpression;
+    public placement: PlacementEnum;
+
+    constructor(staffLine: StaffLine, label: GraphicalLabel, measure: SourceMeasure,
+                sourceMultiExpression: MultiExpression = undefined) {
+        super(staffLine, undefined, measure);
+        this.label = label;
+        this.sourceMultiExpression = sourceMultiExpression;
+    }
+
+    public updateSkyBottomLine(): void {
+        // update Sky-BottomLine
+        const skyBottomLineCalculator: SkyBottomLineCalculator = this.parentStaffLine.SkyBottomLineCalculator;
+        const left: number = this.label.PositionAndShape.RelativePosition.x + this.label.PositionAndShape.BorderMarginLeft;
+        const right: number = this.label.PositionAndShape.RelativePosition.x + this.label.PositionAndShape.BorderMarginRight;
+        switch (this.Placement) {
+            case PlacementEnum.Above:
+                const yValueAbove: number = this.label.PositionAndShape.BorderMarginTop + this.label.PositionAndShape.RelativePosition.y;
+                skyBottomLineCalculator.updateSkyLineInRange(left, right, yValueAbove);
+                break;
+            case PlacementEnum.Below:
+                const yValueBelow: number = this.label.PositionAndShape.BorderMarginBottom + this.label.PositionAndShape.RelativePosition.y;
+                skyBottomLineCalculator.updateBottomLineInRange(left, right, yValueBelow);
+                break;
+            default:
+                log.error("Placement for GraphicalUnknownExpression is unknown");
+        }
+    }
+}

+ 78 - 10
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -21,7 +21,7 @@ import { Tuplet } from "../VoiceData/Tuplet";
 import { MusicSystem } from "./MusicSystem";
 import { GraphicalTie } from "./GraphicalTie";
 import { RepetitionInstruction } from "../VoiceData/Instructions/RepetitionInstruction";
-import { MultiExpression } from "../VoiceData/Expressions/MultiExpression";
+import { MultiExpression, MultiExpressionEntry } from "../VoiceData/Expressions/MultiExpression";
 import { StaffEntryLink } from "../VoiceData/StaffEntryLink";
 import { MusicSystemBuilder } from "./MusicSystemBuilder";
 import { MultiTempoExpression } from "../VoiceData/Expressions/MultiTempoExpression";
@@ -67,6 +67,8 @@ import { GraphicalInstantaneousDynamicExpression } from "./GraphicalInstantaneou
 import { ContDynamicEnum } from "../VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression";
 import { GraphicalContinuousDynamicExpression } from "./GraphicalContinuousDynamicExpression";
 import { FillEmptyMeasuresWithWholeRests } from "../../OpenSheetMusicDisplay/OSMDOptions";
+import { IStafflineNoteCalculator } from "../Interfaces/IStafflineNoteCalculator";
+import { GraphicalUnknownExpression } from "./GraphicalUnknownExpression";
 
 /**
  * Class used to do all the calculations in a MusicSheet, which in the end populates a GraphicalMusicSheet.
@@ -74,6 +76,7 @@ import { FillEmptyMeasuresWithWholeRests } from "../../OpenSheetMusicDisplay/OSM
 export abstract class MusicSheetCalculator {
     public static symbolFactory: IGraphicalSymbolFactory;
     public static transposeCalculator: ITransposeCalculator;
+    public static stafflineNoteCalculator: IStafflineNoteCalculator;
     protected static textMeasurer: ITextMeasurer;
 
     protected staffEntriesWithGraphicalTies: GraphicalStaffEntry[] = [];
@@ -543,7 +546,53 @@ export abstract class MusicSheetCalculator {
      * @param staffIndex
      */
     protected calculateMoodAndUnknownExpression(multiExpression: MultiExpression, measureIndex: number, staffIndex: number): void {
-        throw new Error("abstract, not implemented");
+        // calculate absolute Timestamp
+        const absoluteTimestamp: Fraction = multiExpression.AbsoluteTimestamp;
+        const measures: GraphicalMeasure[] = this.graphicalMusicSheet.MeasureList[measureIndex];
+        let relative: PointF2D = new PointF2D();
+
+        if ((multiExpression.MoodList.length > 0) || (multiExpression.UnknownList.length > 0)) {
+        let combinedExprString: string  = "";
+        for (let idx: number = 0, len: number = multiExpression.EntriesList.length; idx < len; ++idx) {
+            const entry: MultiExpressionEntry = multiExpression.EntriesList[idx];
+            if (entry.prefix !== "") {
+                if (combinedExprString === "") {
+                    combinedExprString += entry.prefix;
+                } else {
+                    combinedExprString += " " + entry.prefix;
+                }
+            }
+            if (combinedExprString === "") {
+                combinedExprString += entry.label;
+            } else {
+                combinedExprString += " " + entry.label;
+            }
+        }
+        const staffLine: StaffLine = measures[staffIndex].ParentStaffLine;
+        if (!staffLine) {
+            log.debug("MusicSheetCalculator.calculateMoodAndUnknownExpression: staffLine undefined. Returning.");
+            return;
+        }
+        relative = this.getRelativePositionInStaffLineFromTimestamp(absoluteTimestamp, staffIndex, staffLine, staffLine?.isPartOfMultiStaffInstrument());
+
+        if (Math.abs(relative.x - 0) < 0.0001) {
+            relative.x = measures[staffIndex].beginInstructionsWidth + this.rules.RhythmRightMargin;
+        }
+
+        const fontHeight: number = this.rules.UnknownTextHeight;
+
+        const graphLabel: GraphicalLabel  = this.calculateLabel(staffLine,
+                                                                relative, combinedExprString,
+                                                                multiExpression.getFontstyleOfFirstEntry(),
+                                                                multiExpression.getPlacementOfFirstEntry(),
+                                                                fontHeight);
+
+        const gue: GraphicalUnknownExpression = new GraphicalUnknownExpression(
+            staffLine, graphLabel, measures[staffIndex]?.parentSourceMeasure, multiExpression);
+        //    multiExpression); // TODO would be nice to hand over and save reference to original expression,
+        //                         but MultiExpression is not an AbstractExpression.
+        staffLine.AbstractExpressions.push(gue);
+        }
     }
 
     /**
@@ -879,7 +928,7 @@ export abstract class MusicSheetCalculator {
         const endMeasure: GraphicalMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(
             graphicalContinuousDynamic.ContinuousDynamic.EndMultiExpression.SourceMeasureParent, staffIndex);
         if (!endMeasure) {
-            log.warn("Not working");
+            log.warn("MusicSheetCalculator.calculateGraphicalContinuousDynamic: No endMeasure found");
             return;
         }
 
@@ -892,9 +941,9 @@ export abstract class MusicSheetCalculator {
 
         let isPartOfMultiStaffInstrument: boolean = false;
         if (endStaffLine) { // unfortunately we can't do something like (endStaffLine?.check() || staffLine?.check()) in this typescript version
-            isPartOfMultiStaffInstrument = endStaffLine.isPartOfMultiStaffInstrument();
+            isPartOfMultiStaffInstrument = endStaffLine?.isPartOfMultiStaffInstrument();
         } else if (staffLine) {
-            isPartOfMultiStaffInstrument = staffLine.isPartOfMultiStaffInstrument();
+            isPartOfMultiStaffInstrument = staffLine?.isPartOfMultiStaffInstrument();
         }
 
         const endAbsoluteTimestamp: Fraction = Fraction.createFromFraction(graphicalContinuousDynamic.ContinuousDynamic.EndMultiExpression.AbsoluteTimestamp);
@@ -931,7 +980,8 @@ export abstract class MusicSheetCalculator {
             lowerEndX = endPosInStaffLine.x;
 
             // must create a new Wedge
-            secondGraphicalContinuousDynamic = new GraphicalContinuousDynamicExpression(graphicalContinuousDynamic.ContinuousDynamic, endStaffLine);
+            secondGraphicalContinuousDynamic = new GraphicalContinuousDynamicExpression(
+                graphicalContinuousDynamic.ContinuousDynamic, endStaffLine, endMeasure.parentSourceMeasure);
             secondGraphicalContinuousDynamic.IsSplittedPart = true;
             graphicalContinuousDynamic.IsSplittedPart = true;
         } else {
@@ -1159,6 +1209,9 @@ export abstract class MusicSheetCalculator {
                                                                startPosInStaffline: PointF2D): void {
         // get Margin Dimensions
         const staffLine: StaffLine = graphicalInstantaneousDynamic.ParentStaffLine;
+        if (!staffLine) {
+            return; // TODO can happen when drawing range modified (osmd.setOptions({drawFromMeasureNumber...}))
+        }
         const left: number = startPosInStaffline.x + graphicalInstantaneousDynamic.PositionAndShape.BorderMarginLeft;
         const right: number = startPosInStaffline.x + graphicalInstantaneousDynamic.PositionAndShape.BorderMarginRight;
         const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
@@ -1485,6 +1538,8 @@ export abstract class MusicSheetCalculator {
                 graphicalNote = MusicSheetCalculator.symbolFactory.createGraceNote(note, gve, activeClef, octaveShiftValue);
             } else {
                 graphicalNote = MusicSheetCalculator.symbolFactory.createNote(note, gve, activeClef, octaveShiftValue, undefined);
+                const staffLineCount: number = voiceEntry.ParentSourceStaffEntry.ParentStaff.StafflineCount;
+                graphicalNote = MusicSheetCalculator.stafflineNoteCalculator.positionNote(graphicalNote, activeClef, staffLineCount);
             }
             if (note.Pitch !== undefined) {
                 this.checkNoteForAccidental(graphicalNote, accidentalCalculator, activeClef, octaveShiftValue);
@@ -1928,6 +1983,17 @@ export abstract class MusicSheetCalculator {
                                    staffEntryLinks: StaffEntryLink[]): GraphicalMeasure {
         const staff: Staff = this.graphicalMusicSheet.ParentMusicSheet.getStaffFromIndex(staffIndex);
         let measure: GraphicalMeasure = undefined;
+        //This property is active...
+        if (this.rules.PercussionOneLineCutoff !== undefined && this.rules.PercussionOneLineCutoff !== 0) {
+            //We have a percussion clef, check to see if this property applies...
+            if (activeClefs[staffIndex].ClefType === ClefEnum.percussion) {
+                //-1 means always trigger, or we are under the cutoff number specified
+                if (this.rules.PercussionOneLineCutoff === -1 ||
+                    staff.ParentInstrument.SubInstruments.length < this.rules.PercussionOneLineCutoff) {
+                    staff.StafflineCount = 1;
+                }
+            }
+        }
         if (activeClefs[staffIndex].ClefType === ClefEnum.TAB) {
             staff.isTab = true;
             measure = MusicSheetCalculator.symbolFactory.createTabStaffMeasure(sourceMeasure, staff);
@@ -2070,10 +2136,12 @@ export abstract class MusicSheetCalculator {
                 graphicalStaffEntry.relInMeasureTimestamp = voiceEntry.Timestamp;
                 const gve: GraphicalVoiceEntry = MusicSheetCalculator.symbolFactory.createVoiceEntry(voiceEntry, graphicalStaffEntry);
                 graphicalStaffEntry.graphicalVoiceEntries.push(gve);
-                const graphicalNote: GraphicalNote = MusicSheetCalculator.symbolFactory.createNote(note,
-                                                                                                   gve,
-                                                                                                   new ClefInstruction(),
-                                                                                                   OctaveEnum.NONE, undefined);
+                let graphicalNote: GraphicalNote = MusicSheetCalculator.symbolFactory.createNote(note,
+                                                                                                 gve,
+                                                                                                 new ClefInstruction(),
+                                                                                                 OctaveEnum.NONE, undefined);
+                const staffLineCount: number = voiceEntry.ParentSourceStaffEntry.ParentStaff.StafflineCount;
+                graphicalNote = MusicSheetCalculator.stafflineNoteCalculator.positionNote(graphicalNote, activeClefs[staffIndex], staffLineCount);
                 gve.notes.push(graphicalNote);
             }
         }

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

@@ -442,7 +442,7 @@ export class MusicSystemBuilder {
                 if (abstractNotationInstruction instanceof ClefInstruction) {
                     currentClef = <ClefInstruction>abstractNotationInstruction;
                 } else if (abstractNotationInstruction instanceof KeyInstruction) {
-                    currentKey = KeyInstruction.copy(<KeyInstruction>abstractNotationInstruction);
+                    currentKey = <KeyInstruction>abstractNotationInstruction;
                 } else if (abstractNotationInstruction instanceof RhythmInstruction) {
                     currentRhythm = <RhythmInstruction>abstractNotationInstruction;
                 }
@@ -453,7 +453,7 @@ export class MusicSystemBuilder {
                 currentClef = this.activeClefs[visibleStaffIdx];
             }
             if (currentKey === undefined) {
-                currentKey = KeyInstruction.copy(this.activeKeys[visibleStaffIdx]);
+                currentKey = this.activeKeys[visibleStaffIdx];
             }
             if (isFirstSourceMeasure && currentRhythm === undefined) {
                 currentRhythm = this.activeRhythm[visibleStaffIdx];

+ 4 - 4
src/MusicalScore/Graphical/SkyBottomLineCalculator.ts

@@ -146,8 +146,8 @@ export class SkyBottomLineCalculator {
             log.debug(`SkyLine calculation was not correct (${this.mSkyLine.length} instead of ${arrayLength})`);
         }
         // Remap the values from 0 to +/- height in units
-        this.mSkyLine = this.mSkyLine.map(v => (v - Math.max(...this.mSkyLine)) / unitInPixels);
-        this.mBottomLine = this.mBottomLine.map(v => (v - Math.min(...this.mBottomLine)) / unitInPixels + this.StaffLineParent.StaffHeight);
+        this.mSkyLine = this.mSkyLine.map(v => (v - Math.max(...this.mSkyLine)) / unitInPixels + this.StaffLineParent.TopLineOffset);
+        this.mBottomLine = this.mBottomLine.map(v => (v - Math.min(...this.mBottomLine)) / unitInPixels + this.StaffLineParent.BottomLineOffset);
     }
 
     /**
@@ -166,8 +166,8 @@ export class SkyBottomLineCalculator {
 
     /**
      * This method updates the SkyLine for a given Wedge.
-     * @param start Start point of the wedge
-     * @param end End point of the wedge
+     * @param start Start point of the wedge (the point where both lines meet)
+     * @param end End point of the wedge (the end of the most extreme line: upper line for skyline, lower line for bottomline)
      */
     public updateSkyLineWithWedge(start: PointF2D, end: PointF2D): void {
         // FIXME: Refactor if wedges will be added. Current status is that vexflow will be used for this

+ 51 - 2
src/MusicalScore/Graphical/StaffLine.ts

@@ -30,6 +30,8 @@ export abstract class StaffLine extends GraphicalObject {
     protected abstractExpressions: AbstractGraphicalExpression[] = [];
     /** The staff height in units */
     private staffHeight: number;
+    private topLineOffset: number;
+    private bottomLineOffset: number;
 
     // For displaying Slurs
     protected graphicalSlurs: GraphicalSlur[] = [];
@@ -41,8 +43,48 @@ export abstract class StaffLine extends GraphicalObject {
         this.boundingBox = new BoundingBox(this, parentSystem.PositionAndShape);
         this.skyBottomLine = new SkyBottomLineCalculator(this);
         this.staffHeight = this.parentMusicSystem.rules.StaffHeight;
-        if (this.parentStaff.isTab) {
-            this.staffHeight = this.parentMusicSystem.rules.TabStaffHeight;
+        this.topLineOffset = 0;
+        this.bottomLineOffset = 4;
+
+        this.calculateStaffLineOffsets();
+    }
+
+    /**
+     * If the musicXML sets different numbers of stafflines, we need to have different offsets
+     * to accomodate this - primarily for the sky and bottom lines and cursor.
+     */
+    private calculateStaffLineOffsets(): void {
+        if (this.ParentStaff.isTab) {
+            switch (this.ParentStaff.StafflineCount) {
+                case 5:
+                    this.staffHeight = this.bottomLineOffset =
+                        this.ParentStaff.ParentInstrument.GetMusicSheet.Rules.TabStaffInterlineHeight * 6;
+                    break;
+                default:
+                    this.staffHeight = this.bottomLineOffset =
+                        this.ParentStaff.ParentInstrument.GetMusicSheet.Rules.TabStaffInterlineHeight * this.ParentStaff.StafflineCount;
+                    break;
+            }
+        } else {
+            switch (this.ParentStaff.StafflineCount) {
+                case 4:
+                    this.bottomLineOffset = 1;
+                    break;
+                case 3:
+                    this.topLineOffset = 1;
+                    this.bottomLineOffset = 1;
+                    break;
+                case 2:
+                    this.topLineOffset = 2;
+                    this.bottomLineOffset = 1;
+                    break;
+                case 1:
+                    this.topLineOffset = 2;
+                    this.bottomLineOffset = 2;
+                    break;
+                default:
+                    break;
+            }
         }
     }
 
@@ -131,6 +173,13 @@ export abstract class StaffLine extends GraphicalObject {
         return this.staffHeight;
     }
 
+    public get TopLineOffset(): number {
+        return this.topLineOffset;
+    }
+    public get BottomLineOffset(): number {
+        return this.bottomLineOffset;
+    }
+
     // get all Graphical Slurs of a staffline
     public get GraphicalSlurs(): GraphicalSlur[] {
         return this.graphicalSlurs;

+ 66 - 5
src/MusicalScore/Graphical/VexFlow/AlignmentManager.ts

@@ -4,6 +4,8 @@ import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicEx
 import { AbstractGraphicalExpression } from "../AbstractGraphicalExpression";
 import { PointF2D } from "../../../Common/DataObjects/PointF2D";
 import { EngravingRules } from "../EngravingRules";
+import { PlacementEnum } from "../../VoiceData/Expressions";
+import { GraphicalUnknownExpression } from "../GraphicalUnknownExpression";
 
 export class AlignmentManager {
     private parentStaffline: StaffLine;
@@ -21,9 +23,54 @@ export class AlignmentManager {
         for (let aeIdx: number = 0; aeIdx < this.parentStaffline.AbstractExpressions.length - 1; aeIdx++) {
             const currentExpression: AbstractGraphicalExpression = this.parentStaffline.AbstractExpressions[aeIdx];
             const nextExpression: AbstractGraphicalExpression = this.parentStaffline.AbstractExpressions[aeIdx + 1];
-            if (currentExpression.Placement === nextExpression.Placement) {
+
+            let currentExpressionPlacement: PlacementEnum = undefined;
+            if (currentExpression?.SourceExpression) {
+                currentExpressionPlacement = currentExpression.Placement;
+            } else if (currentExpression instanceof GraphicalUnknownExpression) {
+                currentExpressionPlacement = (currentExpression as GraphicalUnknownExpression).
+                    sourceMultiExpression?.getPlacementOfFirstEntry();
+            }
+            // same for nextExpression:
+            let nextExpressionPlacement: PlacementEnum = undefined;
+            if (nextExpression?.SourceExpression) {
+                nextExpressionPlacement = nextExpression.Placement;
+            } else if (nextExpression instanceof GraphicalUnknownExpression) {
+                nextExpressionPlacement = (nextExpression as GraphicalUnknownExpression).
+                    sourceMultiExpression?.getPlacementOfFirstEntry();
+            }
+
+            // if (currentExpression?.SourceExpression === undefined ||
+            //     nextExpression?.SourceExpression === undefined) {
+            //     continue;
+            //     // TODO: this doesn't work yet for GraphicalUnknownExpression, because it doesn't have an AbstractExpression,
+            //     //   so it doesn't have a .Placement.
+            //     //   this lead to if (currentExpression.Placement...) crashing.
+
+            //     // same result:
+            //     // if (currentExpression instanceof GraphicalUnknownExpression ||
+            //     //     nextExpression instanceof GraphicalUnknownExpression) {
+            //     //         continue;
+            //     // }
+            // } else {
+            //     samePlacement = currentExpression.Placement === nextExpression.Placement;
+            // }
+
+            // TODO this shifts dynamics in An die Ferne Geliebte, showing that there's something wrong with the RelativePositions etc with wedges
+            // if (currentExpression instanceof GraphicalContinuousDynamicExpression) {
+            //     currentExpression.calcPsi();
+            // }
+            // if (nextExpression instanceof GraphicalContinuousDynamicExpression) {
+            //     nextExpression.calcPsi();
+            // }
+
+            if (currentExpressionPlacement === nextExpressionPlacement) {
+                // if ((currentExpression as any).label?.label?.text?.startsWith("dim") ||
+                //     (nextExpression as any).label?.label?.text?.startsWith("dim")) {
+                //         console.log("here");
+                //     }
                 const dist: PointF2D = this.getDistance(currentExpression.PositionAndShape, nextExpression.PositionAndShape);
-                if (dist.x < this.rules.DynamicExpressionMaxDistance) {
+                if (Math.abs(dist.x) < this.rules.DynamicExpressionMaxDistance) {
                     // Prevent last found expression to be added twice. e.g. p<f as three close expressions
                     if (tmpList.indexOf(currentExpression) === -1) {
                         tmpList.push(currentExpression);
@@ -42,18 +89,31 @@ export class AlignmentManager {
             if (aes.length > 0) {
                 // Get the median y position and shift all group members to that position
                 const centerYs: number[] = aes.map(expr => expr.PositionAndShape.Center.y);
+                // TODO this may not give the right position for wedges (GraphicalContinuousDynamic, !isVerbal())
                 const yIdeal: number = Math.max(...centerYs);
+                // for (const ae of aes) { // debug
+                //     if (ae.PositionAndShape.Center.y > 6) {
+                //         // dynamic positioned at edge of skybottomline
+                //         console.log(`max expression in measure ${ae.SourceExpression.parentMeasure.MeasureNumber}: `);
+                //         console.dir(aes);
+                //     }
+                // }
+
                 for (let exprIdx: number = 0; exprIdx < aes.length; exprIdx++) {
                     const expr: AbstractGraphicalExpression = aes[exprIdx];
                     const centerOffset: number = centerYs[exprIdx] - yIdeal;
+                    // TODO centerOffset is way too big sometimes, like 7.0 in An die Ferne Geliebte (measure 10, dim.)
                     // FIXME: Expressions should not behave differently.
                     if (expr instanceof VexFlowContinuousDynamicExpression) {
                         (expr as VexFlowContinuousDynamicExpression).shiftYPosition(-centerOffset);
+                        (expr as VexFlowContinuousDynamicExpression).calcPsi();
                     } else {
-                        // TODO: The 0.8 are because the letters are a bit to far done
+                        // TODO: The 0.8 are because the letters are a bit too far done
                         expr.PositionAndShape.RelativePosition.y -= centerOffset * 0.8;
+                        // note: verbal GraphicalContinuousDynamicExpressions have a label, nonverbal ones don't.
+                        // take care to update and take the right bounding box for skyline.
+                        expr.PositionAndShape.calculateBoundingBox();
                     }
-                    expr.PositionAndShape.calculateBoundingBox();
                     // Squeeze wedges
                     if ((expr as VexFlowContinuousDynamicExpression).squeeze) {
                         const nextExpression: AbstractGraphicalExpression = exprIdx < aes.length - 1 ? aes[exprIdx + 1] : undefined;
@@ -80,10 +140,11 @@ export class AlignmentManager {
     private getDistance(a: BoundingBox, b: BoundingBox): PointF2D {
         const rightBorderA: number = a.RelativePosition.x + a.BorderMarginRight;
         const leftBorderB: number = b.RelativePosition.x + b.BorderMarginLeft;
-        const bottomBorderA: number = b.RelativePosition.y + a.BorderMarginBottom;
+        const bottomBorderA: number = a.RelativePosition.y + a.BorderMarginBottom;
         const topBorderB: number = b.RelativePosition.y + b.BorderMarginTop;
         return new PointF2D(leftBorderB - rightBorderA,
                             topBorderB - bottomBorderA);
+                            // note: this is a distance vector, not absolute distance, otherwise we need Math.abs
     }
 
     /**

+ 4 - 2
src/MusicalScore/Graphical/VexFlow/VexFlowContinuousDynamicExpression.ts

@@ -5,13 +5,15 @@ import { GraphicalLabel } from "../GraphicalLabel";
 import { Label } from "../../Label";
 import { TextAlignmentEnum } from "../../../Common/Enums/TextAlignment";
 import { FontStyles } from "../../../Common/Enums/FontStyles";
+import { SourceMeasure } from "../../VoiceData/SourceMeasure";
 
 /**
  * This class extends the GraphicalContinuousDynamicExpression and creates all necessary methods for drawing
  */
 export class VexFlowContinuousDynamicExpression extends GraphicalContinuousDynamicExpression {
-    constructor(continuousDynamic: ContinuousDynamicExpression, staffLine: StaffLine, textHeight?: number) {
-        super(continuousDynamic, staffLine);
+    constructor(continuousDynamic: ContinuousDynamicExpression, staffLine: StaffLine,
+                measure: SourceMeasure, textHeight?: number) {
+        super(continuousDynamic, staffLine, measure);
         if (this.IsVerbal) {
             const sourceLabel: Label = new Label(continuousDynamic.Label);
             this.label = new GraphicalLabel(sourceLabel,

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

@@ -207,6 +207,28 @@ export class VexFlowConverter {
                     xShift = rules.WholeRestXShiftVexflow * unitInPixels; // TODO find way to make dependent on the modifiers
                     // affects VexFlowStaffEntry.calculateXPosition()
                 }
+                if (note.sourceNote.ParentStaff.Voices.length > 1) {
+                    let visibleVoiceEntries: number = 0;
+                    //Find all visible voice entries (don't want invisible rests/notes causing visible shift)
+                    for (let idx: number = 0; idx < note.sourceNote.ParentStaffEntry.VoiceEntries.length ; idx++) {
+                        if (note.sourceNote.ParentStaffEntry.VoiceEntries[idx].Notes[0].PrintObject) {
+                            visibleVoiceEntries++;
+                        }
+                    }
+                    //If we have more than one visible voice entry, shift the rests so no collision occurs
+                    if (visibleVoiceEntries > 1) {
+                        switch (note.sourceNote.ParentVoiceEntry?.ParentVoice?.VoiceId) {
+                            case 1:
+                                keys = ["e/5"];
+                                break;
+                            case 2:
+                                keys = ["f/4"];
+                                break;
+                            default:
+                                break;
+                        }
+                    }
+                }
                 break;
             }
 

+ 64 - 1
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -107,6 +107,10 @@ export class VexFlowMeasure extends GraphicalMeasure {
             space_above_staff_ln: 0,
             space_below_staff_ln: 0,
         });
+
+        if (this.ParentStaff !== undefined) {
+            this.setLineNumber(this.ParentStaff.StafflineCount);
+        }
         // constructor sets beginning and end bar type to standard
 
         this.stave.setBegBarType(Vex.Flow.Barline.type.NONE); // technically not correct, but we'd need to set the next measure's beginning bar type
@@ -168,6 +172,64 @@ export class VexFlowMeasure extends GraphicalMeasure {
     }
 
     /**
+     * Sets the number of stafflines that are rendered, so that they are centered properly
+     * @param lineNumber
+     */
+    public setLineNumber(lineNumber: number): void {
+        if (lineNumber !== 5) {
+            if (lineNumber === 0) {
+                (this.stave as any).setNumLines(0);
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(this.options.num_lines);
+                };
+            } else if (lineNumber === 1) {
+                // Vex.Flow.Stave.setNumLines hides all but the top line.
+                // this is better
+                (this.stave.options as any).line_config = [
+                    { visible: false },
+                    { visible: false },
+                    { visible: true }, // show middle
+                    { visible: false },
+                    { visible: false },
+                ];
+                //quick fix to see if this matters for calculation. Doesn't seem to
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(2);
+                };
+                //lines (which isn't this case here)
+                //this.stave.options.num_lines = parseInt(lines, 10);
+            } else if (lineNumber === 2) {
+                (this.stave.options as any).line_config = [
+                    { visible: false },
+                    { visible: false },
+                    { visible: true }, // show middle
+                    { visible: true },
+                    { visible: false },
+                ];
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(3);
+                };
+            } else if (lineNumber === 3) {
+                (this.stave.options as any).line_config = [
+                    { visible: false },
+                    { visible: true },
+                    { visible: true }, // show middle
+                    { visible: true },
+                    { visible: false },
+                ];
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(2);
+                };
+            } else {
+                (this.stave as any).setNumLines(lineNumber);
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(this.options.num_lines);
+                };
+            }
+        }
+    }
+
+    /**
      * adds the given key to the begin of the measure.
      * This has to update/increase BeginInstructionsWidth.
      * @param currentKey the new valid key.
@@ -1179,6 +1241,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
 
             const fretFinger: Vex.Flow.FretHandFinger = new Vex.Flow.FretHandFinger(fingeringInstruction.value);
             fretFinger.setPosition(modifierPosition);
+            fretFinger.setOffsetX(this.rules.FingeringOffsetX);
             if (fingeringPosition === PlacementEnum.Above || fingeringPosition === PlacementEnum.Below) {
                 const offsetYSign: number = fingeringPosition === PlacementEnum.Above ? -1 : 1; // minus y is up
                 const ordering: number = fingeringPosition === PlacementEnum.Above ? fingeringIndex :
@@ -1188,7 +1251,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
                     // set y-shift. vexflow fretfinger simply places directly above/below note
                     const perFingeringShift: number = fretFinger.getWidth() / 2;
                     const shiftCount: number = fingeringsCount * 2.5;
-                    (<any>fretFinger).setOffsetY(offsetYSign * (ordering + shiftCount) * perFingeringShift);
+                    fretFinger.setOffsetY(offsetYSign * (ordering + shiftCount) * perFingeringShift);
                 } else if (!this.rules.FingeringInsideStafflines) { // use StringNumber for placement above/below stafflines
                     const stringNumber: Vex.Flow.StringNumber = new Vex.Flow.StringNumber(fingeringInstruction.value);
                     (<any>stringNumber).radius = 0; // hack to remove the circle around the number

+ 18 - 15
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -47,7 +47,8 @@ import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicEx
 import { InstantaneousTempoExpression } from "../../VoiceData/Expressions";
 import { AlignRestOption } from "../../../OpenSheetMusicDisplay";
 import { VexFlowStaffLine } from "./VexFlowStaffLine";
-import { EngravingRules } from "..";
+import { EngravingRules } from "../EngravingRules";
+import { VexflowStafflineNoteCalculator } from "./VexflowStafflineNoteCalculator";
 
 export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
   /** space needed for a dash for lyrics spacing, calculated once */
@@ -59,10 +60,12 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     this.rules = rules;
     MusicSheetCalculator.symbolFactory = new VexFlowGraphicalSymbolFactory();
     MusicSheetCalculator.TextMeasurer = new VexFlowTextMeasurer(this.rules);
+    MusicSheetCalculator.stafflineNoteCalculator = new VexflowStafflineNoteCalculator(this.rules);
   }
 
   protected clearRecreatedObjects(): void {
     super.clearRecreatedObjects();
+    MusicSheetCalculator.stafflineNoteCalculator = new VexflowStafflineNoteCalculator(this.rules);
     for (const graphicalMeasures of this.graphicalMusicSheet.MeasureList) {
       for (const graphicalMeasure of graphicalMeasures) {
         (<VexFlowMeasure>graphicalMeasure).clean();
@@ -501,7 +504,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
       absoluteTimestamp,
       staffIndex,
       staffLine,
-      staffLine.isPartOfMultiStaffInstrument());
+      staffLine?.isPartOfMultiStaffInstrument());
 
     const dynamicStartPosition: PointF2D = startPosInStaffline;
     if (startPosInStaffline.x <= 0) {
@@ -519,7 +522,8 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
       const continuousDynamic: ContinuousDynamicExpression = multiExpression.StartingContinuousDynamic;
       const graphicalContinuousDynamic: VexFlowContinuousDynamicExpression = new VexFlowContinuousDynamicExpression(
         multiExpression.StartingContinuousDynamic,
-        staffLine);
+        staffLine,
+        startMeasure.parentSourceMeasure);
       graphicalContinuousDynamic.StartMeasure = startMeasure;
 
       if (!graphicalContinuousDynamic.IsVerbal && continuousDynamic.EndMultiExpression) {
@@ -720,26 +724,25 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
   }
 
-  protected calculateMoodAndUnknownExpression(multiExpression: MultiExpression, measureIndex: number, staffIndex: number): void {
-    return;
-  }
-
   /**
    * Re-adjust the x positioning of expressions. Update the skyline afterwards
    */
   protected calculateExpressionAlignements(): void {
     for (const musicSystem of this.musicSystems) {
-            for (const staffLine of musicSystem.StaffLines) {
-              try {
-                (<VexFlowStaffLine>staffLine).AlignmentManager.alignDynamicExpressions();
-                staffLine.AbstractExpressions.forEach(ae => ae.updateSkyBottomLine());
-              } catch (e) {
-                // TODO still necessary when calculation of expression fails, see calculateDynamicExpressionsForMultiExpression()
-                //   see calculateGraphicalContinuousDynamic(), also in MusicSheetCalculator.
+      for (const staffLine of musicSystem.StaffLines) {
+        try {
+          (<VexFlowStaffLine>staffLine).AlignmentManager.alignDynamicExpressions();
+          staffLine.AbstractExpressions.forEach(ae => {
+            ae.updateSkyBottomLine();
+          });
+        } catch (e) {
+          // TODO still necessary when calculation of expression fails, see calculateDynamicExpressionsForMultiExpression()
+          //   see calculateGraphicalContinuousDynamic(), also in MusicSheetCalculator.
         }
+      }
     }
   }
-  }
+
 
   /**
    * Check if the tied graphical note belongs to any beams or tuplets and react accordingly.

+ 5 - 7
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts

@@ -27,6 +27,7 @@ import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicEx
 import { DrawingParameters } from "../DrawingParameters";
 import { GraphicalMusicPage } from "../GraphicalMusicPage";
 import { GraphicalMusicSheet } from "../GraphicalMusicSheet";
+import { GraphicalUnknownExpression } from "../GraphicalUnknownExpression";
 
 /**
  * This is a global constant which denotes the height in pixels of the space between two lines of the stave
@@ -342,13 +343,10 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
                 // // Draw Mood
                 // } else if (abstractGraphicalExpression instanceof GraphicalMoodExpression) {
                 //     GraphicalMoodExpression; graphicalMood = (GraphicalMoodExpression); abstractGraphicalExpression;
-                //     drawLabel(graphicalMood.GetGraphicalLabel, (int)GraphicalLayers.Notes);
-                // // Draw Unknown
-                // } else if (abstractGraphicalExpression instanceof GraphicalUnknownExpression) {
-                //     GraphicalUnknownExpression; graphicalUnknown =
-                //         (GraphicalUnknownExpression); abstractGraphicalExpression;
-                //     drawLabel(graphicalUnknown.GetGraphicalLabel, (int)GraphicalLayers.Notes);
-                // }
+                //     drawLabel(graphicalMood.GetGraphicalLabel, <number>GraphicalLayers.Notes);
+            // Draw Unknown
+            } else if (abstractGraphicalExpression instanceof GraphicalUnknownExpression) {
+                this.drawLabel(abstractGraphicalExpression.Label, <number>GraphicalLayers.Notes);
             } else {
                 log.warn("Unkown type of expression!");
             }

+ 89 - 0
src/MusicalScore/Graphical/VexFlow/VexflowStafflineNoteCalculator.ts

@@ -0,0 +1,89 @@
+import { IStafflineNoteCalculator } from "../../Interfaces/IStafflineNoteCalculator";
+import { GraphicalNote } from "../GraphicalNote";
+import { ClefInstruction, ClefEnum } from "../../VoiceData";
+import { Pitch, NoteEnum, AccidentalEnum } from "../../../Common";
+import { VexFlowGraphicalNote } from "./VexFlowGraphicalNote";
+import { Dictionary } from "typescript-collections";
+import { EngravingRules } from "../EngravingRules";
+
+export class VexflowStafflineNoteCalculator implements IStafflineNoteCalculator {
+    private instrumentVoiceMapping: Dictionary<string, Dictionary<number, {note: NoteEnum, octave: number}>> =
+                                                new Dictionary<string, Dictionary<number, {note: NoteEnum, octave: number}>>();
+    private rules: EngravingRules;
+    private voiceIdx: number = 0;
+
+    constructor(rules: EngravingRules) {
+        this.rules = rules;
+    }
+  /**
+   * This method is called for each note, and should make any necessary position changes based on the number of stafflines, clef, etc.
+   * Right now this just directly maps a voice number to a position above or below a staffline
+   * @param graphicalNote The note to be checked/positioned
+   * @param currentClef The clef that is active for this note
+   * @param stafflineCount The number of stafflines we are rendering on
+   * @returns the minimum required x width of the source measure (=list of staff measures)
+   */
+    public positionNote(graphicalNote: GraphicalNote, currentClef: ClefInstruction, stafflineCount: number): GraphicalNote {
+        if (!(graphicalNote instanceof VexFlowGraphicalNote) || currentClef.ClefType !== ClefEnum.percussion ||
+        graphicalNote.sourceNote.isRest() || stafflineCount > 1 || this.rules.PercussionOneLineCutoff === 0 ) {
+            return graphicalNote;
+        }
+
+        const forceOneLineCutoff: number = this.rules.PercussionForceVoicesOneLineCutoff;
+        const forceOneLine: boolean = (forceOneLineCutoff !== undefined && forceOneLineCutoff !== 0) &&
+                                       (forceOneLineCutoff === -1 ||
+                                        graphicalNote.sourceNote.ParentStaff.ParentInstrument.SubInstruments.length < forceOneLineCutoff);
+
+        const instrumentId: string = graphicalNote.sourceNote.PlaybackInstrumentId;
+        const voiceNumber: number = graphicalNote.parentVoiceEntry.parentVoiceEntry.ParentVoice.VoiceId;
+        let currentInstrumentMapping: Dictionary<number, {note: NoteEnum, octave: number}> = undefined;
+
+        if (!this.instrumentVoiceMapping.containsKey(instrumentId)) {
+            currentInstrumentMapping = new Dictionary<number, {note: NoteEnum, octave: number}>();
+            this.instrumentVoiceMapping.setValue(instrumentId, currentInstrumentMapping);
+        } else {
+            currentInstrumentMapping = this.instrumentVoiceMapping.getValue(instrumentId);
+        }
+
+        let fundamental: NoteEnum = NoteEnum.B;
+        let octave: number = 1;
+        const vfGraphicalNote: VexFlowGraphicalNote = graphicalNote as VexFlowGraphicalNote;
+
+        //if we are forcing to one line, just set to B
+        if (!forceOneLine) {
+            if (!currentInstrumentMapping.containsKey(voiceNumber)) {
+                //Direct mapping for more than one voice, position voices
+                switch (this.voiceIdx % 5) {
+                    case 1:
+                        fundamental = NoteEnum.A;
+                        break;
+                    case 2:
+                        fundamental = NoteEnum.F;
+                        break;
+                    case 3:
+                        fundamental = NoteEnum.D;
+                        break;
+                    case 4:
+                        fundamental = NoteEnum.B;
+                        octave = 0;
+                        break;
+                    default:
+                        fundamental = NoteEnum.C;
+                        octave = 2;
+                        break;
+                }
+                //For every new instrument/voice for a instrument, render on diff line
+                this.voiceIdx++;
+                currentInstrumentMapping.setValue(voiceNumber, {note: fundamental, octave: octave});
+            } else {
+                const storageObj: {note: NoteEnum, octave: number} = currentInstrumentMapping.getValue(voiceNumber);
+                fundamental = storageObj.note;
+                octave = storageObj.octave;
+            }
+        }
+
+        //TODO: Check for playback side effects
+        vfGraphicalNote.setAccidental(new Pitch(fundamental, octave, AccidentalEnum.NONE));
+        return graphicalNote;
+    }
+}

+ 6 - 0
src/MusicalScore/Interfaces/IStafflineNoteCalculator.ts

@@ -0,0 +1,6 @@
+import { GraphicalNote } from "../Graphical/GraphicalNote";
+import { ClefInstruction } from "../VoiceData";
+
+export interface IStafflineNoteCalculator {
+    positionNote(graphicalNote: GraphicalNote, currentClef: ClefInstruction, stafflineCount: number): GraphicalNote;
+}

+ 12 - 0
src/MusicalScore/ScoreIO/InstrumentReader.ts

@@ -435,6 +435,18 @@ export class InstrumentReader {
           if (this.isAttributesNodeAtEndOfMeasure(this.xmlMeasureList[this.currentXmlMeasureIndex], xmlNode)) {
             this.saveClefInstructionAtEndOfMeasure();
           }
+          const staffDetailsNode: IXmlElement = xmlNode.element("staff-details");
+          if (staffDetailsNode) {
+            const staffLinesNode: IXmlElement = staffDetailsNode.element("staff-lines");
+            if (staffLinesNode) {
+              let staffNumber: number = 1;
+              const staffNumberAttr: Attr = staffDetailsNode.attribute("number");
+              if (staffNumberAttr) {
+                staffNumber = parseInt(staffNumberAttr.value, 10);
+              }
+              this.instrument.Staves[staffNumber - 1].StafflineCount = parseInt(staffLinesNode.value, 10);
+            }
+          }
         } else if (xmlNode.name === "forward") {
           const forFraction: number = parseInt(xmlNode.element("duration").value, 10);
           currentFraction.Add(new Fraction(forFraction, 4 * this.divisions));

+ 67 - 46
src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts

@@ -3,7 +3,7 @@ import {Fraction} from "../../../Common/DataObjects/Fraction";
 import {MultiTempoExpression} from "../../VoiceData/Expressions/MultiTempoExpression";
 import {ContDynamicEnum, ContinuousDynamicExpression} from "../../VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression";
 import {ContinuousTempoExpression} from "../../VoiceData/Expressions/ContinuousExpressions/ContinuousTempoExpression";
-import {DynamicEnum, InstantaneousDynamicExpression} from "../../VoiceData/Expressions/InstantaneousDynamicExpression";
+import {InstantaneousDynamicExpression} from "../../VoiceData/Expressions/InstantaneousDynamicExpression";
 import {OctaveShift} from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
 import {Instrument} from "../../Instrument";
 import {MultiExpression} from "../../VoiceData/Expressions/MultiExpression";
@@ -308,34 +308,42 @@ export class ExpressionReader {
                 expressionText = dynamicsNode.elements()[0].value;
             }
             if (expressionText !== undefined) {
-                let dynamicEnum: DynamicEnum;
-                try {
-                    dynamicEnum = DynamicEnum[expressionText];
-                } catch (err) {
-                    const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/DynamicError", "Error while reading dynamic.");
-                    this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
-                    return;
+                // // ToDo: add doublettes recognition again as a afterReadingModule, as we can't check here if there is a repetition:
+                // // Make here a comparison with the active dynamic expression and only add it, if there is a change in dynamic
+                // // Exception is when there starts a repetition, where this might be different when repeating.
+                // // see PR #767 where this was removed
+                // let dynamicEnum: DynamicEnum;
+                // try {
+                //     dynamicEnum = DynamicEnum[expressionText];
+                // } catch (err) {
+                //     const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/DynamicError", "Error while reading dynamic.");
+                //     this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                //     return;
+                // }
+                // if (this.activeInstantaneousDynamic === undefined ||
+                //     (this.activeInstantaneousDynamic !== undefined && this.activeInstantaneousDynamic.DynEnum !== dynamicEnum)) {
+                if (!fromNotation) {
+                    this.createNewMultiExpressionIfNeeded(currentMeasure);
+                } else { this.createNewMultiExpressionIfNeeded(currentMeasure, Fraction.createFromFraction(inSourceMeasureCurrentFraction)); }
+                if (this.openContinuousDynamicExpression !== undefined &&
+                    this.openContinuousDynamicExpression.StartMultiExpression !== this.getMultiExpression) {
+                    this.closeOpenContinuousDynamic();
                 }
-
-                if (this.activeInstantaneousDynamic === undefined ||
-                    (this.activeInstantaneousDynamic !== undefined && this.activeInstantaneousDynamic.DynEnum !== dynamicEnum)) {
-                    if (!fromNotation) {
-                        this.createNewMultiExpressionIfNeeded(currentMeasure);
-                    } else { this.createNewMultiExpressionIfNeeded(currentMeasure, Fraction.createFromFraction(inSourceMeasureCurrentFraction)); }
-                    if (this.openContinuousDynamicExpression !== undefined &&
-                        this.openContinuousDynamicExpression.StartMultiExpression !== this.getMultiExpression) {
-                        this.closeOpenContinuousDynamic();
-                    }
-                    const instantaneousDynamicExpression: InstantaneousDynamicExpression = new InstantaneousDynamicExpression(expressionText,
-                                                                                                                              this.soundDynamic,
-                                                                                                                              this.placement,
-                                                                                                                              this.staffNumber);
-                    this.getMultiExpression.addExpression(instantaneousDynamicExpression, "");
-                    this.initialize();
-                    if (this.activeInstantaneousDynamic !== undefined) {
-                        this.activeInstantaneousDynamic.DynEnum = instantaneousDynamicExpression.DynEnum;
-                    } else { this.activeInstantaneousDynamic = new InstantaneousDynamicExpression(expressionText, 0, PlacementEnum.NotYetDefined, 1); }
+                const instantaneousDynamicExpression: InstantaneousDynamicExpression =
+                    new InstantaneousDynamicExpression(
+                        expressionText,
+                        this.soundDynamic,
+                        this.placement,
+                        this.staffNumber,
+                        currentMeasure);
+                this.getMultiExpression.addExpression(instantaneousDynamicExpression, "");
+                this.initialize();
+                if (this.activeInstantaneousDynamic !== undefined) {
+                    this.activeInstantaneousDynamic.DynEnum = instantaneousDynamicExpression.DynEnum;
+                } else {
+                    this.activeInstantaneousDynamic = new InstantaneousDynamicExpression(expressionText, 0, PlacementEnum.NotYetDefined, 1, currentMeasure);
                 }
+                //}
             }
         }
     }
@@ -357,7 +365,7 @@ export class ExpressionReader {
             this.directionTimestamp = Fraction.createFromFraction(inSourceMeasureCurrentFraction);
         }
         this.createNewMultiExpressionIfNeeded(currentMeasure);
-        this.addWedge(wedgeNode, currentMeasureIndex);
+        this.addWedge(wedgeNode, currentMeasure);
         this.initialize();
     }
     private createNewMultiExpressionIfNeeded(currentMeasure: SourceMeasure, timestamp: Fraction = undefined): void {
@@ -381,13 +389,17 @@ export class ExpressionReader {
             currentMeasure.TempoExpressions.push(this.currentMultiTempoExpression);
         }
     }
-    private addWedge(wedgeNode: IXmlElement, currentMeasureIndex: number): void {
+    private addWedge(wedgeNode: IXmlElement, currentMeasure: SourceMeasure): void {
         if (wedgeNode !== undefined && wedgeNode.hasAttributes) {
             const type: string = wedgeNode.attribute("type").value.toLowerCase();
             try {
                 if (type === "crescendo" || type === "diminuendo") {
-                    const continuousDynamicExpression: ContinuousDynamicExpression = new ContinuousDynamicExpression(ContDynamicEnum[type],
-                                                                                                                     this.placement, this.staffNumber);
+                    const continuousDynamicExpression: ContinuousDynamicExpression =
+                        new ContinuousDynamicExpression(
+                            ContDynamicEnum[type],
+                            this.placement,
+                            this.staffNumber,
+                            currentMeasure);
                     if (this.openContinuousDynamicExpression !== undefined) {
                         this.closeOpenContinuousDynamic();
                     }
@@ -416,11 +428,11 @@ export class ExpressionReader {
         }
         const tmpInputString: string = inputString.trim();
         // split string at enumerating words or signs
-        const splitStrings: string[] = tmpInputString.split(/([\s,\r\n]and[\s,\r\n]|[\s,\r\n]und[\s,\r\n]|[\s,\r\n]e[\s,\r\n]|[\s,\r\n])+/g);
+        //const splitStrings: string[] = tmpInputString.split(/([\s,\r\n]and[\s,\r\n]|[\s,\r\n]und[\s,\r\n]|[\s,\r\n]e[\s,\r\n]|[\s,\r\n])+/g);
 
-        for (const splitStr of splitStrings) {
-            this.createExpressionFromString("", splitStr, currentMeasure, inputString);
-        }
+        //for (const splitStr of splitStrings) {
+        this.createExpressionFromString("", tmpInputString, currentMeasure, inputString);
+        //}
     }
     /*
     private splitStringRecursive(input: [string, string], stringSeparators: string[]): [string, string][] {
@@ -492,18 +504,24 @@ export class ExpressionReader {
                 if (this.openContinuousDynamicExpression !== undefined && this.openContinuousDynamicExpression.EndMultiExpression === undefined) {
                     this.closeOpenContinuousDynamic();
                 }
-                const instantaneousDynamicExpression: InstantaneousDynamicExpression = new InstantaneousDynamicExpression(stringTrimmed,
-                                                                                                                          this.soundDynamic,
-                                                                                                                          this.placement,
-                                                                                                                          this.staffNumber);
+                const instantaneousDynamicExpression: InstantaneousDynamicExpression =
+                    new InstantaneousDynamicExpression(
+                        stringTrimmed,
+                        this.soundDynamic,
+                        this.placement,
+                        this.staffNumber,
+                        currentMeasure);
                 this.getMultiExpression.addExpression(instantaneousDynamicExpression, prefix);
                 return true;
             }
             if (ContinuousDynamicExpression.isInputStringContinuousDynamic(stringTrimmed)) {
-                const continuousDynamicExpression: ContinuousDynamicExpression = new ContinuousDynamicExpression( undefined,
-                                                                                                                  this.placement,
-                                                                                                                  this.staffNumber,
-                                                                                                                  stringTrimmed);
+                const continuousDynamicExpression: ContinuousDynamicExpression =
+                    new ContinuousDynamicExpression(
+                        undefined,
+                        this.placement,
+                        this.staffNumber,
+                        currentMeasure,
+                        stringTrimmed);
                 if (this.openContinuousDynamicExpression !== undefined && this.openContinuousDynamicExpression.EndMultiExpression === undefined) {
                     this.closeOpenContinuousDynamic();
                 }
@@ -525,6 +543,7 @@ export class ExpressionReader {
 
         // create unknown:
         this.createNewMultiExpressionIfNeeded(currentMeasure);
+        // check here first if there might be a tempo expression doublette:
         if (currentMeasure.TempoExpressions.length > 0) {
             for (let idx: number = 0, len: number = currentMeasure.TempoExpressions.length; idx < len; ++idx) {
                 const multiTempoExpression: MultiTempoExpression = currentMeasure.TempoExpressions[idx];
@@ -532,9 +551,11 @@ export class ExpressionReader {
                     multiTempoExpression.InstantaneousTempo !== undefined &&
                     multiTempoExpression.EntriesList.length > 0 &&
                     !this.hasDigit(stringTrimmed)) {
-                    if (this.globalStaffIndex > 0) {
-                        if (multiTempoExpression.EntriesList[0].label.indexOf(stringTrimmed) >= 0) {
-                            return false;
+                        // if at other parts of the score
+                        if (this.globalStaffIndex > 0) {
+                            // don't add duplicate TempoExpression
+                            if (multiTempoExpression.EntriesList[0].label.indexOf(stringTrimmed) >= 0) {
+                                return false;
                         } else {
                             break;
                         }

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

@@ -188,6 +188,8 @@ export class ChordSymbolContainer {
             case ChordSymbolEnum.German:
             case ChordSymbolEnum.pedal:
             case ChordSymbolEnum.power:
+                text += "5";
+                break;
             case ChordSymbolEnum.Tristan:
                 break;
             default:

+ 3 - 0
src/MusicalScore/VoiceData/Expressions/AbstractExpression.ts

@@ -1,5 +1,8 @@
+import { SourceMeasure } from "../SourceMeasure";
+
 export class AbstractExpression {
     protected placement: PlacementEnum;
+    public parentMeasure: SourceMeasure; // could be undefined
 
     constructor(placement: PlacementEnum) {
         this.placement = placement;

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

@@ -1,10 +1,13 @@
 import {PlacementEnum, AbstractExpression} from "../AbstractExpression";
 import {MultiExpression} from "../MultiExpression";
 import {Fraction} from "../../../../Common/DataObjects/Fraction";
+import {SourceMeasure} from "../../SourceMeasure";
 
 export class ContinuousDynamicExpression extends AbstractExpression {
-    constructor(dynamicType: ContDynamicEnum, placement: PlacementEnum, staffNumber: number, label: string = "") {
+    constructor(dynamicType: ContDynamicEnum, placement: PlacementEnum, staffNumber: number, measure: SourceMeasure,
+                label: string = "") {
         super(placement);
+        super.parentMeasure = measure;
         this.dynamicType = dynamicType;
         this.label = label;
         this.staffNumber = staffNumber;
@@ -114,5 +117,6 @@ export class ContinuousDynamicExpression extends AbstractExpression {
 
 export enum ContDynamicEnum {
     crescendo = 0,
+    /** Diminuendo/Decrescendo. These terms are apparently sometimes synonyms, and a falling wedge is given in MusicXML as type="diminuendo". */
     diminuendo = 1
 }

+ 4 - 1
src/MusicalScore/VoiceData/Expressions/InstantaneousDynamicExpression.ts

@@ -4,10 +4,13 @@ import {DynamicExpressionSymbolEnum} from "./DynamicExpressionSymbolEnum";
 //import {ArgumentOutOfRangeException} from "../../Exceptions";
 import {InvalidEnumArgumentException} from "../../Exceptions";
 import log from "loglevel";
+import { SourceMeasure } from "../SourceMeasure";
 
 export class InstantaneousDynamicExpression extends AbstractExpression {
-    constructor(dynamicExpression: string, soundDynamics: number, placement: PlacementEnum, staffNumber: number) {
+    constructor(dynamicExpression: string, soundDynamics: number, placement: PlacementEnum, staffNumber: number,
+                measure: SourceMeasure) {
         super(placement);
+        super.parentMeasure = measure;
         this.dynamicEnum = DynamicEnum[dynamicExpression.toLowerCase()];
         this.soundDynamic = soundDynamics;
         this.staffNumber = staffNumber;

+ 15 - 13
src/MusicalScore/VoiceData/Expressions/MultiExpression.ts

@@ -7,6 +7,7 @@ import {MoodExpression} from "./MoodExpression";
 import {UnknownExpression} from "./UnknownExpression";
 import {AbstractExpression} from "./AbstractExpression";
 import {PlacementEnum} from "./AbstractExpression";
+import { FontStyles } from "../../../Common/Enums/FontStyles";
 
 export class MultiExpression {
 
@@ -109,19 +110,20 @@ export class MultiExpression {
         }
         return placement;
     }
-    // (*)
-    //public getFontstyleOfFirstEntry(): PSFontStyles {
-    //    let fontStyle: PSFontStyles = PSFontStyles.Regular;
-    //    if (this.expressions.length > 0) {
-    //        if (this.expressions[0].expression instanceof ContinuousDynamicExpression)
-    //            fontStyle = PSFontStyles.Italic;
-    //        else if (this.expressions[0].expression instanceof MoodExpression)
-    //            fontStyle = PSFontStyles.Italic;
-    //        else if (this.expressions[0].expression instanceof UnknownExpression)
-    //            fontStyle = PSFontStyles.Regular;
-    //    }
-    //    return fontStyle;
-    //}
+
+    public getFontstyleOfFirstEntry(): FontStyles {
+       let fontStyle: FontStyles = FontStyles.Regular;
+       if (this.expressions.length > 0) {
+           if (this.expressions[0].expression instanceof ContinuousDynamicExpression) {
+            fontStyle = FontStyles.Italic;
+           } else if (this.expressions[0].expression instanceof MoodExpression) {
+            fontStyle = FontStyles.Italic;
+           } else if (this.expressions[0].expression instanceof UnknownExpression) {
+            fontStyle = FontStyles.Regular;
+           }
+       }
+       return fontStyle;
+    }
     //public getFirstEntry(staffLine: StaffLine, graphLabel: GraphicalLabel): AbstractGraphicalExpression {
     //    let indexOfFirstNotInstDynExpr: number = 0;
     //    if (this.expressions[0].expression instanceof InstantaneousDynamicExpression)

+ 14 - 0
src/MusicalScore/VoiceData/Expressions/MultiTempoExpression.ts

@@ -81,6 +81,10 @@ export class MultiTempoExpression {
     //    return undefined;
     //}
     public addExpression(abstractTempoExpression: AbstractTempoExpression, prefix: string): void {
+        if (this.checkIfAlreadyExists(abstractTempoExpression)) {
+            return;
+        }
+
         if (abstractTempoExpression instanceof InstantaneousTempoExpression) {
             this.instantaneousTempo = <InstantaneousTempoExpression>abstractTempoExpression;
         } else if (abstractTempoExpression instanceof ContinuousTempoExpression) {
@@ -102,6 +106,16 @@ export class MultiTempoExpression {
             return 0;
         }
     }
+
+    private checkIfAlreadyExists(abstractTempoExpression: AbstractTempoExpression ): boolean {
+        for (const entry of this.expressions) {
+            if (entry.label === abstractTempoExpression.Label) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
 
 export class TempoExpressionEntry {

+ 7 - 1
src/MusicalScore/VoiceData/Staff.ts

@@ -19,6 +19,7 @@ export class Staff {
     private voices: Voice[] = [];
     private volume: number = 1;
     private id: number;
+    private stafflineCount: number = 5;
 
     public get ParentInstrument(): Instrument {
         return this.parentInstrument;
@@ -38,5 +39,10 @@ export class Staff {
     public set Volume(value: number) {
         this.volume = value;
     }
-
+    public get StafflineCount(): number {
+        return this.stafflineCount;
+    }
+    public set StafflineCount(value: number) {
+        this.stafflineCount = value;
+    }
 }

+ 45 - 0
src/OpenSheetMusicDisplay/OSMDOptions.ts

@@ -143,6 +143,51 @@ export interface IOSMDOptions {
      *  at different measures. So this option may result in a page break after a single measure on a page.
      */
     newPageFromXML?: boolean;
+    /** The cutoff number for rendering percussion clef stafflines as a single line. Default is 4.
+     *  This is number of instruments specified, e.g. a drumset:
+     *     <score-part id="P1">
+     *       <part-name>Drumset</part-name>
+     *       <part-abbreviation>D. Set</part-abbreviation>
+     *       <score-instrument id="P1-I36">
+     *           <instrument-name>Acoustic Bass Drum</instrument-name>
+     *           </score-instrument>
+     *       <score-instrument id="P1-I37">
+     *           <instrument-name>Bass Drum 1</instrument-name>
+     *           </score-instrument>
+     *       <score-instrument id="P1-I38">
+     *           <instrument-name>Side Stick</instrument-name>
+     *           </score-instrument>
+     *       <score-instrument id="P1-I39">
+     *           <instrument-name>Acoustic Snare</instrument-name>
+     *           </score-instrument>
+     *           ...
+     *   Would still render as 5 stafflines by default, since we have 4 (or greater) instruments in this part.
+     *   While a snare:
+     *   <score-part id="P2">
+     *   <part-name>Concert Snare Drum</part-name>
+     *   <part-abbreviation>Con. Sn.</part-abbreviation>
+     *   <score-instrument id="P2-I38">
+     *       <instrument-name>Side Stick</instrument-name>
+     *       </score-instrument>
+     *   <score-instrument id="P2-I39">
+     *       <instrument-name>Acoustic Snare</instrument-name>
+     *       </score-instrument>
+     *       ...
+     *   Would render with 1 line on the staff, since we only have 2 voices.
+     *   If this value is 0, the feature is turned off.
+     *   If this value is -1, it will render all percussion clefs as a single line.
+     */
+    percussionOneLineCutoff?: number;
+    /** This property is only active if the above property is active (percussionOneLineCutoff)
+     *  This is the cutoff for forcing all voices to the single line, instead of rendering them at different
+     *  positions above/below the line.
+     *  The default is 3, so if a part has less than voices, all of them will be rendered on the line.
+     *  This is for cases like a Concert snare, which has multiple 'instruments' available (snare, side stick)
+     *  should still render only on the line since there is no ambiguity.
+     *  If this value is 0, the feature is turned off.
+     *  IF this value is -1, it will render all percussion clef voices on the single line.
+     */
+    percussionForceVoicesOneLineCutoff?: number;
 }
 
 export enum AlignRestOption {

+ 11 - 3
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -366,7 +366,13 @@ export class OpenSheetMusicDisplay {
                 }
             }
         }
-
+        if (options.percussionOneLineCutoff !== undefined) {
+            this.rules.PercussionOneLineCutoff = options.percussionOneLineCutoff;
+        }
+        if (this.rules.PercussionOneLineCutoff !== 0 &&
+            options.percussionForceVoicesOneLineCutoff !== undefined) {
+            this.rules.PercussionForceVoicesOneLineCutoff = options.percussionForceVoicesOneLineCutoff;
+        }
         if (options.alignRests !== undefined) {
             this.rules.AlignRests = options.alignRests;
         }
@@ -831,7 +837,8 @@ export class OpenSheetMusicDisplay {
         this.drawSkyLine = value;
         if (this.drawer) {
             this.drawer.skyLineVisible = value;
-            this.render();
+            // this.render(); // note: we probably shouldn't automatically render when someone sets the setter
+            //   this can cause a lot of rendering time.
         }
     }
     public get DrawSkyLine(): boolean {
@@ -842,7 +849,8 @@ export class OpenSheetMusicDisplay {
         this.drawBottomLine = value;
         if (this.drawer) {
             this.drawer.bottomLineVisible = value;
-            this.render();
+            // this.render(); // note: we probably shouldn't automatically render when someone sets the setter
+            //   this can cause a lot of rendering time.
         }
     }
     public get DrawBottomLine(): boolean {

+ 106 - 82
test/Util/generateImages_browserless.js

@@ -22,19 +22,28 @@ function sleep (ms) {
     })
 }
 
+// global variables
+//   (without these being global, we'd have to pass many of these values to the generateSampleImage function)
+let [osmdBuildDir, sampleDir, imageDir, pageWidth, pageHeight, filterRegex, mode, debugSleepTimeString] = process.argv.slice(2, 10)
+if (!osmdBuildDir || !sampleDir || !imageDir) {
+    console.log('usage: ' +
+        'node test/Util/generateImages_browserless.js osmdBuildDir sampleDirectory imageDirectory [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]')
+    console.log('  (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)')
+    console.log('example: node test/Util/generateImages_browserless.js ../../build ./test/data/ ./export 210 297 allSmall --debug 5000')
+    console.log('Error: need osmdBuildDir, sampleDir and imageDir. Exiting.')
+    process.exit(1)
+}
+
+if (!mode) {
+    mode = ''
+}
+
+let OSMD // can only be required once window was simulated
+const FS = require('fs')
+
 async function init () {
     console.log('[OSMD.generateImages] init')
 
-    let [osmdBuildDir, sampleDir, imageDir, pageWidth, pageHeight, filterRegex, mode, debugSleepTimeString] = process.argv.slice(2, 10)
-    if (!osmdBuildDir || !sampleDir || !imageDir) {
-        console.log('usage: ' +
-            'node test/Util/generateImages_browserless.js osmdBuildDir sampleDirectory imageDirectory [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]')
-        console.log('  (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)')
-        console.log('example: node test/Util/generateImages_browserless.js ../../build ./test/data/ ./export 210 297 allSmall --debug 5000')
-        console.log('Error: need osmdBuildDir, sampleDir and imageDir. Exiting.')
-        process.exit(1)
-    }
-
     const osmdTestingMode = mode.includes('osmdtesting') // can also be --debugosmdtesting
     const DEBUG = mode.startsWith('--debug')
     // const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
@@ -131,13 +140,13 @@ async function init () {
     debug('div.height: ' + div.height, DEBUG)
     // ---- end browser hacks (hopefully) ----
 
-    const OSMD = require(`${osmdBuildDir}/opensheetmusicdisplay.min.js`)
+    // load globally
+    OSMD = require(`${osmdBuildDir}/opensheetmusicdisplay.min.js`) // window needs to be available before we can require OSMD
 
-    const fs = require('fs')
     // Create the image directory if it doesn't exist.
-    fs.mkdirSync(imageDir, { recursive: true })
+    FS.mkdirSync(imageDir, { recursive: true })
 
-    const sampleDirFilenames = fs.readdirSync(sampleDir)
+    const sampleDirFilenames = FS.readdirSync(sampleDir)
     let samplesToProcess = [] // samples we want to process/generate pngs of, excluding the filtered out files/filenames
     for (const sampleFilename of sampleDirFilenames) {
         if (osmdTestingMode && filterRegex === 'allSmall') {
@@ -201,81 +210,96 @@ async function init () {
         var sampleFilename = samplesToProcess[i]
         debug('sampleFilename: ' + sampleFilename, DEBUG)
 
-        let loadParameter = fs.readFileSync(sampleDir + '/' + sampleFilename)
-        if (sampleFilename.endsWith('.mxl')) {
-            loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter)
-        } else {
-            loadParameter = loadParameter.toString()
+        await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, false)
+
+        if (osmdTestingMode && sampleFilename.startsWith('Beethoven') && sampleFilename.includes('Geliebte')) {
+            // generate one more testing image with skyline and bottomline. (startsWith 'Beethoven' don't catch the function test)
+            await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, true, DEBUG)
         }
-        // console.log('loadParameter: ' + loadParameter)
-        // console.log('typeof loadParameter: ' + typeof loadParameter)
-
-        // set sample-specific options for OSMD visual regression testing
-        if (osmdTestingMode) {
-            const isFunctionTestAutobeam = sampleFilename.startsWith('OSMD_function_test_autobeam')
-            const isFunctionTestAutoColoring = sampleFilename.startsWith('OSMD_function_test_auto-custom-coloring')
-            const isFunctionTestSystemAndPageBreaks = sampleFilename.startsWith('OSMD_Function_Test_System_and_Page_Breaks')
-            const isFunctionTestDrawingRange = sampleFilename.startsWith('OSMD_function_test_measuresToDraw_')
-            osmdInstance.setOptions({
-                autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
-                coloringMode: isFunctionTestAutoColoring ? 2 : 0,
-                coloringSetCustom: isFunctionTestAutoColoring ? ['#d82c6b', '#F89D15', '#FFE21A', '#4dbd5c', '#009D96', '#43469d', '#76429c', '#ff0000'] : undefined,
-                colorStemsLikeNoteheads: isFunctionTestAutoColoring,
-                drawFromMeasureNumber: isFunctionTestDrawingRange ? 9 : 1,
-                drawUpToMeasureNumber: isFunctionTestDrawingRange ? 12 : Number.MAX_SAFE_INTEGER,
-                newSystemFromXML: isFunctionTestSystemAndPageBreaks,
-                newPageFromXML: isFunctionTestSystemAndPageBreaks
-            })
+    }
+
+    console.log('[OSMD.generateImages] done, exiting.')
+}
+
+async function generateSampleImage (sampleFilename, directory, osmdInstance, osmdTestingMode,
+    includeSkyBottomLine = false, DEBUG = false) {
+    var samplePath = directory + '/' + sampleFilename
+    let loadParameter = FS.readFileSync(samplePath)
+
+    if (sampleFilename.endsWith('.mxl')) {
+        loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter)
+    } else {
+        loadParameter = loadParameter.toString()
+    }
+    // console.log('loadParameter: ' + loadParameter)
+    // console.log('typeof loadParameter: ' + typeof loadParameter)
+
+    // set sample-specific options for OSMD visual regression testing
+    if (osmdTestingMode) {
+        const isFunctionTestAutobeam = sampleFilename.startsWith('OSMD_function_test_autobeam')
+        const isFunctionTestAutoColoring = sampleFilename.startsWith('OSMD_function_test_auto-custom-coloring')
+        const isFunctionTestSystemAndPageBreaks = sampleFilename.startsWith('OSMD_Function_Test_System_and_Page_Breaks')
+        const isFunctionTestDrawingRange = sampleFilename.startsWith('OSMD_function_test_measuresToDraw_')
+        osmdInstance.setOptions({
+            autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
+            coloringMode: isFunctionTestAutoColoring ? 2 : 0,
+            coloringSetCustom: isFunctionTestAutoColoring ? ['#d82c6b', '#F89D15', '#FFE21A', '#4dbd5c', '#009D96', '#43469d', '#76429c', '#ff0000'] : undefined,
+            colorStemsLikeNoteheads: isFunctionTestAutoColoring,
+            drawFromMeasureNumber: isFunctionTestDrawingRange ? 9 : 1,
+            drawUpToMeasureNumber: isFunctionTestDrawingRange ? 12 : Number.MAX_SAFE_INTEGER,
+            newSystemFromXML: isFunctionTestSystemAndPageBreaks,
+            newPageFromXML: isFunctionTestSystemAndPageBreaks
+        })
+        osmdInstance.drawSkyLine = includeSkyBottomLine // if includeSkyBottomLine, draw skyline and bottomline, else not
+        osmdInstance.drawBottomLine = includeSkyBottomLine
+    }
+
+    osmdInstance.load(loadParameter).then(function () {
+        debug('xml loaded', DEBUG)
+        try {
+            osmdInstance.render()
+        } catch (ex) {
+            console.log('renderError: ' + ex)
         }
+        debug('rendered', DEBUG)
 
-        await osmdInstance.load(loadParameter).then(function () {
-            debug('xml loaded', DEBUG)
-            try {
-                osmdInstance.render()
-            } catch (ex) {
-                console.log('renderError: ' + ex)
+        const dataUrls = []
+        let canvasImage
+
+        for (let pageNumber = 1; pageNumber < 999; pageNumber++) {
+            canvasImage = document.getElementById('osmdCanvasVexFlowBackendCanvas' + pageNumber)
+            if (!canvasImage) {
+                break
             }
-            debug('rendered', DEBUG)
-
-            const dataUrls = []
-            let canvasImage
-
-            for (let pageNumber = 1; pageNumber < 999; pageNumber++) {
-                canvasImage = document.getElementById('osmdCanvasVexFlowBackendCanvas' + pageNumber)
-                if (!canvasImage) {
-                    break
-                }
-                if (!canvasImage.toDataURL) {
-                    console.log(`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`)
-                    break
-                }
-                dataUrls.push(canvasImage.toDataURL())
+            if (!canvasImage.toDataURL) {
+                console.log(`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`)
+                break
             }
-            for (let urlIndex = 0; urlIndex < dataUrls.length; urlIndex++) {
-                const pageNumberingString = `_${urlIndex + 1}`
-                // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
-                var pageFilename = `${imageDir}/${sampleFilename}${pageNumberingString}.png`
-
-                const dataUrl = dataUrls[urlIndex]
-                if (!dataUrl || !dataUrl.split) {
-                    console.log(`error: could not get dataUrl (imageData) for page ${urlIndex + 1} of sample: ${sampleFilename}`)
-                    continue
-                }
-                const imageData = dataUrl.split(';base64,').pop()
-                const imageBuffer = Buffer.from(imageData, 'base64')
-
-                debug('got image data, saving to: ' + pageFilename, DEBUG)
-                fs.writeFileSync(pageFilename, imageBuffer, { encoding: 'base64' })
+            dataUrls.push(canvasImage.toDataURL())
+        }
+        for (let urlIndex = 0; urlIndex < dataUrls.length; urlIndex++) {
+            const pageNumberingString = `${urlIndex + 1}`
+            const skybottomlineString = includeSkyBottomLine ? 'skybottomline_' : ''
+            // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
+            var pageFilename = `${imageDir}/${sampleFilename}_${skybottomlineString}${pageNumberingString}.png`
+
+            const dataUrl = dataUrls[urlIndex]
+            if (!dataUrl || !dataUrl.split) {
+                console.log(`error: could not get dataUrl (imageData) for page ${urlIndex + 1} of sample: ${sampleFilename}`)
+                continue
             }
-        }) // end render then
-        //     },
-        //     function (e) {
-        //         console.log('error while rendering: ' + e)
-        //     }) // end load then
-        // }) // end read file
-    }
+            const imageData = dataUrl.split(';base64,').pop()
+            const imageBuffer = Buffer.from(imageData, 'base64')
 
-    console.log('[OSMD.generateImages] done, exiting.')
+            debug('got image data, saving to: ' + pageFilename, DEBUG)
+            FS.writeFileSync(pageFilename, imageBuffer, { encoding: 'base64' })
+        }
+    }) // end render then
+    //     },
+    //     function (e) {
+    //         console.log('error while rendering: ' + e)
+    //     }) // end load then
+    // }) // end read file
 }
 
 function debug (msg, debugEnabled) {

+ 516 - 0
test/data/OSMD_Function_Test_Drums_one_line_snare_plus_piano.musicxml

@@ -0,0 +1,516 @@
+<?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 - Drums on one Line</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.4.2</software>
+      <encoding-date>2020-05-21</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7.05556</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1683.78</page-height>
+      <page-width>1190.55</page-width>
+      <page-margins type="even">
+        <left-margin>56.6929</left-margin>
+        <right-margin>56.6929</right-margin>
+        <top-margin>56.6929</top-margin>
+        <bottom-margin>113.386</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>56.6929</left-margin>
+        <right-margin>56.6929</right-margin>
+        <top-margin>56.6929</top-margin>
+        <bottom-margin>113.386</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="FreeSerif" font-size="10"/>
+    <lyric-font font-family="FreeSerif" font-size="11"/>
+    </defaults>
+  <credit page="1">
+    <credit-words default-x="595.275" default-y="1627.09" justify="center" valign="top" font-size="24">Test Drumline</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>
+    <score-part id="P2">
+      <part-name>Concert Snare Drum</part-name>
+      <part-abbreviation>Con. Sn.</part-abbreviation>
+      <score-instrument id="P2-I38">
+        <instrument-name>Side Stick</instrument-name>
+        </score-instrument>
+      <score-instrument id="P2-I39">
+        <instrument-name>Acoustic Snare</instrument-name>
+        </score-instrument>
+      <midi-device port="1"></midi-device>
+      <midi-instrument id="P2-I38">
+        <midi-channel>10</midi-channel>
+        <midi-program>49</midi-program>
+        <midi-unpitched>38</midi-unpitched>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      <midi-instrument id="P2-I39">
+        <midi-channel>10</midi-channel>
+        <midi-program>49</midi-program>
+        <midi-unpitched>39</midi-unpitched>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="344.86">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>228.20</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        <staff-layout number="2">
+          <staff-distance>65.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>2</divisions>
+        <key>
+          <fifths>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="85.50" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="139.76" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="207.60" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>8</duration>
+        </backup>
+      <note default-x="85.50" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>half</type>
+        <staff>2</staff>
+        </note>
+      </measure>
+    <measure number="2" width="249.92">
+      <note default-x="10.00" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <staff>1</staff>
+        </note>
+      <note default-x="129.16" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>8</duration>
+        </backup>
+      <note default-x="10.00" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>half</type>
+        <staff>2</staff>
+        </note>
+      </measure>
+    <measure number="3" width="254.19">
+      <note>
+        <rest/>
+        <duration>8</duration>
+        <voice>1</voice>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>8</duration>
+        </backup>
+      <note default-x="21.87" default-y="-130.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>half</type>
+        <staff>2</staff>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  <part id="P2">
+    <measure number="1" width="344.86">
+      <print>
+        <staff-layout number="1">
+          <staff-distance>65.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>2</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time symbol="common">
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>percussion</sign>
+          <line>2</line>
+          </clef>
+        <staff-details>
+          <staff-lines>1</staff-lines>
+          </staff-details>
+        </attributes>
+      <note default-x="85.50" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>2</duration>
+        <instrument id="P2-I38"/>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <notehead>x</notehead>
+        </note>
+      <note default-x="139.76" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I38"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <notehead>x</notehead>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="173.68" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I38"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <notehead>x</notehead>
+        <beam number="1">end</beam>
+        </note>
+      <note default-x="207.60" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I39"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="241.51" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I39"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="275.43" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I38"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <notehead>x</notehead>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="309.34" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I38"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <notehead>x</notehead>
+        <beam number="1">end</beam>
+        </note>
+      </measure>
+    <measure number="2" width="249.92">
+      <note default-x="10.00" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I38"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <notehead>x</notehead>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        </note>
+      <note default-x="129.16" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I38"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <notehead>x</notehead>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        </note>
+      </measure>
+    <measure number="3" width="254.19">
+      <note default-x="10.00" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I38"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <notehead>x</notehead>
+        </note>
+      <note default-x="21.87" default-y="-210.00">
+        <chord/>
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I39"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      <note default-x="93.39" default-y="-210.00">
+        <unpitched>
+          <display-step>E</display-step>
+          <display-octave>4</display-octave>
+          </unpitched>
+        <duration>1</duration>
+        <instrument id="P2-I39"/>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>half</type>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 0 - 10
test/data/OSMD_function_test_Ornaments.xml

@@ -83,16 +83,6 @@
           <line>2</line>
           </clef>
         </attributes>
-      <direction placement="above">
-        <direction-type>
-          <words default-y="40.00" relative-x="-75.25" relative-y="19.26" font-weight="bold" font-size="12">Ornaments</words>
-          </direction-type>
-        </direction>
-      <direction placement="above">
-        <direction-type>
-          <words default-y="40.00" relative-x="-75.25" relative-y="19.26" font-weight="bold" font-size="12">Ornaments</words>
-          </direction-type>
-        </direction>
       <note default-x="137.53" default-y="-20.00">
         <pitch>
           <step>B</step>

+ 0 - 5
test/data/OSMD_function_test_expressions.musicxml

@@ -98,11 +98,6 @@
         </direction>
       <direction placement="above">
         <direction-type>
-          <words default-y="40.00">Notenzeilentext</words>
-          </direction-type>
-        </direction>
-      <direction placement="above">
-        <direction-type>
           <octave-shift type="down" size="8" number="1" default-y="30.00"/>
           </direction-type>
         </direction>