Просмотр исходного кода

feat(Expressions): Added expressions

Expresions for p,pp, f, ff ,... working. Still need to add to bottom line

#309
Benjamin Giesinger 7 лет назад
Родитель
Сommit
5f7125b73f
27 измененных файлов с 2064 добавлено и 178 удалено
  1. 5 2
      demo/index.js
  2. 23 6
      external/vexflow/vexflow.d.ts
  3. 5 0
      src/Common/DataObjects/Fraction.ts
  4. 3 2
      src/MusicalScore/Graphical/BoundingBox.ts
  5. 1 5
      src/MusicalScore/Graphical/GraphicalInstantaniousDynamicExpression.ts
  6. 1 0
      src/MusicalScore/Graphical/GraphicalLabel.ts
  7. 1 1
      src/MusicalScore/Graphical/GraphicalVoiceEntry.ts
  8. 17 28
      src/MusicalScore/Graphical/MusicSheetDrawer.ts
  9. 39 7
      src/MusicalScore/Graphical/SkyBottomLineCalculator.ts
  10. 10 0
      src/MusicalScore/Graphical/StaffLine.ts
  11. 1 1
      src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts
  12. 6 2
      src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalNote.ts
  13. 63 2
      src/MusicalScore/Graphical/VexFlow/VexFlowInstantaniousDynamicExpression.ts
  14. 14 12
      src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts
  15. 79 15
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts
  16. 22 0
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts
  17. 88 0
      src/MusicalScore/Graphical/VexFlow/VexFlowOctaveShift.ts
  18. 5 7
      src/MusicalScore/Graphical/VexFlow/VexFlowStaffEntry.ts
  19. 20 1
      src/MusicalScore/Graphical/VexFlow/VexFlowVoiceEntry.ts
  20. 70 74
      src/MusicalScore/ScoreIO/InstrumentReader.ts
  21. 580 0
      src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts
  22. 4 10
      src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression.ts
  23. 29 0
      src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/OctaveShift.ts
  24. 2 1
      src/MusicalScore/VoiceData/Expressions/InstantaniousTempoExpression.ts
  25. 2 2
      src/MusicalScore/VoiceData/SourceStaffEntry.ts
  26. 599 0
      test/data/OSMD_function_test_expressions.musicxml
  27. 375 0
      test/data/visual_compare/Expression_Test.musicxml

+ 5 - 2
demo/index.js

@@ -26,6 +26,7 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
             "OSMD Function Test - Grace Notes": "OSMD_function_test_GraceNotes.xml",
             "OSMD Function Test - Ornaments": "OSMD_function_test_Ornaments.xml",
             "OSMD Function Test - Accidentals": "OSMD_function_test_accidentals.musicxml",
+            "OSMD Function Test - Expressions": "OSMD_function_test_expressions.musicxml",
             "Schubert, F. - An Die Musik": "Schubert_An_die_Musik.xml",
             "Actor, L. - Prelude (Sample)": "ActorPreludeSample.xml",
             "Anonymous - Saltarello": "Saltarello.mxl",
@@ -127,8 +128,10 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
                 var width = document.body.clientWidth;
                 canvas.width = width;
                 try {
-                openSheetMusicDisplay.render();
-                } catch (e) {}
+                    openSheetMusicDisplay.render();
+                } catch (e) {
+                    console.warn(e.stack);
+                }
                 enable();
             }
         );

+ 23 - 6
external/vexflow/vexflow.d.ts

@@ -6,14 +6,14 @@ declare namespace Vex {
         const RESOLUTION: any;
 
         export class Formatter {
-            constructor(opts?: any);
+            constructor();
 
             public hasMinTotalWidth: boolean;
             public minTotalWidth: number;
 
             public joinVoices(voices: Voice[]): void;
 
-            public format(voices: Voice[], width: number): void;
+            public format(voices: Voice[], width: number, options?: any): void;
 
             public preCalculateMinTotalWidth(voices: Voice[]): number;
         }
@@ -23,13 +23,13 @@ declare namespace Vex {
 
             public mergeWith(bb: BoundingBox): BoundingBox;
 
-            public getX(): number;
+            public x: number;
 
-            public getY(): number;
+            public y: number;
 
-            public getW(): number;
+            public w: number;
 
-            public getH(): number;
+            public h: number;
 
             public draw(ctx: Vex.Flow.RenderContext): void;
         }
@@ -69,6 +69,23 @@ declare namespace Vex {
         export class Note extends Tickable {
         }
 
+        export class TextBracket {
+            constructor(note_struct: any);
+            
+            public setContext(ctx: RenderContext): TextBracket;
+
+            public draw(): void;
+
+        }
+
+        export class TextNote extends Note {
+            constructor(note_struct: any);
+            
+            public setContext(ctx: RenderContext): TextBracket;
+
+            public draw(): void;
+        }
+
         export class Stem {
             public static UP: number;
             public static DOWN: number;

+ 5 - 0
src/Common/DataObjects/Fraction.ts

@@ -50,6 +50,11 @@ export class Fraction {
     return sum;
   }
 
+    public static multiply (f1: Fraction, f2: Fraction): Fraction {
+        return new Fraction ( (f1.wholeValue * f1.denominator + f1.numerator) * (f2.wholeValue * f2.denominator + f2.numerator),
+                              f1.denominator * f2.denominator);
+    }
+
   private static greatestCommonDenominator(a: number, b: number): number {
     if (a === 0) {
       return b;

+ 3 - 2
src/MusicalScore/Graphical/BoundingBox.ts

@@ -38,11 +38,12 @@ export class BoundingBox {
      * Create a bounding box
      * @param dataObject Graphical object where the bounding box will be attached
      * @param parent Parent bounding box of an object in a higher hierarchy position
-     * @param connectChildToParent Create a child to parent relationship too. Will be true by default
+     * @param isSymbol Defines the bounding box to be symbol thus not calculating it's boundaries by itself. NOTE: Borders need to be set!
      */
-    constructor(dataObject: Object = undefined, parent: BoundingBox = undefined) {
+    constructor(dataObject: Object = undefined, parent: BoundingBox = undefined, isSymbol: boolean = false) {
         this.parent = parent;
         this.dataObject = dataObject;
+        this.isSymbol = isSymbol;
         this.xBordersHaveBeenSet = false;
         this.yBordersHaveBeenSet = false;
         if (parent !== undefined) {

+ 1 - 5
src/MusicalScore/Graphical/GraphicalInstantaniousDynamicExpression.ts

@@ -1,11 +1,7 @@
 import { GraphicalObject } from "./GraphicalObject";
-import { InstantaniousDynamicExpression } from "../VoiceData/Expressions/InstantaniousDynamicExpression";
 
 export class GraphicalInstantaniousDynamicExpression extends GraphicalObject {
-
-    protected instantaniousDynamicExpression: InstantaniousDynamicExpression;
-
-    constructor(instantaniousDynamicExpression: InstantaniousDynamicExpression) {
+    constructor() {
         super();
     }
 }

+ 1 - 0
src/MusicalScore/Graphical/GraphicalLabel.ts

@@ -10,6 +10,7 @@ import {MusicSheetCalculator} from "./MusicSheetCalculator";
  */
 export class GraphicalLabel extends Clickable {
     private label: Label;
+
     constructor(label: Label, textHeight: number, alignment: TextAlignment, parent: BoundingBox = undefined) {
         super();
         this.label = label;

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

@@ -12,7 +12,7 @@ export class GraphicalVoiceEntry extends GraphicalObject {
         super();
         this.parentVoiceEntry = parentVoiceEntry;
         this.parentStaffEntry = parentStaffEntry;
-        this.PositionAndShape = new BoundingBox(this, parentStaffEntry ? parentStaffEntry.PositionAndShape : undefined);
+        this.PositionAndShape = new BoundingBox(this, parentStaffEntry ? parentStaffEntry.PositionAndShape : undefined, true);
         this.notes = [];
     }
 

+ 17 - 28
src/MusicalScore/Graphical/MusicSheetDrawer.ts

@@ -22,7 +22,6 @@ import {MusicSymbol} from "./MusicSymbol";
 import {GraphicalMusicPage} from "./GraphicalMusicPage";
 import {Instrument} from "../Instrument";
 import {MusicSymbolDrawingStyle, PhonicScoreModes} from "./DrawingMode";
-import {GraphicalOctaveShift} from "./GraphicalOctaveShift";
 import {GraphicalObject} from "./GraphicalObject";
 
 /**
@@ -349,6 +348,14 @@ export abstract class MusicSheetDrawer {
             this.drawMeasure(measure);
         }
 
+        if (staffLine.LyricsDashes.length > 0) {
+            this.drawDashes(staffLine.LyricsDashes);
+        }
+
+        this.drawOctaveShifts(staffLine);
+
+        this.drawInstantaniousDynamic(staffLine);
+
         if (this.skyLineVisible) {
             this.drawSkyLine(staffLine);
         }
@@ -397,26 +404,8 @@ export abstract class MusicSheetDrawer {
     //
     // }
 
-    protected drawOctaveShift(staffLine: StaffLine, graphicalOctaveShift: GraphicalOctaveShift): void {
-        this.drawSymbol(graphicalOctaveShift.octaveSymbol, MusicSymbolDrawingStyle.Normal, graphicalOctaveShift.PositionAndShape.AbsolutePosition);
-        const absolutePos: PointF2D = staffLine.PositionAndShape.AbsolutePosition;
-        if (graphicalOctaveShift.dashesStart.x < graphicalOctaveShift.dashesEnd.x) {
-            const horizontalLine: GraphicalLine = new GraphicalLine(graphicalOctaveShift.dashesStart, graphicalOctaveShift.dashesEnd,
-                                                                    this.rules.OctaveShiftLineWidth);
-            this.drawLineAsHorizontalRectangleWithOffset(horizontalLine, absolutePos, <number>GraphicalLayers.Notes);
-        }
-        if (!graphicalOctaveShift.endsOnDifferentStaffLine || graphicalOctaveShift.isSecondPart) {
-            let verticalLine: GraphicalLine;
-            const dashEnd: PointF2D = graphicalOctaveShift.dashesEnd;
-            const octShiftVertLineLength: number = this.rules.OctaveShiftVerticalLineLength;
-            const octShiftLineWidth: number = this.rules.OctaveShiftLineWidth;
-            if (graphicalOctaveShift.octaveSymbol === MusicSymbol.VA8 || graphicalOctaveShift.octaveSymbol === MusicSymbol.MA15) {
-                verticalLine = new GraphicalLine(dashEnd, new PointF2D(dashEnd.x, dashEnd.y + octShiftVertLineLength), octShiftLineWidth);
-            } else {
-                verticalLine = new GraphicalLine(new PointF2D(dashEnd.x, dashEnd.y - octShiftVertLineLength), dashEnd, octShiftLineWidth);
-            }
-            this.drawLineAsVerticalRectangleWithOffset(verticalLine, absolutePos, <number>GraphicalLayers.Notes);
-        }
+    protected drawOctaveShifts(staffLine: StaffLine): void {
+        return;
     }
 
     protected drawStaffLines(staffLine: StaffLine): void {
@@ -436,13 +425,13 @@ export abstract class MusicSheetDrawer {
     //         drawLineAsVerticalRectangle(ending.Right, absolutePosition, <number>GraphicalLayers.Notes);
     //     this.drawLabel(ending.Label, <number>GraphicalLayers.Notes);
     // }
-    // protected drawInstantaniousDynamic(expression: GraphicalInstantaniousDynamicExpression): void {
-    //     expression.ExpressionSymbols.forEach(function (expressionSymbol) {
-    //         let position: PointF2D = expressionSymbol.PositionAndShape.AbsolutePosition;
-    //         let symbol: MusicSymbol = expressionSymbol.GetSymbol;
-    //         drawSymbol(symbol, MusicSymbolDrawingStyle.Normal, position);
-    //     });
-    // }
+    protected drawInstantaniousDynamic(staffline: StaffLine): void {
+        // expression.ExpressionSymbols.forEach(function (expressionSymbol) {
+        //     let position: PointF2D = expressionSymbol.PositionAndShape.AbsolutePosition;
+        //     let symbol: MusicSymbol = expressionSymbol.GetSymbol;
+        //     drawSymbol(symbol, MusicSymbolDrawingStyle.Normal, position);
+        // });
+    }
     // protected drawContinuousDynamic(expression: GraphicalContinuousDynamicExpression,
     //     absolute: PointF2D): void {
     //     throw new Error("not implemented");

+ 39 - 7
src/MusicalScore/Graphical/SkyBottomLineCalculator.ts

@@ -1,10 +1,11 @@
-import {EngravingRules} from "./EngravingRules";
-import {StaffLine} from "./StaffLine";
-import {PointF2D} from "../../Common/DataObjects/PointF2D";
-import {CanvasVexFlowBackend} from "./VexFlow/CanvasVexFlowBackend";
-import {VexFlowMeasure} from "./VexFlow/VexFlowMeasure";
-import {unitInPixels} from "./VexFlow/VexFlowMusicSheetDrawer";
+import { EngravingRules } from "./EngravingRules";
+import { StaffLine } from "./StaffLine";
+import { PointF2D } from "../../Common/DataObjects/PointF2D";
+import { CanvasVexFlowBackend } from "./VexFlow/CanvasVexFlowBackend";
+import { VexFlowMeasure } from "./VexFlow/VexFlowMeasure";
+import { unitInPixels } from "./VexFlow/VexFlowMusicSheetDrawer";
 import * as log from "loglevel";
+import { BoundingBox } from "./BoundingBox";
 /**
  * This class calculates and holds the skyline and bottom line information.
  * It also has functions to update areas of the two lines if new elements are
@@ -147,7 +148,7 @@ export class SkyBottomLineCalculator {
         const ctx: any = backend.getContext();
         const oldStyle: string = ctx.fillStyle;
         ctx.fillStyle = color;
-        ctx.fillRect( coord.x, coord.y, 2, 2 );
+        ctx.fillRect(coord.x, coord.y, 2, 2);
         ctx.fillStyle = oldStyle;
     }
 
@@ -366,6 +367,37 @@ export class SkyBottomLineCalculator {
         return this.getMaxInRange(this.BottomLine, startIndex, endIndex);
     }
 
+    /**
+     * This method finds the BottomLine's maximum value within a given range.
+     * @param staffLine Staffline to find the max value in
+     * @param startIndex Start index of the range
+     * @param endIndex End index of the range
+     */
+
+
+    /**
+     * This method returns the maximum value of the bottom line around a specific
+     * bounding box. Will return undefined if the bounding box is not valid or inside staffline
+     * @param boundingBox Bounding box where the maximum should be retrieved from
+     * @returns Maximum value inside bounding box boundaries or undefined if not possible
+     */
+    public getBottomLineMaxInBoundingBox(boundingBox: BoundingBox): number {
+        // FIXME: See if this really works as expected!
+        let iteratorBB: BoundingBox = boundingBox;
+        let startIndex: number = boundingBox.BorderLeft;
+        let endIndex: number = boundingBox.BorderRight;
+        let successfull: boolean = false;
+        while (iteratorBB.Parent) {
+            if (iteratorBB === this.mStaffLineParent.PositionAndShape) {
+                successfull = true;
+                break;
+            }
+            startIndex += iteratorBB.BorderLeft;
+            endIndex += iteratorBB.BorderRight;
+            iteratorBB = iteratorBB.Parent;
+        }
+        return successfull ? this.getMaxInRange(this.BottomLine, startIndex, endIndex) : undefined;
+    }
 
     //#region Private methods
 

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

@@ -10,6 +10,7 @@ import {StaffLineActivitySymbol} from "./StaffLineActivitySymbol";
 import {PointF2D} from "../../Common/DataObjects/PointF2D";
 import {GraphicalLabel} from "./GraphicalLabel";
 import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
+import { GraphicalOctaveShift } from "./GraphicalOctaveShift";
 
 /**
  * A StaffLine contains the [[Measure]]s in one line of the music sheet
@@ -20,6 +21,7 @@ export abstract class StaffLine extends GraphicalObject {
     protected staffLines: GraphicalLine[] = new Array(5);
     protected parentMusicSystem: MusicSystem;
     protected parentStaff: Staff;
+    protected octaveShifts: GraphicalOctaveShift[] = [];
     protected skyBottomLine: SkyBottomLineCalculator;
     protected lyricLines: GraphicalLine[] = [];
     protected lyricsDashes: GraphicalLabel[] = [];
@@ -97,6 +99,14 @@ export abstract class StaffLine extends GraphicalObject {
         return this.skyBottomLine.BottomLine;
     }
 
+    public get OctaveShifts(): GraphicalOctaveShift[] {
+        return this.octaveShifts;
+    }
+
+    public set OctaveShifts(value: GraphicalOctaveShift[]) {
+        this.octaveShifts = value;
+    }
+
     public addActivitySymbolClickArea(): void {
         const activitySymbol: StaffLineActivitySymbol = new StaffLineActivitySymbol(this);
         const staffLinePsi: BoundingBox = this.PositionAndShape;

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

@@ -105,7 +105,7 @@ export class VexFlowConverter {
      */
     public static pitch(pitch: Pitch, clef: ClefInstruction): [string, string, ClefInstruction] {
         const fund: string = NoteEnum[pitch.FundamentalNote].toLowerCase();
-        // The octave seems to need a shift of three FIXME?
+        // FIXME: The octave seems to need a shift of three?
         const octave: number = pitch.Octave - clef.OctaveOffset + 3;
         const acc: string = VexFlowConverter.accidental(pitch.Accidental);
         return [fund + "n/" + octave, acc, clef];

+ 6 - 2
src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalNote.ts

@@ -5,7 +5,7 @@ import {ClefInstruction} from "../../VoiceData/Instructions/ClefInstruction";
 import {VexFlowConverter} from "./VexFlowConverter";
 import {Pitch} from "../../../Common/DataObjects/Pitch";
 import {Fraction} from "../../../Common/DataObjects/Fraction";
-import {OctaveEnum} from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
+import {OctaveEnum, OctaveShift} from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
 import { GraphicalVoiceEntry } from "../GraphicalVoiceEntry";
 
 /**
@@ -16,12 +16,16 @@ export class VexFlowGraphicalNote extends GraphicalNote {
                 octaveShift: OctaveEnum = OctaveEnum.NONE,  graphicalNoteLength: Fraction = undefined) {
         super(note, parent, graphicalNoteLength);
         this.clef = activeClef;
+        this.octaveShift = octaveShift;
         if (note.Pitch) {
-            this.vfpitch = VexFlowConverter.pitch(note.Pitch, this.clef);
+            // TODO: Maybe shift to Transpose function when available
+            const drawPitch: Pitch = OctaveShift.getPitchFromOctaveShift(note.Pitch, octaveShift);
+            this.vfpitch = VexFlowConverter.pitch(drawPitch, this.clef);
             this.vfpitch[1] = undefined;
         }
     }
 
+    public octaveShift: OctaveEnum;
     // The pitch of this note as given by VexFlowConverter.pitch
     public vfpitch: [string, string, ClefInstruction];
     // The corresponding VexFlow StaveNote (plus its index in the chord)

+ 63 - 2
src/MusicalScore/Graphical/VexFlow/VexFlowInstantaniousDynamicExpression.ts

@@ -1,10 +1,71 @@
 import { GraphicalInstantaniousDynamicExpression } from "../GraphicalInstantaniousDynamicExpression";
-import { InstantaniousDynamicExpression } from "../../VoiceData/Expressions/InstantaniousDynamicExpression";
+import { InstantaniousDynamicExpression, DynamicEnum } from "../../VoiceData/Expressions/InstantaniousDynamicExpression";
 import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
+import { GraphicalLabel } from "../GraphicalLabel";
+import { Label } from "../../Label";
+import { TextAlignment } from "../../../Common/Enums/TextAlignment";
+import { EngravingRules } from "../EngravingRules";
+import { FontStyles } from "../../../Common/Enums/FontStyles";
+import { SkyBottomLineCalculator } from "../SkyBottomLineCalculator";
+import { StaffLine } from "../StaffLine";
+import { GraphicalMeasure } from "../GraphicalMeasure";
+import { MusicSystem } from "../MusicSystem";
 
 export class VexFlowInstantaniousDynamicExpression extends GraphicalInstantaniousDynamicExpression {
+    private mInstantaniousDynamicExpression: InstantaniousDynamicExpression;
+    private mLabel: GraphicalLabel;
 
     constructor(instantaniousDynamicExpression: InstantaniousDynamicExpression, staffEntry: GraphicalStaffEntry) {
-        super(instantaniousDynamicExpression);
+        super();
+        this.mInstantaniousDynamicExpression = instantaniousDynamicExpression;
+        this.mLabel = new GraphicalLabel(new Label(this.Expression),
+                                         EngravingRules.Rules.ContinuousDynamicTextHeight,
+                                         TextAlignment.LeftTop,
+                                         staffEntry ? staffEntry.PositionAndShape : undefined);
+
+        const offset: number = staffEntry ? staffEntry.parentMeasure.ParentStaffLine
+                                       .SkyBottomLineCalculator.getBottomLineMaxInBoundingBox(staffEntry.parentMeasure.PositionAndShape) : 0;
+        this.mLabel.PositionAndShape.RelativePosition.y += offset;
+        this.mLabel.Label.fontStyle = FontStyles.BoldItalic;
+        this.mLabel.setLabelPositionAndShapeBorders();
+    }
+
+    public calculcateBottomLine(measure: GraphicalMeasure): void {
+        const skyBottomLineCalculator: SkyBottomLineCalculator = measure.ParentStaffLine.SkyBottomLineCalculator;
+        const staffLine: StaffLine = measure.ParentStaffLine;
+        const musicSystem: MusicSystem = measure.parentMusicSystem;
+
+        // calculate LabelBoundingBox and set PSI parent
+        this.mLabel.setLabelPositionAndShapeBorders();
+        this.mLabel.PositionAndShape.Parent = musicSystem.PositionAndShape;
+
+        // calculate relative Position
+        const relativeX: number = staffLine.PositionAndShape.RelativePosition.x +
+        measure.PositionAndShape.RelativePosition.x - this.mLabel.PositionAndShape.BorderMarginLeft;
+        let relativeY: number;
+
+        // and the corresponding SkyLine indeces
+        let start: number = relativeX;
+        let end: number = relativeX - this.mLabel.PositionAndShape.BorderLeft + this.mLabel.PositionAndShape.BorderMarginRight;
+
+          // take into account the InstrumentNameLabel's at the beginning of the first MusicSystem
+        if (staffLine === musicSystem.StaffLines[0] && musicSystem === musicSystem.Parent.MusicSystems[0]) {
+              start -= staffLine.PositionAndShape.RelativePosition.x;
+              end -= staffLine.PositionAndShape.RelativePosition.x;
+          }
+
+          // get the minimum corresponding SkyLine value
+        const bottomLineMaxValue: number = skyBottomLineCalculator.getBottomLineMaxInRange(start, end);
+        relativeY = bottomLineMaxValue;
+        // console.log(start, end, relativeY, this.mLabel.PositionAndShape.BorderMarginBottom)
+        skyBottomLineCalculator.updateBottomLineInRange(start, end, relativeY + this.mLabel.PositionAndShape.BorderMarginBottom);
+    }
+
+    get Expression(): string {
+        return DynamicEnum[this.mInstantaniousDynamicExpression.DynEnum];
+    }
+
+    get Label(): GraphicalLabel {
+        return this.mLabel;
     }
 }

+ 14 - 12
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -26,6 +26,7 @@ import {GraphicalVoiceEntry} from "../GraphicalVoiceEntry";
 import {VexFlowVoiceEntry} from "./VexFlowVoiceEntry";
 import {Fraction} from "../../../Common/DataObjects/Fraction";
 import { Voice } from "../../VoiceData/Voice";
+import { VexFlowInstantaniousDynamicExpression } from "./VexFlowInstantaniousDynamicExpression";
 import { LinkedVoice } from "../../VoiceData/LinkedVoice";
 
 export class VexFlowMeasure extends GraphicalMeasure {
@@ -35,28 +36,29 @@ export class VexFlowMeasure extends GraphicalMeasure {
         this.resetLayout();
     }
 
-    // octaveOffset according to active clef
+    /** octaveOffset according to active clef */
     public octaveOffset: number = 3;
-    // The VexFlow Voices in the measure
+    /** The VexFlow Voices in the measure */
     public vfVoices: { [voiceID: number]: Vex.Flow.Voice; } = {};
-    // Call this function (if present) to x-format all the voices in the measure
+    /** Call this function (if present) to x-format all the voices in the measure */
     public formatVoices: (width: number) => void;
-    // The VexFlow Ties in the measure
+    /** The VexFlow Ties in the measure */
     public vfTies: Vex.Flow.StaveTie[] = [];
-    // The repetition instructions given as words or symbols (coda, dal segno..)
+    /** The repetition instructions given as words or symbols (coda, dal segno..) */
     public vfRepetitionWords: Vex.Flow.Repetition[] = [];
-
-    // The VexFlow Stave (= one measure in a staffline)
+    /** Instant dynamics */
+    public instantaniousDynamics: VexFlowInstantaniousDynamicExpression[] = [];
+    /** The VexFlow Stave (= one measure in a staffline) */
     private stave: Vex.Flow.Stave;
-    // VexFlow StaveConnectors (vertical lines)
+    /** VexFlow StaveConnectors (vertical lines) */
     private connectors: Vex.Flow.StaveConnector[] = [];
-    // Intermediate object to construct beams
+    /** Intermediate object to construct beams */
     private beams: { [voiceID: number]: [Beam, VexFlowVoiceEntry[]][]; } = {};
-    // VexFlow Beams
+    /** VexFlow Beams */
     private vfbeams: { [voiceID: number]: Vex.Flow.Beam[]; };
-    // Intermediate object to construct tuplets
+    /** Intermediate object to construct tuplets */
     private tuplets: { [voiceID: number]: [Tuplet, VexFlowVoiceEntry[]][]; } = {};
-    // VexFlow Tuplets
+    /** VexFlow Tuplets */
     private vftuplets: { [voiceID: number]: Vex.Flow.Tuplet[]; } = {};
 
     // Sets the absolute coordinates of the VFStave on the canvas

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

@@ -12,7 +12,7 @@ import {MultiExpression} from "../../VoiceData/Expressions/MultiExpression";
 import {RepetitionInstruction} from "../../VoiceData/Instructions/RepetitionInstruction";
 import {Beam} from "../../VoiceData/Beam";
 import {ClefInstruction} from "../../VoiceData/Instructions/ClefInstruction";
-import {OctaveEnum} from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
+import {OctaveEnum, OctaveShift} from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
 import {Fraction} from "../../../Common/DataObjects/Fraction";
 import {LyricWord} from "../../VoiceData/Lyrics/LyricsWord";
 import {OrnamentContainer} from "../../VoiceData/OrnamentContainer";
@@ -30,6 +30,8 @@ import {GraphicalLabel} from "../GraphicalLabel";
 import {LyricsEntry} from "../../VoiceData/Lyrics/LyricsEntry";
 import {GraphicalLyricWord} from "../GraphicalLyricWord";
 import {VexFlowStaffEntry} from "./VexFlowStaffEntry";
+import { VexFlowOctaveShift } from "./VexFlowOctaveShift";
+import { VexFlowInstantaniousDynamicExpression } from "./VexFlowInstantaniousDynamicExpression";
 import {BoundingBox} from "../BoundingBox";
 import { EngravingRules } from "../EngravingRules";
 
@@ -55,8 +57,10 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         const firstMeasure: VexFlowMeasure = verticalMeasureList[0] as VexFlowMeasure;
         // first measure has formatting method as lambda function object, but formats all measures. TODO this could be refactored
         firstMeasure.format();
-        for (const staffEntry of firstMeasure.staffEntries) {
-          (<VexFlowStaffEntry>staffEntry).calculateXPosition();
+        for (const measure of verticalMeasureList) {
+          for (const staffEntry of measure.staffEntries) {
+            (<VexFlowStaffEntry>staffEntry).calculateXPosition();
+          }
         }
       }
   }
@@ -86,8 +90,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
      }*/
     // Format the voices
     const allVoices: Vex.Flow.Voice[] = [];
-    const formatter: Vex.Flow.Formatter = new Vex.Flow.Formatter({align_rests: true,
-    });
+    const formatter: Vex.Flow.Formatter = new Vex.Flow.Formatter();
 
     for (const measure of measures) {
         const mvoices:  { [voiceID: number]: Vex.Flow.Voice; } = (measure as VexFlowMeasure).vfVoices;
@@ -116,12 +119,13 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         // };
         MusicSheetCalculator.setMeasuresMinStaffEntriesWidth(measures, minStaffEntriesWidth);
         for (const measure of measures) {
-          measure.PositionAndShape.BorderRight = minStaffEntriesWidth;
           if (measure === measures[0]) {
             const vexflowMeasure: VexFlowMeasure = (measure as VexFlowMeasure);
             // prepare format function for voices, will be called later for formatting measure again
             vexflowMeasure.formatVoices = (w: number) => {
-              formatter.format(allVoices, w);
+              formatter.format(allVoices, w, {
+                align_rests: true,
+          });
             };
             // format now for minimum width
             vexflowMeasure.formatVoices(minStaffEntriesWidth * unitInPixels);
@@ -344,15 +348,75 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
   }
 
-    /**
-     * Calculate a single OctaveShift for a [[MultiExpression]].
-     * @param sourceMeasure
-     * @param multiExpression
-     * @param measureIndex
-     * @param staffIndex
-     */
+  protected calculateDynamicExpressionsForSingleMultiExpression(multiExpression: MultiExpression, measureIndex: number, staffIndex: number): void {
+
+    if (multiExpression.InstantaniousDynamic) {
+        const timeStamp: Fraction = multiExpression.Timestamp;
+        const measure: GraphicalMeasure = this.graphicalMusicSheet.MeasureList[measureIndex][staffIndex];
+        const startStaffEntry: GraphicalStaffEntry = measure.findGraphicalStaffEntryFromTimestamp(timeStamp);
+        const idx: VexFlowInstantaniousDynamicExpression = new VexFlowInstantaniousDynamicExpression(multiExpression.InstantaniousDynamic, startStaffEntry);
+        // idx.calculcateBottomLine(measure);
+        (measure as VexFlowMeasure).instantaniousDynamics.push(idx);
+    }
+  }
+
+  /**
+   * Calculate a single OctaveShift for a [[MultiExpression]].
+   * @param sourceMeasure
+   * @param multiExpression
+   * @param measureIndex
+   * @param staffIndex
+   */
   protected calculateSingleOctaveShift(sourceMeasure: SourceMeasure, multiExpression: MultiExpression, measureIndex: number, staffIndex: number): void {
-    return;
+    // calculate absolute Timestamp and startStaffLine (and EndStaffLine if needed)
+    const octaveShift: OctaveShift = multiExpression.OctaveShiftStart;
+
+    const startTimeStamp: Fraction = octaveShift.ParentStartMultiExpression.Timestamp;
+    const endTimeStamp: Fraction = octaveShift.ParentEndMultiExpression.Timestamp;
+
+    const startStaffLine: StaffLine = this.graphicalMusicSheet.MeasureList[measureIndex][staffIndex].ParentStaffLine;
+
+    let endMeasure: GraphicalMeasure = undefined;
+    if (octaveShift.ParentEndMultiExpression !== undefined) {
+        endMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(octaveShift.ParentEndMultiExpression.SourceMeasureParent,
+                                                                                           staffIndex);
+    }
+    let startMeasure: GraphicalMeasure = undefined;
+    if (octaveShift.ParentEndMultiExpression !== undefined) {
+      startMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(octaveShift.ParentStartMultiExpression.SourceMeasureParent,
+                                                                                           staffIndex);
+    }
+
+    if (endMeasure !== undefined) {
+        // calculate GraphicalOctaveShift and RelativePositions
+        const graphicalOctaveShift: VexFlowOctaveShift = new VexFlowOctaveShift(octaveShift, startStaffLine.PositionAndShape);
+        startStaffLine.OctaveShifts.push(graphicalOctaveShift);
+
+        // calculate RelativePosition and Dashes
+        const startStaffEntry: GraphicalStaffEntry = startMeasure.findGraphicalStaffEntryFromTimestamp(startTimeStamp);
+        const endStaffEntry: GraphicalStaffEntry = endMeasure.findGraphicalStaffEntryFromTimestamp(endTimeStamp);
+
+        graphicalOctaveShift.setStartNote(startStaffEntry);
+
+        if (endMeasure.ParentStaffLine !== startMeasure.ParentStaffLine) {
+          graphicalOctaveShift.endsOnDifferentStaffLine = true;
+          const lastMeasure: GraphicalMeasure = startMeasure.ParentStaffLine.Measures[startMeasure.ParentStaffLine.Measures.length - 1];
+          const lastNote: GraphicalStaffEntry = lastMeasure.staffEntries[lastMeasure.staffEntries.length - 1];
+          graphicalOctaveShift.setEndNote(lastNote);
+
+          // Now finish the shift on the next line
+          const remainingOctaveShift: VexFlowOctaveShift = new VexFlowOctaveShift(octaveShift, endMeasure.PositionAndShape);
+          endMeasure.ParentStaffLine.OctaveShifts.push(remainingOctaveShift);
+          const firstMeasure: GraphicalMeasure = endMeasure.ParentStaffLine.Measures[0];
+          const firstNote: GraphicalStaffEntry = firstMeasure.staffEntries[0];
+          remainingOctaveShift.setStartNote(firstNote);
+          remainingOctaveShift.setEndNote(endStaffEntry);
+        } else {
+          graphicalOctaveShift.setEndNote(endStaffEntry);
+        }
+    } else {
+      log.warn("End measure for octave shift is undefined! This should not happen!");
+    }
   }
 
     /**

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

@@ -1,3 +1,4 @@
+import Vex = require("vexflow");
 import {MusicSheetDrawer} from "../MusicSheetDrawer";
 import {RectangleF2D} from "../../../Common/DataObjects/RectangleF2D";
 import {VexFlowMeasure} from "./VexFlowMeasure";
@@ -9,6 +10,8 @@ import {GraphicalObject} from "../GraphicalObject";
 import {GraphicalLayers} from "../DrawingEnums";
 import {GraphicalStaffEntry} from "../GraphicalStaffEntry";
 import {VexFlowBackend} from "./VexFlowBackend";
+import {VexFlowOctaveShift} from "./VexFlowOctaveShift";
+import {VexFlowInstantaniousDynamicExpression} from "./VexFlowInstantaniousDynamicExpression";
 import {VexFlowInstrumentBracket} from "./VexFlowInstrumentBracket";
 import {VexFlowInstrumentBrace} from "./VexFlowInstrumentBrace";
 import {GraphicalLyricEntry} from "../GraphicalLyricEntry";
@@ -205,6 +208,25 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
         vexBrace.draw(this.backend.getContext());
     }
 
+    protected drawOctaveShifts(staffLine: StaffLine): void {
+        for (const graphicalOctaveShift of staffLine.OctaveShifts) {
+            if (graphicalOctaveShift) {
+                const ctx: Vex.Flow.RenderContext = this.backend.getContext();
+                const textBracket: Vex.Flow.TextBracket = (graphicalOctaveShift as VexFlowOctaveShift).getTextBracket();
+                textBracket.setContext(ctx);
+                textBracket.draw();
+            }
+        }
+    }
+
+    protected drawInstantaniousDynamic(staffline: StaffLine): void {
+        for (const m of staffline.Measures as VexFlowMeasure[]) {
+            for (const idx of m.instantaniousDynamics as VexFlowInstantaniousDynamicExpression[]) {
+                this.drawLabel(idx.Label, <number>GraphicalLayers.Notes);
+            }
+        }
+    }
+
     /**
      * Renders a Label to the screen (e.g. Title, composer..)
      * @param graphicalLabel holds the label string, the text height in units and the font parameters

+ 88 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowOctaveShift.ts

@@ -0,0 +1,88 @@
+import Vex = require("vexflow");
+import { GraphicalOctaveShift } from "../GraphicalOctaveShift";
+import { OctaveShift, OctaveEnum } from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
+import { BoundingBox } from "../BoundingBox";
+import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
+import { VexFlowVoiceEntry } from "./VexFlowVoiceEntry";
+import * as log from "loglevel";
+
+/**
+ * The vexflow adaptation of a graphical shift.
+ */
+export class VexFlowOctaveShift extends GraphicalOctaveShift {
+
+    /** Defines the note where the octave shift starts */
+    private startNote: Vex.Flow.StemmableNote;
+    /** Defines the note where the octave shift ends */
+    private endNote: Vex.Flow.StemmableNote;
+    /** Top or bottom of the staffline */
+    private position: string;
+    /** Supscript is a smaller text after the regular text (e.g. va after 8) */
+    private supscript: string;
+    /** Main text element */
+    private text: string;
+
+    /**
+     * Create a new vexflow ocatve shift
+     * @param octaveShift the object read by the ExpressionReader
+     * @param parent the bounding box of the parent
+     */
+    constructor(octaveShift: OctaveShift, parent: BoundingBox) {
+        super(octaveShift, parent);
+        switch (octaveShift.Type) {
+            case OctaveEnum.VA8:
+                this.position = "top";
+                this.supscript = "va";
+                this.text = "8";
+                break;
+                case OctaveEnum.MA15:
+                this.position = "top";
+                this.supscript = "ma";
+                this.text = "15";
+                break;
+                case OctaveEnum.VB8:
+                this.position = "bottom";
+                this.supscript = "vb";
+                this.text = "8";
+                break;
+                case OctaveEnum.MB15:
+                this.position = "bottom";
+                this.supscript = "mb";
+                this.text = "15";
+                break;
+            default:
+                log.error("Unknown or NONE octaveshift. This should not be called!");
+                break;
+        }
+    }
+
+    /**
+     * Set a start note using a staff entry
+     * @param graphicalStaffEntry the staff entry that holds the start note
+     */
+    public setStartNote(graphicalStaffEntry: GraphicalStaffEntry): void {
+        this.startNote = (graphicalStaffEntry.graphicalVoiceEntries[0] as VexFlowVoiceEntry).vfStaveNote;
+    }
+
+    /**
+     * Set an end note using a staff entry
+     * @param graphicalStaffEntry the staff entry that holds the end note
+     */
+    public setEndNote(graphicalStaffEntry: GraphicalStaffEntry): void {
+        this.endNote = (graphicalStaffEntry.graphicalVoiceEntries[0] as VexFlowVoiceEntry).vfStaveNote;
+    }
+
+    /**
+     * Get the actual vexflow text bracket used for drawing
+     */
+    public getTextBracket(): Vex.Flow.TextBracket {
+        return new Vex.Flow.TextBracket({
+            position: this.position,
+            start: this.startNote,
+            stop: this.endNote,
+            superscript: this.supscript,
+            text: this.text,
+        });
+    }
+
+}

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

@@ -29,6 +29,8 @@ export class VexFlowStaffEntry extends GraphicalStaffEntry {
             const tickable: Vex.Flow.StemmableNote = (gve as VexFlowVoiceEntry).vfStaveNote;
             // This will let the tickable know how to calculate it's bounding box
             tickable.setStave(stave);
+            // setting Borders from Vexflow to OSMD
+            (gve as VexFlowVoiceEntry).applyBordersFromVexflow();
             // The middle of the tickable is also the OSMD BoundingBox center
             if (tickable.getAttribute("type") === "StaveNote") {
                 // The middle of the tickable is also the OSMD BoundingBox center
@@ -42,15 +44,11 @@ export class VexFlowStaffEntry extends GraphicalStaffEntry {
             numberOfValidTickables++;
         }
         tickablePosition = tickablePosition / numberOfValidTickables;
-        // Calculate parent absolute position and reverse calculate the relative position
-        // All the modifiers signs, clefs, you name it have an offset in the measure. Therefore remove it.
-        // NOTE: Somehow vexflows shift is off by 25px.
-        const modifierOffset: number = stave.getModifierXShift(); // - (this.parentMeasure.MeasureNumber === 1 ? 25 : 0);
-        // const modifierOffset: number = 0;
         // sets the vexflow x positions back into the bounding boxes of the staff entries in the osmd object model.
         // The positions are needed for cursor placement and mouse/tap interactions
-        this.PositionAndShape.RelativePosition.x = (tickablePosition - stave.getNoteStartX() + modifierOffset) / unitInPixels
-            + 0.5; // half note head offset
+        if (!(this.graphicalVoiceEntries[0] as VexFlowVoiceEntry).parentVoiceEntry.IsGrace) {
+            this.PositionAndShape.RelativePosition.x = (this.graphicalVoiceEntries[0] as VexFlowVoiceEntry).vfStaveNote.getBoundingBox().x / unitInPixels;
+        }
         this.PositionAndShape.calculateBoundingBox();
     }
 }

+ 20 - 1
src/MusicalScore/Graphical/VexFlow/VexFlowVoiceEntry.ts

@@ -1,11 +1,30 @@
 import { VoiceEntry } from "../../VoiceData/VoiceEntry";
 import { GraphicalVoiceEntry } from "../GraphicalVoiceEntry";
 import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
+import { unitInPixels } from "./VexFlowMusicSheetDrawer";
 
 export class VexFlowVoiceEntry extends GraphicalVoiceEntry {
+    private mVexFlowStaveNote: Vex.Flow.StemmableNote;
+
     constructor(parentVoiceEntry: VoiceEntry, parentStaffEntry: GraphicalStaffEntry) {
         super(parentVoiceEntry, parentStaffEntry);
     }
 
-    public vfStaveNote: Vex.Flow.StemmableNote;
+    public applyBordersFromVexflow(): void {
+        const a: any = (this.vfStaveNote as any);
+        const bb: any = a.getBoundingBox();
+        this.PositionAndShape.RelativePosition.y = bb.y / unitInPixels;
+        this.PositionAndShape.BorderTop = 0;
+        this.PositionAndShape.BorderBottom = bb.h / unitInPixels;
+        this.PositionAndShape.BorderLeft = bb.x / unitInPixels;
+        this.PositionAndShape.BorderRight = bb.w / unitInPixels;
+    }
+
+    public set vfStaveNote(value: Vex.Flow.StemmableNote) {
+        this.mVexFlowStaveNote = value;
+    }
+
+    public get vfStaveNote(): Vex.Flow.StemmableNote {
+        return this.mVexFlowStaveNote;
+    }
 }

+ 70 - 74
src/MusicalScore/ScoreIO/InstrumentReader.ts

@@ -20,16 +20,16 @@ import {ChordSymbolContainer} from "../VoiceData/ChordSymbolContainer";
 import * as log from "loglevel";
 import {MidiInstrument} from "../VoiceData/Instructions/ClefInstruction";
 import {ChordSymbolReader} from "./MusicSymbolModules/ChordSymbolReader";
+import {ExpressionReader} from "./MusicSymbolModules/ExpressionReader";
 import { RepetitionInstructionReader } from "./MusicSymbolModules/RepetitionInstructionReader";
 //import Dictionary from "typescript-collections/dist/lib/Dictionary";
 
 // FIXME: The following classes are missing
 //type ChordSymbolContainer = any;
 //type SlurReader = any;
-//type ExpressionReader = any;
+//type RepetitionInstructionReader = any;
 //declare class MusicSymbolModuleFactory {
 //  public static createSlurReader(x: any): any;
-//  public static createExpressionGenerator(musicSheet: MusicSheet, instrument: Instrument, n: number);
 //}
 //
 //class MetronomeReader {
@@ -60,7 +60,7 @@ export class InstrumentReader {
       for (let i: number = 0; i < instrument.Staves.length; i++) {
         this.activeClefsHaveBeenInitialized[i] = false;
       }
-      // FIXME createExpressionGenerators(instrument.Staves.length);
+      this.createExpressionGenerators(instrument.Staves.length);
       // (*) this.slurReader = MusicSymbolModuleFactory.createSlurReader(this.musicSheet);
   }
 
@@ -85,7 +85,7 @@ export class InstrumentReader {
   private activeKeyHasBeenInitialized: boolean = false;
   private abstractInstructions: [number, AbstractNotationInstruction][] = [];
   private openChordSymbolContainer: ChordSymbolContainer;
-  // (*) private expressionReaders: ExpressionReader[];
+  private expressionReaders: ExpressionReader[];
   private currentVoiceGenerator: VoiceGenerator;
   //private openSlurDict: { [n: number]: Slur; } = {};
   private maxTieNoteFraction: Fraction;
@@ -263,15 +263,15 @@ export class InstrumentReader {
 
           const notationsNode: IXmlElement = xmlNode.element("notations");
           if (notationsNode !== undefined && notationsNode.element("dynamics") !== undefined) {
-            // (*) let expressionReader: ExpressionReader = this.expressionReaders[this.readExpressionStaffNumber(xmlNode) - 1];
-            //if (expressionReader !== undefined) {
-            //  expressionReader.readExpressionParameters(
-            //    xmlNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, false
-            //  );
-            //  expressionReader.read(
-            //    xmlNode, this.currentMeasure, previousFraction
-            //  );
-            //}
+            const expressionReader: ExpressionReader = this.expressionReaders[this.readExpressionStaffNumber(xmlNode) - 1];
+            if (expressionReader !== undefined) {
+             expressionReader.readExpressionParameters(
+               xmlNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, false
+             );
+             expressionReader.read(
+               xmlNode, this.currentMeasure, previousFraction
+             );
+            }
           }
           lastNoteWasGrace = isGraceNote;
         } else if (xmlNode.name === "attributes") {
@@ -328,39 +328,36 @@ export class InstrumentReader {
             previousFraction = new Fraction(0, 1);
           }
         } else if (xmlNode.name === "direction") {
-          // unused let directionTypeNode: IXmlElement = xmlNode.element("direction-type");
-          // (*) MetronomeReader.readMetronomeInstructions(xmlNode, this.musicSheet, this.currentXmlMeasureIndex);
+          const directionTypeNode: IXmlElement = xmlNode.element("direction-type");
+          //(*) MetronomeReader.readMetronomeInstructions(xmlNode, this.musicSheet, this.currentXmlMeasureIndex);
           let relativePositionInMeasure: number = Math.min(1, currentFraction.RealValue);
           if (this.activeRhythm !== undefined && this.activeRhythm.Rhythm !== undefined) {
             relativePositionInMeasure /= this.activeRhythm.Rhythm.RealValue;
           }
-          const directionTypeNode: IXmlElement = xmlNode.element("direction-type");
-          //let handeled: boolean = false;
+          let handeled: boolean = false;
           if (this.repetitionInstructionReader !== undefined) {
-            //handeled =
-            this.repetitionInstructionReader.handleRepetitionInstructionsFromWordsOrSymbols(directionTypeNode,
-                                                                                            relativePositionInMeasure);
+            handeled = this.repetitionInstructionReader.handleRepetitionInstructionsFromWordsOrSymbols(directionTypeNode,
+                                                                                                       relativePositionInMeasure);
+          }
+          if (!handeled) {
+           let expressionReader: ExpressionReader = this.expressionReaders[0];
+           const staffIndex: number = this.readExpressionStaffNumber(xmlNode) - 1;
+           if (staffIndex < this.expressionReaders.length) {
+             expressionReader = this.expressionReaders[staffIndex];
+           }
+           if (expressionReader !== undefined) {
+             if (directionTypeNode.element("octave-shift") !== undefined) {
+               expressionReader.readExpressionParameters(
+                 xmlNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, true
+               );
+               expressionReader.addOctaveShift(xmlNode, this.currentMeasure, previousFraction.clone());
+             }
+             expressionReader.readExpressionParameters(
+               xmlNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, false
+             );
+             expressionReader.read(xmlNode, this.currentMeasure, currentFraction);
+           }
           }
-          //}
-          //if (!handeled) {
-          //  let expressionReader: ExpressionReader = this.expressionReaders[0];
-          //  let staffIndex: number = this.readExpressionStaffNumber(xmlNode) - 1;
-          //  if (staffIndex < this.expressionReaders.length) {
-          //    expressionReader = this.expressionReaders[staffIndex];
-          //  }
-          //  if (expressionReader !== undefined) {
-          //    if (directionTypeNode.element("octave-shift") !== undefined) {
-          //      expressionReader.readExpressionParameters(
-          //        xmlNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, true
-          //      );
-          //      expressionReader.addOctaveShift(xmlNode, this.currentMeasure, previousFraction.clone());
-          //    }
-          //    expressionReader.readExpressionParameters(
-          //      xmlNode, this.instrument, this.divisions, currentFraction, previousFraction, this.currentMeasure.MeasureNumber, false
-          //    );
-          //    expressionReader.read(xmlNode, this.currentMeasure, currentFraction);
-          //  }
-          //}
         } else if (xmlNode.name === "barline") {
           if (this.repetitionInstructionReader !== undefined) {
            const measureEndsSystem: boolean = false;
@@ -391,13 +388,13 @@ export class InstrumentReader {
         if (!this.activeKeyHasBeenInitialized) {
           this.createDefaultKeyInstruction();
         }
-        // (*)
-        //for (let i: number = 0; i < this.expressionReaders.length; i++) {
-        //  let reader: ExpressionReader = this.expressionReaders[i];
-        //  if (reader !== undefined) {
-        //    reader.checkForOpenExpressions(this.currentMeasure, currentFraction);
-        //  }
-        //}
+
+        for (let i: number = 0; i < this.expressionReaders.length; i++) {
+         const reader: ExpressionReader = this.expressionReaders[i];
+         if (reader !== undefined) {
+           reader.checkForOpenExpressions(this.currentMeasure, currentFraction);
+         }
+        }
       }
     } catch (e) {
       if (divisionsException) {
@@ -451,13 +448,12 @@ export class InstrumentReader {
   }
 
 
-  //private createExpressionGenerators(numberOfStaves: number): void {
-  //  // (*)
-  //  //this.expressionReaders = new Array(numberOfStaves);
-  //  //for (let i: number = 0; i < numberOfStaves; i++) {
-  //  //  this.expressionReaders[i] = MusicSymbolModuleFactory.createExpressionGenerator(this.musicSheet, this.instrument, i + 1);
-  //  //}
-  //}
+  private createExpressionGenerators(numberOfStaves: number): void {
+     this.expressionReaders = new Array(numberOfStaves);
+     for (let i: number = 0; i < numberOfStaves; i++) {
+      this.expressionReaders[i] = new ExpressionReader(this.musicSheet, this.instrument, i + 1);
+     }
+  }
 
   /**
    * Create the default [[ClefInstruction]] for the given staff index.
@@ -986,26 +982,26 @@ export class InstrumentReader {
     return duration;
   }
 
-  //private readExpressionStaffNumber(xmlNode: IXmlElement): number {
-  //  let directionStaffNumber: number = 1;
-  //  if (xmlNode.element("staff") !== undefined) {
-  //    let staffNode: IXmlElement = xmlNode.element("staff");
-  //    if (staffNode !== undefined) {
-  //      try {
-  //        directionStaffNumber = parseInt(staffNode.value, 10);
-  //      } catch (ex) {
-  //        let errorMsg: string = ITextTranslation.translateText(
-  //          "ReaderErrorMessages/ExpressionStaffError", "Invalid Expression staff number -> set to default."
-  //        );
-  //        this.musicSheet.SheetErrors.pushTemp(errorMsg);
-  //        directionStaffNumber = 1;
-  //        log.debug("InstrumentReader.readExpressionStaffNumber", errorMsg, ex);
-  //      }
-  //
-  //    }
-  //  }
-  //  return directionStaffNumber;
-  //}
+  private readExpressionStaffNumber(xmlNode: IXmlElement): number {
+   let directionStaffNumber: number = 1;
+   if (xmlNode.element("staff") !== undefined) {
+     const staffNode: IXmlElement = xmlNode.element("staff");
+     if (staffNode !== undefined) {
+       try {
+         directionStaffNumber = parseInt(staffNode.value, 10);
+       } catch (ex) {
+         const errorMsg: string = ITextTranslation.translateText(
+           "ReaderErrorMessages/ExpressionStaffError", "Invalid Expression staff number -> set to default."
+         );
+         this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+         directionStaffNumber = 1;
+         log.debug("InstrumentReader.readExpressionStaffNumber", errorMsg, ex);
+       }
+
+     }
+   }
+   return directionStaffNumber;
+  }
 
   /**
    * Calculate the divisions value from the type and duration of the first MeasureNote that makes sense

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

@@ -0,0 +1,580 @@
+import {MusicSheet} from "../../MusicSheet";
+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, InstantaniousDynamicExpression} from "../../VoiceData/Expressions/InstantaniousDynamicExpression";
+import {OctaveShift} from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
+import {Instrument} from "../../Instrument";
+import {MultiExpression} from "../../VoiceData/Expressions/MultiExpression";
+import {IXmlAttribute, IXmlElement} from "../../../Common/FileIO/Xml";
+import {SourceMeasure} from "../../VoiceData/SourceMeasure";
+import {InstantaniousTempoExpression} from "../../VoiceData/Expressions/InstantaniousTempoExpression";
+import {MoodExpression} from "../../VoiceData/Expressions/MoodExpression";
+import {UnknownExpression} from "../../VoiceData/Expressions/UnknownExpression";
+import {TextAlignment} from "../../../Common/Enums/TextAlignment";
+import {ITextTranslation} from "../../Interfaces/ITextTranslation";
+import * as log from "loglevel";
+
+export enum PlacementEnum {
+    Above = 0,
+    Below = 1,
+    NotYetDefined = 2
+}
+
+export class ExpressionReader {
+    private musicSheet: MusicSheet;
+    private placement: PlacementEnum;
+    private soundTempo: number;
+    private soundDynamic: number;
+    private offsetDivisions: number;
+    private staffNumber: number;
+    private globalStaffIndex: number;
+    private directionTimestamp: Fraction;
+    private currentMultiTempoExpression: MultiTempoExpression;
+    private openContinuousDynamicExpression: ContinuousDynamicExpression;
+    private openContinuousTempoExpression: ContinuousTempoExpression;
+    private activeInstantaniousDynamic: InstantaniousDynamicExpression;
+    private openOctaveShift: OctaveShift;
+    constructor(musicSheet: MusicSheet, instrument: Instrument, staffNumber: number) {
+        this.musicSheet = musicSheet;
+        this.staffNumber = staffNumber;
+        this.globalStaffIndex = musicSheet.getGlobalStaffIndexOfFirstStaff(instrument) + (staffNumber - 1);
+        this.initialize();
+    }
+    public getMultiExpression: MultiExpression;
+    public readExpressionParameters(xmlNode: IXmlElement, currentInstrument: Instrument, divisions: number,
+                                    inSourceMeasureCurrentFraction: Fraction,
+                                    inSourceMeasureFormerFraction: Fraction,
+                                    currentMeasureIndex: number,
+                                    ignoreDivisionsOffset: boolean): void {
+        this.initialize();
+        const offsetNode: IXmlElement = xmlNode.element("offset");
+        if (offsetNode !== undefined && !ignoreDivisionsOffset) {
+            try {
+                this.offsetDivisions = parseInt(offsetNode.value, 10);
+            } catch (ex) {
+                const errorMsg: string = "ReaderErrorMessages/ExpressionOffsetError" + ", Invalid expression offset -> set to default.";
+                log.debug("ExpressionReader.readExpressionParameters", errorMsg, ex);
+                this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                this.offsetDivisions = 0;
+            }
+        }
+        this.directionTimestamp = Fraction.createFromFraction(inSourceMeasureCurrentFraction);
+        let offsetFraction: Fraction = new Fraction(Math.abs(this.offsetDivisions), divisions * 4);
+
+        if (this.offsetDivisions > 0) {
+            if (inSourceMeasureCurrentFraction.RealValue > 0) {
+                offsetFraction = Fraction.multiply(Fraction.minus(inSourceMeasureCurrentFraction, inSourceMeasureFormerFraction), offsetFraction);
+                this.directionTimestamp = Fraction.plus(offsetFraction, inSourceMeasureCurrentFraction);
+            } else { this.directionTimestamp = Fraction.createFromFraction(offsetFraction); }
+        } else if (this.offsetDivisions < 0) {
+            if (inSourceMeasureCurrentFraction.RealValue > 0) {
+                offsetFraction = Fraction.multiply(Fraction.minus(inSourceMeasureCurrentFraction, inSourceMeasureFormerFraction), offsetFraction);
+                this.directionTimestamp = Fraction.minus(inSourceMeasureCurrentFraction, offsetFraction);
+            } else { this.directionTimestamp = Fraction.createFromFraction(offsetFraction); }
+        }
+
+        const placeAttr: IXmlAttribute = xmlNode.attribute("placement");
+        if (placeAttr !== undefined) {
+            try {
+                const placementString: string = placeAttr.value;
+                if (placementString === "below") {
+                    this.placement = PlacementEnum.Below;
+                } else if (placementString === "above") {
+                    this.placement = PlacementEnum.Above;
+                     }
+            } catch (ex) {
+                const errorMsg: string = ITextTranslation.translateText(  "ReaderErrorMessages/ExpressionPlacementError",
+                                                                          "Invalid expression placement -> set to default.");
+                log.debug("ExpressionReader.readExpressionParameters", errorMsg, ex);
+                this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                this.placement = PlacementEnum.Below;
+            }
+
+        }
+        if (this.placement === PlacementEnum.NotYetDefined) {
+            try {
+                const directionTypeNode: IXmlElement = xmlNode.element("direction-type");
+                if (directionTypeNode !== undefined) {
+                    const dynamicsNode: IXmlElement = directionTypeNode.element("dynamics");
+                    if (dynamicsNode !== undefined) {
+                        const defAttr: IXmlAttribute = dynamicsNode.attribute("default-y");
+                        if (defAttr !== undefined) {
+                            this.readExpressionPlacement(defAttr, "read dynamics y pos");
+                        }
+                    }
+                    const wedgeNode: IXmlElement = directionTypeNode.element("wedge");
+                    if (wedgeNode !== undefined) {
+                        const defAttr: IXmlAttribute = wedgeNode.attribute("default-y");
+                        if (defAttr !== undefined) {
+                            this.readExpressionPlacement(defAttr, "read wedge y pos");
+                        }
+                    }
+                    const wordsNode: IXmlElement = directionTypeNode.element("words");
+                    if (wordsNode !== undefined) {
+                        const defAttr: IXmlAttribute = wordsNode.attribute("default-y");
+                        if (defAttr !== undefined) {
+                            this.readExpressionPlacement(defAttr, "read words y pos");
+                        }
+                    }
+                }
+            } catch (ex) {
+                const errorMsg: string = ITextTranslation.translateText(  "ReaderErrorMessages/ExpressionPlacementError",
+                                                                          "Invalid expression placement -> set to default.");
+                log.debug("ExpressionReader.readExpressionParameters", errorMsg, ex);
+                this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                this.placement = PlacementEnum.Below;
+            }
+
+        }
+        if (this.placement === PlacementEnum.NotYetDefined) {
+            if (currentInstrument.Staves.length > 1) {
+                this.placement = PlacementEnum.Below;
+            } else if (currentInstrument.HasLyrics) {
+                this.placement = PlacementEnum.Above;
+                 } else { this.placement = PlacementEnum.Below; }
+        }
+    }
+    public read(directionNode: IXmlElement, currentMeasure: SourceMeasure, inSourceMeasureCurrentFraction: Fraction): void {
+        let isTempoInstruction: boolean = false;
+        let isDynamicInstruction: boolean = false;
+        const n: IXmlElement = directionNode.element("sound");
+        if (n !== undefined) {
+            const tempoAttr: IXmlAttribute = n.attribute("tempo");
+            const dynAttr: IXmlAttribute = n.attribute("dynamics");
+            if (tempoAttr) {
+                const match: string[] = tempoAttr.value.match(/\d+/);
+                this.soundTempo = match !== undefined ? parseInt(match[0], 10) : 100;
+                isTempoInstruction = true;
+            }
+            if (dynAttr) {
+                const match: string[] = dynAttr.value.match(/\d+/);
+                this.soundDynamic = match !== undefined ? parseInt(match[0], 10) : 100;
+                isDynamicInstruction = true;
+            }
+        }
+        const dirNode: IXmlElement = directionNode.element("direction-type");
+        if (dirNode === undefined) {
+            return;
+        }
+        let dirContentNode: IXmlElement = dirNode.element("metronome");
+        if (dirContentNode !== undefined) {
+            const beatUnit: IXmlElement = dirContentNode.element("beat-unit");
+            const hasDot: boolean = dirContentNode.element("beat-unit-dot") !== undefined;
+            const bpm: IXmlElement = dirContentNode.element("per-minute");
+            if (beatUnit !== undefined && bpm !== undefined) {
+                const useCurrentFractionForPositioning: boolean = (dirContentNode.hasAttributes && dirContentNode.attribute("default-x") !== undefined);
+                if (useCurrentFractionForPositioning) {
+                    this.directionTimestamp = Fraction.createFromFraction(inSourceMeasureCurrentFraction);
+                }
+                let text: string = beatUnit.value + " = " + bpm.value;
+                if (hasDot) {
+                    text = "dotted " + text;
+                }
+                const bpmNumber: number = parseInt(bpm.value, 10);
+                this.createNewTempoExpressionIfNeeded(currentMeasure);
+                const instantaniousTempoExpression: InstantaniousTempoExpression =
+                    new InstantaniousTempoExpression(text,
+                                                     this.placement,
+                                                     this.staffNumber,
+                                                     bpmNumber,
+                                                     this.currentMultiTempoExpression,
+                                                     true);
+                this.currentMultiTempoExpression.addExpression(instantaniousTempoExpression, "");
+                this.currentMultiTempoExpression.CombinedExpressionsText = text;
+            }
+            return;
+        }
+
+        dirContentNode = dirNode.element("dynamics");
+        if (dirContentNode !== undefined) {
+            const fromNotation: boolean = directionNode.element("notations") !== undefined;
+            this.interpretInstantaniousDynamics(dirContentNode, currentMeasure, inSourceMeasureCurrentFraction, fromNotation);
+            return;
+        }
+
+        dirContentNode = dirNode.element("words");
+        if (dirContentNode !== undefined) {
+            if (isTempoInstruction) {
+                this.createNewTempoExpressionIfNeeded(currentMeasure);
+                this.currentMultiTempoExpression.CombinedExpressionsText = dirContentNode.value;
+                const instantaniousTempoExpression: InstantaniousTempoExpression =
+                    new InstantaniousTempoExpression(dirContentNode.value, this.placement, this.staffNumber, this.soundTempo, this.currentMultiTempoExpression);
+                this.currentMultiTempoExpression.addExpression(instantaniousTempoExpression, "");
+            } else if (!isDynamicInstruction) {
+                this.interpretWords(dirContentNode, currentMeasure, inSourceMeasureCurrentFraction);
+            }
+            return;
+        }
+
+        dirContentNode = dirNode.element("wedge");
+        if (dirContentNode !== undefined) {
+            this.interpretWedge(dirContentNode, currentMeasure, inSourceMeasureCurrentFraction, currentMeasure.MeasureNumber);
+            return;
+        }
+    }
+    public checkForOpenExpressions(sourceMeasure: SourceMeasure, timestamp: Fraction): void {
+        if (this.openContinuousDynamicExpression !== undefined) {
+            this.createNewMultiExpressionIfNeeded(sourceMeasure, timestamp);
+            this.closeOpenContinuousDynamic();
+        }
+        if (this.openContinuousTempoExpression !== undefined) {
+            this.closeOpenContinuousTempo(Fraction.plus(sourceMeasure.AbsoluteTimestamp, timestamp));
+        }
+    }
+    public addOctaveShift(directionNode: IXmlElement, currentMeasure: SourceMeasure, endTimestamp: Fraction): void {
+        let octaveStaffNumber: number = 1;
+        const staffNode: IXmlElement = directionNode.element("staff");
+        if (staffNode !== undefined) {
+            try {
+                octaveStaffNumber = parseInt(staffNode.value, 10);
+            } catch (ex) {
+                const errorMsg: string = ITextTranslation.translateText(  "ReaderErrorMessages/OctaveShiftStaffError",
+                                                                          "Invalid octave shift staff number -> set to default");
+                this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                octaveStaffNumber = 1;
+                log.debug("ExpressionReader.addOctaveShift", errorMsg, ex);
+            }
+        }
+        const directionTypeNode: IXmlElement = directionNode.element("direction-type");
+        if (directionTypeNode !== undefined) {
+            const octaveShiftNode: IXmlElement = directionTypeNode.element("octave-shift");
+            if (octaveShiftNode !== undefined && octaveShiftNode.hasAttributes) {
+                try {
+                    if (octaveShiftNode.attribute("size") !== undefined) {
+                        const size: number = parseInt(octaveShiftNode.attribute("size").value, 10);
+                        let octave: number = 0;
+                        if (size === 8) {
+                            octave = 1;
+                        } else if (size === 15) {
+                            octave = 2;
+                             }
+                        if (octaveShiftNode.attribute("type") !== undefined) {
+                            const type: string = octaveShiftNode.attribute("type").value;
+                            if (type === "up" || type === "down") {
+                                const octaveShift: OctaveShift = new OctaveShift(type, octave);
+                                octaveShift.StaffNumber = octaveStaffNumber;
+                                this.createNewMultiExpressionIfNeeded(currentMeasure);
+                                this.getMultiExpression.OctaveShiftStart = octaveShift;
+                                octaveShift.ParentStartMultiExpression = this.getMultiExpression;
+                                this.openOctaveShift = octaveShift;
+                            } else if (type === "stop") {
+                                if (this.openOctaveShift !== undefined) {
+                                    this.createNewMultiExpressionIfNeeded(currentMeasure, endTimestamp);
+                                    this.getMultiExpression.OctaveShiftEnd = this.openOctaveShift;
+                                    this.openOctaveShift.ParentEndMultiExpression = this.getMultiExpression;
+                                    this.openOctaveShift = undefined;
+                                }
+                            }
+                        }
+                    }
+                } catch (ex) {
+                    const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/OctaveShiftError", "Error while reading octave shift.");
+                    this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                    log.debug("ExpressionReader.addOctaveShift", errorMsg, ex);
+                }
+            }
+        }
+    }
+    private initialize(): void {
+        this.placement = PlacementEnum.NotYetDefined;
+        this.soundTempo = 0;
+        this.soundDynamic = 0;
+        this.offsetDivisions = 0;
+    }
+    private readExpressionPlacement(defAttr: IXmlAttribute, catchLogMessage: string): void {
+        try {
+            const y: number = parseInt(defAttr.value, 10);
+            if (y < 0) {
+                this.placement = PlacementEnum.Below;
+            } else if (y > 0) {
+                this.placement = PlacementEnum.Above;
+                 }
+        } catch (ex) {
+            log.debug("ExpressionReader.readExpressionParameters", catchLogMessage, ex);
+        }
+
+    }
+    private interpretInstantaniousDynamics(dynamicsNode: IXmlElement,
+                                           currentMeasure: SourceMeasure,
+                                           inSourceMeasureCurrentFraction: Fraction,
+                                           fromNotation: boolean): void {
+        if (dynamicsNode.hasElements) {
+            if (dynamicsNode.hasAttributes && dynamicsNode.attribute("default-x") !== undefined) {
+                this.directionTimestamp = Fraction.createFromFraction(inSourceMeasureCurrentFraction);
+            }
+            const name: string = dynamicsNode.elements()[0].name;
+            if (name !== undefined) {
+                let dynamicEnum: DynamicEnum;
+                try {
+                    dynamicEnum = DynamicEnum[name];
+                } catch (err) {
+                    const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/DynamicError", "Error while reading dynamic.");
+                    this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                    return;
+                }
+
+                if (this.activeInstantaniousDynamic === undefined ||
+                    (this.activeInstantaniousDynamic !== undefined && this.activeInstantaniousDynamic.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 instantaniousDynamicExpression: InstantaniousDynamicExpression = new InstantaniousDynamicExpression(name,
+                                                                                                                              this.soundDynamic,
+                                                                                                                              this.placement,
+                                                                                                                              this.staffNumber);
+                    this.getMultiExpression.addExpression(instantaniousDynamicExpression, "");
+                    this.initialize();
+                    if (this.activeInstantaniousDynamic !== undefined) {
+                        this.activeInstantaniousDynamic.DynEnum = instantaniousDynamicExpression.DynEnum;
+                    } else { this.activeInstantaniousDynamic = new InstantaniousDynamicExpression(name, 0, PlacementEnum.NotYetDefined, 1); }
+                }
+            }
+        }
+    }
+    private interpretWords(wordsNode: IXmlElement, currentMeasure: SourceMeasure, inSourceMeasureCurrentFraction: Fraction): void {
+        const text: string = wordsNode.value;
+        if (text.length > 0) {
+            if (wordsNode.hasAttributes && wordsNode.attribute("default-x") !== undefined) {
+                this.directionTimestamp = Fraction.createFromFraction(inSourceMeasureCurrentFraction);
+            }
+            if (this.checkIfWordsNodeIsRepetitionInstruction(text)) {
+                return;
+            }
+            this.fillMultiOrTempoExpression(text, currentMeasure);
+            this.initialize();
+        }
+    }
+    private interpretWedge(wedgeNode: IXmlElement, currentMeasure: SourceMeasure, inSourceMeasureCurrentFraction: Fraction, currentMeasureIndex: number): void {
+        if (wedgeNode !== undefined && wedgeNode.hasAttributes && wedgeNode.attribute("default-x") !== undefined) {
+            this.directionTimestamp = Fraction.createFromFraction(inSourceMeasureCurrentFraction);
+        }
+        this.createNewMultiExpressionIfNeeded(currentMeasure);
+        this.addWedge(wedgeNode, currentMeasureIndex);
+        this.initialize();
+    }
+    private createNewMultiExpressionIfNeeded(currentMeasure: SourceMeasure, timestamp: Fraction = undefined): void {
+        if (timestamp === undefined) {
+            timestamp = this.directionTimestamp;
+        }
+        if (this.getMultiExpression === undefined ||
+            this.getMultiExpression !== undefined &&
+            (this.getMultiExpression.SourceMeasureParent !== currentMeasure ||
+                (this.getMultiExpression.SourceMeasureParent === currentMeasure && this.getMultiExpression.Timestamp !== timestamp))) {
+            this.getMultiExpression = new MultiExpression(currentMeasure, Fraction.createFromFraction(timestamp));
+            currentMeasure.StaffLinkedExpressions[this.globalStaffIndex].push(this.getMultiExpression);
+        }
+    }
+
+    private createNewTempoExpressionIfNeeded(currentMeasure: SourceMeasure): void {
+        if (this.currentMultiTempoExpression === undefined ||
+            this.currentMultiTempoExpression.SourceMeasureParent !== currentMeasure ||
+            this.currentMultiTempoExpression.Timestamp !== this.directionTimestamp) {
+            this.currentMultiTempoExpression = new MultiTempoExpression(currentMeasure, Fraction.createFromFraction(this.directionTimestamp));
+            currentMeasure.TempoExpressions.push(this.currentMultiTempoExpression);
+        }
+    }
+    private addWedge(wedgeNode: IXmlElement, currentMeasureIndex: number): 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);
+                    if (this.openContinuousDynamicExpression !== undefined) {
+                        this.closeOpenContinuousDynamic();
+                    }
+                    this.openContinuousDynamicExpression = continuousDynamicExpression;
+                    this.getMultiExpression.StartingContinuousDynamic = continuousDynamicExpression;
+                    continuousDynamicExpression.StartMultiExpression = this.getMultiExpression;
+                    if (this.activeInstantaniousDynamic !== undefined &&
+                        this.activeInstantaniousDynamic.StaffNumber === continuousDynamicExpression.StaffNumber) {
+                        this.activeInstantaniousDynamic = undefined;
+                    }
+                } else if (type === "stop") {
+                    if (this.openContinuousDynamicExpression !== undefined) {
+                        this.closeOpenContinuousDynamic();
+                    }
+                }
+            } catch (ex) {
+                const errorMsg: string = "ReaderErrorMessages/WedgeError" + ", Error while reading Crescendo / Diminuendo.";
+                this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                log.debug("ExpressionReader.addWedge", errorMsg, ex);
+            }
+        }
+    }
+    private fillMultiOrTempoExpression(inputString: string, currentMeasure: SourceMeasure): void {
+        if (inputString === undefined) {
+            return;
+        }
+        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);
+
+        for (const splitStr of splitStrings) {
+            this.createExpressionFromString("", splitStr, currentMeasure, inputString);
+        }
+    }
+    /*
+    private splitStringRecursive(input: [string, string], stringSeparators: string[]): [string, string][] {
+        let text: string = input[1];
+        let lastSeparator: string = input[0];
+        let resultList: [string, string][] = [];
+        for (let idx: number = 0, len: number = stringSeparators.length; idx < len; ++idx) {
+            let stringSeparator: string = stringSeparators[idx];
+            if (text.indexOf(stringSeparator) < 0) {
+                continue;
+            }
+            let splitStrings: string[] = text.split(stringSeparator, StringSplitOptions.RemoveEmptyEntries);
+
+            if (splitStrings.length !== 0) {
+                resultList.push(...this.splitStringRecursive([lastSeparator, splitStrings[0]], stringSeparators));
+                for (let index: number = 1; index < splitStrings.length; index++) {
+                    resultList.push(...this.splitStringRecursive([stringSeparator, splitStrings[index]], stringSeparators));
+                }
+            } else {
+                resultList.push(["", stringSeparator]);
+            }
+            break;
+        }
+        if (resultList.length === 0) {
+            resultList.push(input);
+        }
+        return resultList;
+    }
+    */
+    private createExpressionFromString(prefix: string, stringTrimmed: string,
+                                       currentMeasure: SourceMeasure, inputString: string): boolean {
+        if (InstantaniousTempoExpression.isInputStringInstantaniousTempo(stringTrimmed) ||
+            ContinuousTempoExpression.isInputStringContinuousTempo(stringTrimmed)) {
+            // first check if there is already a tempo expression with the same function
+            if (currentMeasure.TempoExpressions.length > 0) {
+                for (let idx: number = 0, len: number = currentMeasure.TempoExpressions.length; idx < len; ++idx) {
+                    const multiTempoExpression: MultiTempoExpression = currentMeasure.TempoExpressions[idx];
+                    if (multiTempoExpression.Timestamp === this.directionTimestamp &&
+                        multiTempoExpression.InstantaniousTempo !== undefined &&
+                        multiTempoExpression.InstantaniousTempo.Label.indexOf(stringTrimmed) !== -1) {
+                        return false;
+                    }
+                }
+            }
+            this.createNewTempoExpressionIfNeeded(currentMeasure);
+            this.currentMultiTempoExpression.CombinedExpressionsText = inputString;
+            if (InstantaniousTempoExpression.isInputStringInstantaniousTempo(stringTrimmed)) {
+                const instantaniousTempoExpression: InstantaniousTempoExpression = new InstantaniousTempoExpression(  stringTrimmed,
+                                                                                                                      this.placement,
+                                                                                                                      this.staffNumber,
+                                                                                                                      this.soundTempo,
+                                                                                                                      this.currentMultiTempoExpression);
+                this.currentMultiTempoExpression.addExpression(instantaniousTempoExpression, prefix);
+                return true;
+            }
+            if (ContinuousTempoExpression.isInputStringContinuousTempo(stringTrimmed)) {
+                const continuousTempoExpression: ContinuousTempoExpression = new ContinuousTempoExpression(   stringTrimmed,
+                                                                                                              this.placement,
+                                                                                                              this.staffNumber,
+                                                                                                              this.currentMultiTempoExpression);
+                this.currentMultiTempoExpression.addExpression(continuousTempoExpression, prefix);
+                return true;
+            }
+        }
+        if (InstantaniousDynamicExpression.isInputStringInstantaniousDynamic(stringTrimmed) ||
+            ContinuousDynamicExpression.isInputStringContinuousDynamic(stringTrimmed)) {
+            this.createNewMultiExpressionIfNeeded(currentMeasure);
+            if (InstantaniousDynamicExpression.isInputStringInstantaniousDynamic(stringTrimmed)) {
+                if (this.openContinuousDynamicExpression !== undefined && this.openContinuousDynamicExpression.EndMultiExpression === undefined) {
+                    this.closeOpenContinuousDynamic();
+                }
+                const instantaniousDynamicExpression: InstantaniousDynamicExpression = new InstantaniousDynamicExpression(stringTrimmed,
+                                                                                                                          this.soundDynamic,
+                                                                                                                          this.placement,
+                                                                                                                          this.staffNumber);
+                this.getMultiExpression.addExpression(instantaniousDynamicExpression, prefix);
+                return true;
+            }
+            if (ContinuousDynamicExpression.isInputStringContinuousDynamic(stringTrimmed)) {
+                const continuousDynamicExpression: ContinuousDynamicExpression = new ContinuousDynamicExpression( undefined,
+                                                                                                                  this.placement,
+                                                                                                                  this.staffNumber,
+                                                                                                                  stringTrimmed);
+                if (this.openContinuousDynamicExpression !== undefined && this.openContinuousDynamicExpression.EndMultiExpression === undefined) {
+                    this.closeOpenContinuousDynamic();
+                }
+                if (this.activeInstantaniousDynamic !== undefined && this.activeInstantaniousDynamic.StaffNumber === continuousDynamicExpression.StaffNumber) {
+                    this.activeInstantaniousDynamic = undefined;
+                }
+                this.openContinuousDynamicExpression = continuousDynamicExpression;
+                continuousDynamicExpression.StartMultiExpression = this.getMultiExpression;
+                this.getMultiExpression.addExpression(continuousDynamicExpression, prefix);
+                return true;
+            }
+        }
+        if (MoodExpression.isInputStringMood(stringTrimmed)) {
+            this.createNewMultiExpressionIfNeeded(currentMeasure);
+            const moodExpression: MoodExpression = new MoodExpression(stringTrimmed, this.placement, this.staffNumber);
+            this.getMultiExpression.addExpression(moodExpression, prefix);
+            return true;
+        }
+
+        // create unknown:
+        this.createNewMultiExpressionIfNeeded(currentMeasure);
+        if (currentMeasure.TempoExpressions.length > 0) {
+            for (let idx: number = 0, len: number = currentMeasure.TempoExpressions.length; idx < len; ++idx) {
+                const multiTempoExpression: MultiTempoExpression = currentMeasure.TempoExpressions[idx];
+                if (multiTempoExpression.Timestamp === this.directionTimestamp &&
+                    multiTempoExpression.InstantaniousTempo !== undefined &&
+                    multiTempoExpression.EntriesList.length > 0 &&
+                    !this.hasDigit(stringTrimmed)) {
+                    if (this.globalStaffIndex > 0) {
+                        if (multiTempoExpression.EntriesList[0].label.indexOf(stringTrimmed) >= 0) {
+                            return false;
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        const unknownExpression: UnknownExpression = new UnknownExpression(stringTrimmed, this.placement, TextAlignment.CenterBottom, this.staffNumber);
+        this.getMultiExpression.addExpression(unknownExpression, prefix);
+
+        return false;
+    }
+    private closeOpenContinuousDynamic(): void {
+        this.openContinuousDynamicExpression.EndMultiExpression = this.getMultiExpression;
+        this.getMultiExpression.EndingContinuousDynamic = this.openContinuousDynamicExpression;
+        this.openContinuousDynamicExpression = undefined;
+    }
+    private closeOpenContinuousTempo(endTimestamp: Fraction): void {
+        this.openContinuousTempoExpression.AbsoluteEndTimestamp = endTimestamp;
+        this.openContinuousTempoExpression = undefined;
+    }
+    private checkIfWordsNodeIsRepetitionInstruction(inputString: string): boolean {
+        inputString = inputString.trim().toLowerCase();
+        if (inputString === "coda" ||
+            inputString === "tocoda" ||
+            inputString === "to coda" ||
+            inputString === "fine" ||
+            inputString === "d.c." ||
+            inputString === "dacapo" ||
+            inputString === "da capo" ||
+            inputString === "d.s." ||
+            inputString === "dalsegno" ||
+            inputString === "dal segno" ||
+            inputString === "d.c. al fine" ||
+            inputString === "d.s. al fine" ||
+            inputString === "d.c. al coda" ||
+            inputString === "d.s. al coda") {
+            return true;
+        }
+        return false;
+    }
+    private hasDigit(input: string): boolean {
+        return /\d/.test(input);
+    }
+}

+ 4 - 10
src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression.ts

@@ -3,15 +3,7 @@ import {MultiExpression} from "../MultiExpression";
 import {Fraction} from "../../../../Common/DataObjects/Fraction";
 
 export class ContinuousDynamicExpression extends AbstractExpression {
-    //constructor(placement: PlacementEnum, staffNumber: number, label: string) {
-    //    this.label = label;
-    //    this.placement = placement;
-    //    this.staffNumber = staffNumber;
-    //    this.startVolume = -1;
-    //    this.endVolume = -1;
-    //    this.setType();
-    //}
-    constructor(dynamicType: ContDynamicEnum, placement: PlacementEnum, staffNumber: number, label: string) {
+    constructor(dynamicType: ContDynamicEnum, placement: PlacementEnum, staffNumber: number, label: string = "") {
         super();
         this.dynamicType = dynamicType;
         this.label = label;
@@ -19,7 +11,9 @@ export class ContinuousDynamicExpression extends AbstractExpression {
         this.staffNumber = staffNumber;
         this.startVolume = -1;
         this.endVolume = -1;
-        this.setType();
+        if (label !== "") {
+            this.setType();
+        }
     }
 
     private static listContinuousDynamicIncreasing: string[] = ["crescendo", "cresc", "cresc.", "cres."];

+ 29 - 0
src/MusicalScore/VoiceData/Expressions/ContinuousExpressions/OctaveShift.ts

@@ -1,4 +1,5 @@
 import {MultiExpression} from "../MultiExpression";
+import { Pitch } from "../../../../Common/DataObjects/Pitch";
 
 export class OctaveShift {
     constructor(type: string, octave: number) {
@@ -48,6 +49,34 @@ export class OctaveShift {
             this.octaveValue = OctaveEnum.NONE;
         }
     }
+
+    /**
+     * Convert a source (XML) pitch of a note to the pitch needed to draw. E.g. 8va would draw +1 octave so we reduce by 1
+     * @param pitch Original pitch
+     * @param octaveShiftValue octave shift
+     * @returns New pitch with corrected octave shift
+     */
+    public static getPitchFromOctaveShift(pitch: Pitch, octaveShiftValue: OctaveEnum): Pitch {
+        let result: number = pitch.Octave;
+        switch (octaveShiftValue) {
+            case OctaveEnum.VA8:
+                result -= 1;
+                break;
+            case OctaveEnum.VB8:
+                result += 1;
+                break;
+            case OctaveEnum.MA15:
+                result -= 2;
+                break;
+            case OctaveEnum.MB15:
+                result += 2;
+                break;
+            case OctaveEnum.NONE:
+            default:
+                result += 0;
+        }
+        return new Pitch(pitch.FundamentalNote, result, pitch.Accidental);
+    }
 }
 
 export enum OctaveEnum {

+ 2 - 1
src/MusicalScore/VoiceData/Expressions/InstantaniousTempoExpression.ts

@@ -5,7 +5,8 @@ import {Fraction} from "../../../Common/DataObjects/Fraction";
 import {MultiTempoExpression} from "./MultiTempoExpression";
 
 export class InstantaniousTempoExpression extends AbstractTempoExpression {
-    constructor(label: string, placement: PlacementEnum, staffNumber: number, soundTempo: number, parentMultiTempoExpression: MultiTempoExpression) {
+    constructor(label: string, placement: PlacementEnum, staffNumber: number,
+                soundTempo: number, parentMultiTempoExpression: MultiTempoExpression, isMetronomeMark: boolean = false) {
         super(label, placement, staffNumber, parentMultiTempoExpression);
         this.setTempoAndTempoType(soundTempo);
     }

+ 2 - 2
src/MusicalScore/VoiceData/SourceStaffEntry.ts

@@ -224,8 +224,8 @@ export class SourceStaffEntry {
                             tieRestDuration.Add(n.Length);
                         }
                     }
-                    if (duration.lt(note.NoteTie.Duration)) {
-                        duration = note.NoteTie.Duration;
+                    if (duration.lt(tieRestDuration)) {
+                        duration = tieRestDuration;
                     }
                 } else if (duration.lt(note.Length)) {
                     duration = note.Length;

+ 599 - 0
test/data/OSMD_function_test_expressions.musicxml

@@ -0,0 +1,599 @@
+<?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>Expression Test</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 2.2.1</software>
+      <encoding-date>2018-08-15</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7.05556</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1683.36</page-height>
+      <page-width>1190.88</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.44" default-y="1626.67" justify="center" valign="top" font-size="24">Expression Test</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Klavier</part-name>
+      <part-abbreviation>Klav.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Klavier</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="259.32">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>0.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>2</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.58" default-y="-80.00">
+            <ppp/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="17.78"/>
+        </direction>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="down" size="8" number="1" default-y="30.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="75.17" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="120.81" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.58" default-y="-80.00">
+            <p/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="54.44"/>
+        </direction>
+      <note default-x="166.45" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="212.08" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="2" width="204.54">
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.58" default-y="-80.00">
+            <fff/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="140.00"/>
+        </direction>
+      <note default-x="12.00" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="59.74" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.58" default-y="-80.00">
+            <f/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="106.67"/>
+        </direction>
+      <note default-x="107.47" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="155.21" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="stop" size="8" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="3" width="204.54">
+      <direction placement="below">
+        <direction-type>
+          <octave-shift type="up" size="8" number="1" default-y="-79.41"/>
+          </direction-type>
+        </direction>
+      <note default-x="12.00" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="59.74" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="107.47" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="155.21" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <octave-shift type="stop" size="8" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="4" width="204.54">
+      <direction placement="below">
+        <direction-type>
+          <wedge type="diminuendo" number="1" default-y="-75.43" relative-x="-1.43"/>
+          </direction-type>
+        </direction>
+      <note default-x="12.00" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="59.74" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="107.47" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="155.21" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1" relative-x="-1.43"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="5" width="204.54">
+      <direction placement="below">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="-75.00"/>
+          </direction-type>
+        </direction>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="down" size="15" number="1" default-y="30.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="12.00" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>6</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="59.74" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="107.47" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="155.21" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="6" width="246.92">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>0.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <system-distance>150.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="49.07" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>6</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="98.14" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="147.20" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="196.26" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>6</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="stop" size="15" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="7" width="284.43">
+      <note default-x="12.00" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="45.85" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="45.85" default-y="0.00">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="79.71" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="79.71" default-y="0.00">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="113.56" default-y="10.00">
+        <pitch>
+          <step>A</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <beam number="1">end</beam>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.58" default-y="-80.00">
+            <ff/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="124.44"/>
+        </direction>
+      <note default-x="147.41" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      <note default-x="215.12" default-y="-5.00">
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      </measure>
+    <measure number="8" width="174.57">
+      <note default-x="12.00" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      </measure>
+    <measure number="9" width="174.57">
+      <note default-x="12.00" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>8</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      </measure>
+    <measure number="10" width="197.00">
+      <barline location="left">
+        <ending number="1" type="start" default-y="31.43" relative-x="-1.57"/>
+        </barline>
+      <note default-x="12.00" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="12.00" default-y="-10.00">
+        <chord/>
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="99.02" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>up</stem>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        <ending number="1" type="discontinue" relative-x="-3.41"/>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 375 - 0
test/data/visual_compare/Expression_Test.musicxml

@@ -0,0 +1,375 @@
+<?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>Expression Test</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 2.2.1</software>
+      <encoding-date>2018-06-04</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.36</page-height>
+      <page-width>1190.88</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.44" default-y="1626.67" justify="center" valign="top" font-size="24">Expression Test</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Klavier</part-name>
+      <part-abbreviation>Klav.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Klavier</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="257.52">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>-0.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.58" default-y="-80.00">
+            <ppp/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="17.78"/>
+        </direction>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="down" size="8" number="1" default-y="30.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="75.17" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="120.36" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.58" default-y="-80.00">
+            <p/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="54.44"/>
+        </direction>
+      <note default-x="165.55" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="210.73" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      </measure>
+    <measure number="2" width="202.74">
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.58" default-y="-80.00">
+            <fff/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="140.00"/>
+        </direction>
+      <note default-x="12.00" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="59.29" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.58" default-y="-80.00">
+            <f/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="106.67"/>
+        </direction>
+      <note default-x="106.57" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="153.86" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="stop" size="8" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="3" width="202.74">
+      <direction placement="below">
+        <direction-type>
+          <octave-shift type="up" size="8" number="1" default-y="-79.41"/>
+          </direction-type>
+        </direction>
+      <note default-x="12.00" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="59.29" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="106.57" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="153.86" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <octave-shift type="stop" size="8" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="4" width="202.74">
+      <direction placement="below">
+        <direction-type>
+          <wedge type="diminuendo" number="1" default-y="-75.43" relative-x="-1.43"/>
+          </direction-type>
+        </direction>
+      <note default-x="12.00" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="59.29" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="106.57" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="153.86" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1" relative-x="-1.43"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="5" width="211.74">
+      <barline location="left">
+        <ending number="1" type="start" default-y="28.57"/>
+        </barline>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="-75.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="12.00" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="59.29" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="106.57" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="153.86" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        <ending number="1" type="discontinue"/>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>