فهرست منبع

Merge pull request #716 from opensheetmusicdisplay/feat/Tabs

Merge feat/tabs into develop, implement #126
Simon 5 سال پیش
والد
کامیت
8b2f7a19f2

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

@@ -41,6 +41,7 @@ export class EngravingRules {
     private staffDistance: number;
     private betweenStaffDistance: number;
     private staffHeight: number;
+    private tabStaffHeight: number;
     private betweenStaffLinesDistance: number;
     /** Whether to automatically beam notes that don't already have beams in XML. */
     private autoBeamNotes: boolean;
@@ -246,6 +247,7 @@ export class EngravingRules {
 
         // System Sizing and Label Variables
         this.staffHeight = 4.0;
+        this.tabStaffHeight = 6.67;
         this.betweenStaffLinesDistance = EngravingRules.unit;
         this.systemLeftMargin = 0.0;
         this.systemRightMargin = 0.0;
@@ -622,6 +624,12 @@ export class EngravingRules {
     public set StaffHeight(value: number) {
         this.staffHeight = value;
     }
+    public get TabStaffHeight(): number {
+        return this.tabStaffHeight;
+    }
+    public set TabStaffHeight(value: number) {
+        this.tabStaffHeight = value;
+    }
     public get BetweenStaffLinesDistance(): number {
         return this.betweenStaffLinesDistance;
     }

+ 9 - 1
src/MusicalScore/Graphical/GraphicalMeasure.ts

@@ -42,7 +42,6 @@ export abstract class GraphicalMeasure extends GraphicalObject {
 
     public parentSourceMeasure: SourceMeasure;
     public staffEntries: GraphicalStaffEntry[];
-    public parentMusicSystem: MusicSystem;
     /**
      * The x-width of possibly existing: repetition start line, clef, key, rhythm.
      */
@@ -62,6 +61,7 @@ export abstract class GraphicalMeasure extends GraphicalObject {
     public hasError: boolean;
 
     private parentStaff: Staff;
+    private parentMusicSystem: MusicSystem;
     private measureNumber: number = -1;
     private parentStaffLine: StaffLine;
 
@@ -69,6 +69,14 @@ export abstract class GraphicalMeasure extends GraphicalObject {
         return this.parentStaff;
     }
 
+    public get ParentMusicSystem(): MusicSystem {
+        return this.parentMusicSystem;
+    }
+
+    public set ParentMusicSystem(value: MusicSystem) {
+        this.parentMusicSystem = value;
+    }
+
     public get MeasureNumber(): number {
         return this.measureNumber;
     }

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

@@ -60,6 +60,6 @@ export class GraphicalNote extends GraphicalObject {
     }
 
     public get ParentMusicPage(): GraphicalMusicPage {
-      return this.parentVoiceEntry.parentStaffEntry.parentMeasure.parentMusicSystem.Parent;
+      return this.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentMusicSystem.Parent;
     }
 }

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

@@ -49,6 +49,7 @@ export abstract class GraphicalStaffEntry extends GraphicalObject {
     public graphicalVoiceEntries: GraphicalVoiceEntry[];
     public staffEntryParent: GraphicalStaffEntry;
     public parentVerticalContainer: VerticalGraphicalStaffEntryContainer;
+    public tabStaffEntry: GraphicalStaffEntry = undefined;
 
     private graphicalInstructions: AbstractGraphicalInstruction[] = [];
     private graphicalTies: GraphicalTie[] = [];

+ 53 - 8
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -7,7 +7,7 @@ import { Fraction } from "../../Common/DataObjects/Fraction";
 import { Note } from "../VoiceData/Note";
 import { MusicSheet } from "../MusicSheet";
 import { GraphicalMeasure } from "./GraphicalMeasure";
-import { ClefInstruction } from "../VoiceData/Instructions/ClefInstruction";
+import {ClefInstruction, ClefEnum} from "../VoiceData/Instructions/ClefInstruction";
 import { LyricWord } from "../VoiceData/Lyrics/LyricsWord";
 import { SourceMeasure } from "../VoiceData/SourceMeasure";
 import { GraphicalMusicPage } from "./GraphicalMusicPage";
@@ -1458,8 +1458,16 @@ export abstract class MusicSheetCalculator {
         } else {
             this.calculateStemDirectionFromVoices(voiceEntry);
         }
+        // if GraphicalStaffEntry has been created earlier (because of Tie), then the GraphicalNotesLists have also been created
         const gve: GraphicalVoiceEntry = graphicalStaffEntry.findOrCreateGraphicalVoiceEntry(voiceEntry);
         gve.octaveShiftValue = octaveShiftValue;
+        // check for Tabs:
+        const tabStaffEntry: GraphicalStaffEntry = graphicalStaffEntry.tabStaffEntry;
+        let graphicalTabVoiceEntry: GraphicalVoiceEntry;
+        if (tabStaffEntry !== undefined) {
+            graphicalTabVoiceEntry = tabStaffEntry.findOrCreateGraphicalVoiceEntry(voiceEntry);
+        }
+
         for (let idx: number = 0, len: number = voiceEntry.Notes.length; idx < len; ++idx) {
             const note: Note = voiceEntry.Notes[idx];
             if (note === undefined) {
@@ -1488,6 +1496,24 @@ export abstract class MusicSheetCalculator {
                     this.handleTuplet(graphicalNote, note.NoteTuplet, openTuplets);
                 }
             }
+
+            // handle TabNotes:
+            if (graphicalTabVoiceEntry) {
+                // notes should be either TabNotes or RestNotes -> add all:
+                const graphicalTabNote: GraphicalNote = MusicSheetCalculator.symbolFactory.createNote(  note,
+                                                                                                        graphicalTabVoiceEntry,
+                                                                                                        activeClef,
+                                                                                                        octaveShiftValue,
+                                                                                                        undefined);
+                tabStaffEntry.addGraphicalNoteToListAtCorrectYPosition(graphicalTabVoiceEntry, graphicalTabNote);
+                graphicalTabNote.PositionAndShape.calculateBoundingBox();
+
+                if (!this.leadSheet) {
+                    if (note.NoteTuplet) {
+                        this.handleTuplet(graphicalTabNote, note.NoteTuplet, openTuplets);
+                    }
+                }
+            }
         }
         if (voiceEntry.Articulations.length > 0) {
             this.handleVoiceEntryArticulations(voiceEntry.Articulations, voiceEntry, graphicalStaffEntry);
@@ -1894,8 +1920,15 @@ export abstract class MusicSheetCalculator {
                                    openOctaveShifts: OctaveShiftParams[], openLyricWords: LyricWord[], staffIndex: number,
                                    staffEntryLinks: StaffEntryLink[]): GraphicalMeasure {
         const staff: Staff = this.graphicalMusicSheet.ParentMusicSheet.getStaffFromIndex(staffIndex);
-        const measure: GraphicalMeasure = MusicSheetCalculator.symbolFactory.createGraphicalMeasure(sourceMeasure, staff);
+        let measure: GraphicalMeasure = undefined;
+        if (activeClefs[staffIndex].ClefType === ClefEnum.TAB) {
+            staff.isTab = true;
+            measure = MusicSheetCalculator.symbolFactory.createTabStaffMeasure(sourceMeasure, staff);
+        } else {
+            measure = MusicSheetCalculator.symbolFactory.createGraphicalMeasure(sourceMeasure, staff);
+        }
         measure.hasError = sourceMeasure.getErrorInMeasure(staffIndex);
+        // check for key instruction changes
         if (sourceMeasure.FirstInstructionsStaffEntries[staffIndex] !== undefined) {
             for (let idx: number = 0, len: number = sourceMeasure.FirstInstructionsStaffEntries[staffIndex].Instructions.length; idx < len; ++idx) {
                 const instruction: AbstractNotationInstruction = sourceMeasure.FirstInstructionsStaffEntries[staffIndex].Instructions[idx];
@@ -1912,6 +1945,7 @@ export abstract class MusicSheetCalculator {
                 }
             }
         }
+        // check for octave shifts
         for (let idx: number = 0, len: number = sourceMeasure.StaffLinkedExpressions[staffIndex].length; idx < len; ++idx) {
             const multiExpression: MultiExpression = sourceMeasure.StaffLinkedExpressions[staffIndex][idx];
             if (multiExpression.OctaveShiftStart !== undefined) {
@@ -1923,36 +1957,45 @@ export abstract class MusicSheetCalculator {
                 );
             }
         }
+        // create GraphicalStaffEntries - always check for possible null Entry
         for (let entryIndex: number = 0; entryIndex < sourceMeasure.VerticalSourceStaffEntryContainers.length; entryIndex++) {
             const sourceStaffEntry: SourceStaffEntry = sourceMeasure.VerticalSourceStaffEntryContainers[entryIndex].StaffEntries[staffIndex];
+            // is there a SourceStaffEntry at this Index
             if (sourceStaffEntry !== undefined) {
+                // a SourceStaffEntry exists
+                // is there an inStaff ClefInstruction? -> update activeClef
                 for (let idx: number = 0, len: number = sourceStaffEntry.Instructions.length; idx < len; ++idx) {
                     const abstractNotationInstruction: AbstractNotationInstruction = sourceStaffEntry.Instructions[idx];
                     if (abstractNotationInstruction instanceof ClefInstruction) {
                         activeClefs[staffIndex] = <ClefInstruction>abstractNotationInstruction;
                     }
                 }
+                // create new GraphicalStaffEntry
                 const graphicalStaffEntry: GraphicalStaffEntry = MusicSheetCalculator.symbolFactory.createStaffEntry(sourceStaffEntry, measure);
-                if (measure.staffEntries.length > entryIndex) {
+                if (entryIndex < measure.staffEntries.length) {
+                    // a GraphicalStaffEntry has been inserted already at this Index (from Tie)
                     measure.addGraphicalStaffEntryAtTimestamp(graphicalStaffEntry);
                 } else {
                     measure.addGraphicalStaffEntry(graphicalStaffEntry);
                 }
+
                 const linkedNotes: Note[] = [];
                 if (sourceStaffEntry.Link !== undefined) {
                     sourceStaffEntry.findLinkedNotes(linkedNotes);
                     this.handleStaffEntryLink(graphicalStaffEntry, staffEntryLinks);
                 }
+                // check for possible OctaveShift
                 let octaveShiftValue: OctaveEnum = OctaveEnum.NONE;
                 if (openOctaveShifts[staffIndex] !== undefined) {
-                    const octaveShiftParams: OctaveShiftParams = openOctaveShifts[staffIndex];
-                    if (octaveShiftParams.getAbsoluteStartTimestamp.lte(sourceStaffEntry.AbsoluteTimestamp) &&
-                        sourceStaffEntry.AbsoluteTimestamp.lte(octaveShiftParams.getAbsoluteEndTimestamp)) {
-                        octaveShiftValue = octaveShiftParams.getOpenOctaveShift.Type;
+                    if (openOctaveShifts[staffIndex].getAbsoluteStartTimestamp.lte(sourceStaffEntry.AbsoluteTimestamp) &&
+                        sourceStaffEntry.AbsoluteTimestamp.lte(openOctaveShifts[staffIndex].getAbsoluteEndTimestamp)) {
+                        octaveShiftValue = openOctaveShifts[staffIndex].getOpenOctaveShift.Type;
                     }
                 }
+                // for each visible Voice create the corresponding GraphicalNotes
                 for (let idx: number = 0, len: number = sourceStaffEntry.VoiceEntries.length; idx < len; ++idx) {
                     const voiceEntry: VoiceEntry = sourceStaffEntry.VoiceEntries[idx];
+                    // Normal Notes...
                     octaveShiftValue = this.handleVoiceEntry(
                         voiceEntry, graphicalStaffEntry,
                         accidentalCalculator, openLyricWords,
@@ -1961,6 +2004,7 @@ export abstract class MusicSheetCalculator {
                         sourceStaffEntry
                     );
                 }
+                // SourceStaffEntry has inStaff ClefInstruction -> create graphical clef
                 if (sourceStaffEntry.Instructions.length > 0) {
                     const clefInstruction: ClefInstruction = <ClefInstruction>sourceStaffEntry.Instructions[0];
                     MusicSheetCalculator.symbolFactory.createInStaffClef(graphicalStaffEntry, clefInstruction);
@@ -1976,6 +2020,7 @@ export abstract class MusicSheetCalculator {
         }
 
         accidentalCalculator.doCalculationsAtEndOfMeasure();
+        // update activeClef given at end of measure if needed
         if (sourceMeasure.LastInstructionsStaffEntries[staffIndex] !== undefined) {
             const lastStaffEntry: SourceStaffEntry = sourceMeasure.LastInstructionsStaffEntries[staffIndex];
             for (let idx: number = 0, len: number = lastStaffEntry.Instructions.length; idx < len; ++idx) {
@@ -1989,7 +2034,7 @@ export abstract class MusicSheetCalculator {
             const multiExpression: MultiExpression = sourceMeasure.StaffLinkedExpressions[staffIndex][idx];
             if (multiExpression.OctaveShiftEnd !== undefined && openOctaveShifts[staffIndex] !== undefined &&
                 multiExpression.OctaveShiftEnd === openOctaveShifts[staffIndex].getOpenOctaveShift) {
-                openOctaveShifts[staffIndex] = undefined;
+                    openOctaveShifts[staffIndex] = undefined;
             }
         }
         // check wantedStemDirections of beam notes at end of measure (e.g. for beam with grace notes)

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

@@ -201,7 +201,7 @@ export abstract class MusicSystem extends GraphicalObject {
     public AddGraphicalMeasures(graphicalMeasures: GraphicalMeasure[]): void {
         for (let idx: number = 0, len: number = graphicalMeasures.length; idx < len; ++idx) {
             const graphicalMeasure: GraphicalMeasure = graphicalMeasures[idx];
-            graphicalMeasure.parentMusicSystem = this;
+            graphicalMeasure.ParentMusicSystem = this;
         }
         this.graphicalMeasures.push(graphicalMeasures);
     }

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

@@ -147,7 +147,7 @@ export class SkyBottomLineCalculator {
         }
         // Remap the values from 0 to +/- height in units
         this.mSkyLine = this.mSkyLine.map(v => (v - Math.max(...this.mSkyLine)) / unitInPixels);
-        this.mBottomLine = this.mBottomLine.map(v => (v - Math.min(...this.mBottomLine)) / unitInPixels + this.mRules.StaffHeight);
+        this.mBottomLine = this.mBottomLine.map(v => (v - Math.min(...this.mBottomLine)) / unitInPixels + this.StaffLineParent.StaffHeight);
     }
 
     /**
@@ -409,7 +409,7 @@ export class SkyBottomLineCalculator {
                 const endPoint: number = Math.ceil(boundingBox.AbsolutePosition.x + boundingBox.BorderRight) ;
 
                 this.updateInRange(this.mSkyLine, startPoint, endPoint, currentTopBorder);
-            } else if (currentBottomBorder > this.mRules.StaffHeight) {
+            } else if (currentBottomBorder > this.StaffLineParent.StaffHeight) {
                 const startPoint: number = Math.floor(boundingBox.AbsolutePosition.x + boundingBox.BorderLeft);
                 const endPoint: number = Math.ceil(boundingBox.AbsolutePosition.x + boundingBox.BorderRight);
 

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

@@ -13,6 +13,7 @@ import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
 import { GraphicalOctaveShift } from "./GraphicalOctaveShift";
 import { GraphicalSlur } from "./GraphicalSlur";
 import { AbstractGraphicalExpression } from "./AbstractGraphicalExpression";
+import { EngravingRules } from "./EngravingRules";
 
 /**
  * A StaffLine contains the [[Measure]]s in one line of the music sheet
@@ -28,6 +29,8 @@ export abstract class StaffLine extends GraphicalObject {
     protected lyricLines: GraphicalLine[] = [];
     protected lyricsDashes: GraphicalLabel[] = [];
     protected abstractExpressions: AbstractGraphicalExpression[] = [];
+    /** The staff height in units */
+    private staffHeight: number;
 
     // For displaying Slurs
     protected graphicalSlurs: GraphicalSlur[] = [];
@@ -38,6 +41,10 @@ export abstract class StaffLine extends GraphicalObject {
         this.parentStaff = parentStaff;
         this.boundingBox = new BoundingBox(this, parentSystem.PositionAndShape);
         this.skyBottomLine = new SkyBottomLineCalculator(this);
+        this.staffHeight = EngravingRules.Rules.StaffHeight;
+        if (this.parentStaff.isTab) {
+            this.staffHeight = EngravingRules.Rules.TabStaffHeight;
+        }
     }
 
     public get Measures(): GraphicalMeasure[] {
@@ -121,6 +128,10 @@ export abstract class StaffLine extends GraphicalObject {
         this.octaveShifts = value;
     }
 
+    public get StaffHeight(): number {
+        return this.staffHeight;
+    }
+
     // get all Graphical Slurs of a staffline
     public get GraphicalSlurs(): GraphicalSlur[] {
         return this.graphicalSlurs;

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

@@ -26,6 +26,7 @@ import { EngravingRules } from "../EngravingRules";
 import { Note } from "../..";
 import StaveNote = Vex.Flow.StaveNote;
 import { ArpeggioType } from "../../VoiceData";
+import { TabNote } from "../../VoiceData/TabNote";
 
 /**
  * Helper class, which contains static methods which actually convert
@@ -487,6 +488,37 @@ export class VexFlowConverter {
     }
 
     /**
+     * Convert a set of GraphicalNotes to a VexFlow StaveNote
+     * @param notes form a chord on the staff
+     * @returns {Vex.Flow.StaveNote}
+     */
+    public static CreateTabNote(gve: GraphicalVoiceEntry): Vex.Flow.TabNote {
+        const tabPositions: {str: number, fret: number}[] = [];
+        const frac: Fraction = gve.notes[0].graphicalNoteLength;
+        const isTuplet: boolean = gve.notes[0].sourceNote.NoteTuplet !== undefined;
+        let duration: string = VexFlowConverter.duration(frac, isTuplet);
+        let numDots: number = 0;
+        for (const note of gve.notes) {
+            const tabNote: TabNote = note.sourceNote as TabNote;
+            const tabPosition: {str: number, fret: number} = {str: tabNote.StringNumber, fret: tabNote.FretNumber};
+            tabPositions.push(tabPosition);
+
+            if (numDots < note.numberOfDots) {
+                numDots = note.numberOfDots;
+            }
+        }
+        for (let i: number = 0, len: number = numDots; i < len; ++i) {
+            duration += "d";
+        }
+        const vfnote: Vex.Flow.TabNote = new Vex.Flow.TabNote({
+            duration: duration,
+            positions: tabPositions,
+        });
+
+        return vfnote;
+    }
+
+    /**
      * Convert a ClefInstruction to a string represention of a clef type in VexFlow.
      *
      * @param clef The OSMD object to be converted representing the clef
@@ -573,7 +605,8 @@ export class VexFlowConverter {
 
             // TAB Clef
             case ClefEnum.TAB:
-                type = "tab";
+                // only used currently for creating the notes in the normal stave: There we need a normal treble clef
+                type = "treble";
                 break;
             default:
         }

+ 15 - 4
src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalSymbolFactory.ts

@@ -4,7 +4,6 @@ import {MusicSystem} from "../MusicSystem";
 import {VexFlowMusicSystem} from "./VexFlowMusicSystem";
 import {Staff} from "../../VoiceData/Staff";
 import {StaffLine} from "../StaffLine";
-import {VexFlowStaffLine} from "./VexFlowStaffLine";
 import {SourceMeasure} from "../../VoiceData/SourceMeasure";
 import {GraphicalMeasure} from "../GraphicalMeasure";
 import {VexFlowMeasure} from "./VexFlowMeasure";
@@ -26,6 +25,8 @@ import { GraphicalVoiceEntry } from "../GraphicalVoiceEntry";
 import { VoiceEntry } from "../../VoiceData/VoiceEntry";
 import { VexFlowVoiceEntry } from "./VexFlowVoiceEntry";
 import { VexFlowConverter } from "./VexFlowConverter";
+import { VexFlowTabMeasure } from "./VexFlowTabMeasure";
+import { VexFlowStaffLine } from "./VexFlowStaffLine";
 
 export class VexFlowGraphicalSymbolFactory implements IGraphicalSymbolFactory {
     /**
@@ -55,8 +56,18 @@ export class VexFlowGraphicalSymbolFactory implements IGraphicalSymbolFactory {
      * @param staff
      * @returns {VexFlowMeasure}
      */
-    public createGraphicalMeasure(sourceMeasure: SourceMeasure, staff: Staff): GraphicalMeasure {
-        return new VexFlowMeasure(staff, undefined, sourceMeasure);
+    public createGraphicalMeasure(sourceMeasure: SourceMeasure, staff: Staff, isTabMeasure: boolean = false): GraphicalMeasure {
+        return new VexFlowMeasure(staff, sourceMeasure, undefined);
+    }
+
+    /**
+     * Construct an empty Tab staffMeasure from the given source measure and staff.
+     * @param sourceMeasure
+     * @param staff
+     * @returns {VexFlowTabMeasure}
+     */
+    public createTabStaffMeasure(sourceMeasure: SourceMeasure, staff: Staff): GraphicalMeasure {
+        return new VexFlowTabMeasure(staff, sourceMeasure);
     }
 
     /**
@@ -65,7 +76,7 @@ export class VexFlowGraphicalSymbolFactory implements IGraphicalSymbolFactory {
      * @returns {VexFlowMeasure}
      */
     public createExtraGraphicalMeasure(staffLine: StaffLine): GraphicalMeasure {
-        return new VexFlowMeasure(staffLine.ParentStaff, staffLine);
+        return new VexFlowMeasure(staffLine.ParentStaff, undefined, staffLine);
     }
 
     /**

+ 42 - 24
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -4,7 +4,7 @@ import {SourceMeasure} from "../../VoiceData/SourceMeasure";
 import {Staff} from "../../VoiceData/Staff";
 import {StaffLine} from "../StaffLine";
 import {SystemLinesEnum} from "../SystemLinesEnum";
-import {ClefInstruction} from "../../VoiceData/Instructions/ClefInstruction";
+import {ClefInstruction, ClefEnum} from "../../VoiceData/Instructions/ClefInstruction";
 import {KeyInstruction} from "../../VoiceData/Instructions/KeyInstruction";
 import {RhythmInstruction} from "../../VoiceData/Instructions/RhythmInstruction";
 import {VexFlowConverter} from "./VexFlowConverter";
@@ -29,14 +29,14 @@ import {Voice} from "../../VoiceData/Voice";
 import {LinkedVoice} from "../../VoiceData/LinkedVoice";
 import {EngravingRules} from "../EngravingRules";
 import {OrnamentContainer} from "../../VoiceData/OrnamentContainer";
-import {TechnicalInstruction} from "../../VoiceData/Instructions/TechnicalInstruction";
+import {TechnicalInstruction, TechnicalInstructionType} from "../../VoiceData/Instructions/TechnicalInstruction";
 import {PlacementEnum} from "../../VoiceData/Expressions/AbstractExpression";
 import {VexFlowGraphicalNote} from "./VexFlowGraphicalNote";
 import {AutoBeamOptions} from "../../../OpenSheetMusicDisplay/OSMDOptions";
 import {NoteType, Arpeggio} from "../../VoiceData";
 
 export class VexFlowMeasure extends GraphicalMeasure {
-    constructor(staff: Staff, staffLine: StaffLine = undefined, sourceMeasure: SourceMeasure = undefined) {
+    constructor(staff: Staff, sourceMeasure: SourceMeasure = undefined, staffLine: StaffLine = undefined) {
         super(staff, sourceMeasure, staffLine);
         this.minimumStaffEntriesWidth = -1;
         this.resetLayout();
@@ -53,9 +53,9 @@ export class VexFlowMeasure extends GraphicalMeasure {
     /** The repetition instructions given as words or symbols (coda, dal segno..) */
     public vfRepetitionWords: Vex.Flow.Repetition[] = [];
     /** The VexFlow Stave (= one measure in a staffline) */
-    private stave: Vex.Flow.Stave;
+    protected stave: Vex.Flow.Stave;
     /** VexFlow StaveConnectors (vertical lines) */
-    private connectors: Vex.Flow.StaveConnector[] = [];
+    protected connectors: Vex.Flow.StaveConnector[] = [];
     /** Intermediate object to construct beams */
     private beams: { [voiceID: number]: [Beam, VexFlowVoiceEntry[]][]; } = {};
     /** Beams created by (optional) autoBeam function. */
@@ -65,9 +65,9 @@ export class VexFlowMeasure extends GraphicalMeasure {
     /** VexFlow Beams */
     private vfbeams: { [voiceID: number]: Vex.Flow.Beam[]; };
     /** Intermediate object to construct tuplets */
-    private tuplets: { [voiceID: number]: [Tuplet, VexFlowVoiceEntry[]][]; } = {};
+    protected tuplets: { [voiceID: number]: [Tuplet, VexFlowVoiceEntry[]][]; } = {};
     /** VexFlow Tuplets */
-    private vftuplets: { [voiceID: number]: Vex.Flow.Tuplet[]; } = {};
+    protected vftuplets: { [voiceID: number]: Vex.Flow.Tuplet[]; } = {};
 
     // Sets the absolute coordinates of the VFStave on the canvas
     public setAbsoluteCoordinates(x: number, y: number): void {
@@ -144,8 +144,12 @@ export class VexFlowMeasure extends GraphicalMeasure {
      */
     public addClefAtBegin(clef: ClefInstruction): void {
         this.octaveOffset = clef.OctaveOffset;
+        if (clef.ClefType === ClefEnum.TAB) {
+            this.stave.addClef("tab", undefined, undefined, undefined);
+        } else {
         const vfclef: { type: string, size: string, annotation: string } = VexFlowConverter.Clef(clef, "default");
         this.stave.addClef(vfclef.type, vfclef.size, vfclef.annotation, Vex.Flow.StaveModifier.Position.BEGIN);
+        }
         this.updateInstructionWidth();
     }
 
@@ -451,7 +455,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
      * And the graphical notes have to be analysed directly (and not the voiceEntries, as it actually should be -> needs refactoring)
      * @param voice the voice for which the ghost notes shall be searched.
      */
-    private getRestFilledVexFlowStaveNotesPerVoice(voice: Voice): GraphicalVoiceEntry[] {
+    protected getRestFilledVexFlowStaveNotesPerVoice(voice: Voice): GraphicalVoiceEntry[] {
         let latestVoiceTimestamp: Fraction = undefined;
         const gvEntries: GraphicalVoiceEntry[] = this.getGraphicalVoiceEntriesPerVoice(voice);
         for (let idx: number = 0, len: number = gvEntries.length; idx < len; ++idx) {
@@ -999,7 +1003,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
     /**
      * Create the articulations for all notes of the current staff entry
      */
-    private createArticulations(): void {
+    protected createArticulations(): void {
         for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
             const graphicalStaffEntry: VexFlowStaffEntry = (this.staffEntries[idx] as VexFlowStaffEntry);
 
@@ -1015,7 +1019,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
     /**
      * Create the ornaments for all notes of the current staff entry
      */
-    private createOrnaments(): void {
+    protected createOrnaments(): void {
         for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
             const graphicalStaffEntry: VexFlowStaffEntry = (this.staffEntries[idx] as VexFlowStaffEntry);
             const gvoices: { [voiceID: number]: GraphicalVoiceEntry; } = graphicalStaffEntry.graphicalVoiceEntries;
@@ -1032,15 +1036,24 @@ export class VexFlowMeasure extends GraphicalMeasure {
         }
     }
 
-    private createFingerings(voiceEntry: GraphicalVoiceEntry): void {
+    protected createFingerings(voiceEntry: GraphicalVoiceEntry): void {
         const vexFlowVoiceEntry: VexFlowVoiceEntry = voiceEntry as VexFlowVoiceEntry;
         const technicalInstructions: TechnicalInstruction[] = voiceEntry.parentVoiceEntry.TechnicalInstructions;
-        const fingeringsCount: number = technicalInstructions.length;
-        for (let i: number = 0; i < technicalInstructions.length; i++) {
-            const technicalInstruction: TechnicalInstruction = technicalInstructions[i];
+        let fingeringsCount: number = 0;
+        for (const instruction of technicalInstructions) {
+            if (instruction.type === TechnicalInstructionType.Fingering) {
+                fingeringsCount++;
+            }
+        }
+        let fingeringIndex: number = -1;
+        for (const fingeringInstruction of technicalInstructions) {
+            if (fingeringInstruction.type !== TechnicalInstructionType.Fingering) {
+                continue;
+            }
+            fingeringIndex++; // 0 for first fingering
             let fingeringPosition: PlacementEnum = EngravingRules.Rules.FingeringPosition;
-            if (technicalInstruction.placement !== PlacementEnum.NotYetDefined) {
-                fingeringPosition = technicalInstruction.placement;
+            if (fingeringInstruction.placement !== PlacementEnum.NotYetDefined) {
+                fingeringPosition = fingeringInstruction.placement;
             }
             let modifierPosition: any; // Vex.Flow.Stavemodifier.Position
             switch (fingeringPosition) {
@@ -1070,12 +1083,12 @@ export class VexFlowMeasure extends GraphicalMeasure {
                     }
             }
 
-            const fretFinger: Vex.Flow.FretHandFinger = new Vex.Flow.FretHandFinger(technicalInstruction.value);
+            const fretFinger: Vex.Flow.FretHandFinger = new Vex.Flow.FretHandFinger(fingeringInstruction.value);
             fretFinger.setPosition(modifierPosition);
             if (fingeringPosition === PlacementEnum.Above || fingeringPosition === PlacementEnum.Below) {
                 const offsetYSign: number = fingeringPosition === PlacementEnum.Above ? -1 : 1; // minus y is up
-                const ordering: number = fingeringPosition === PlacementEnum.Above ? i :
-                    technicalInstructions.length - 1 - i; // reverse order for fingerings below staff
+                const ordering: number = fingeringPosition === PlacementEnum.Above ? fingeringIndex :
+                    fingeringsCount - 1 - fingeringIndex; // reverse order for fingerings below staff
                 if (EngravingRules.Rules.FingeringInsideStafflines && fingeringsCount > 1) { // y-shift for single fingering is ok
                     // experimental, bounding boxes wrong for fretFinger above/below, better would be creating Labels
                     // set y-shift. vexflow fretfinger simply places directly above/below note
@@ -1083,17 +1096,22 @@ export class VexFlowMeasure extends GraphicalMeasure {
                     const shiftCount: number = fingeringsCount * 2.5;
                     (<any>fretFinger).setOffsetY(offsetYSign * (ordering + shiftCount) * perFingeringShift);
                 } else if (!EngravingRules.Rules.FingeringInsideStafflines) { // use StringNumber for placement above/below stafflines
-                    const stringNumber: Vex.Flow.StringNumber = new Vex.Flow.StringNumber(technicalInstruction.value);
+                    const stringNumber: Vex.Flow.StringNumber = new Vex.Flow.StringNumber(fingeringInstruction.value);
                     (<any>stringNumber).radius = 0; // hack to remove the circle around the number
                     stringNumber.setPosition(modifierPosition);
                     stringNumber.setOffsetY(offsetYSign * ordering * stringNumber.getWidth() * 2 / 3);
                     // Vexflow made a mess with the addModifier signature that changes through each class so we just cast to any :(
-                    vexFlowVoiceEntry.vfStaveNote.addModifier((i as any), (stringNumber as any));
+                    vexFlowVoiceEntry.vfStaveNote.addModifier((fingeringIndex as any), (stringNumber as any));
                     continue;
                 }
             }
-            // Vexflow made a mess with the addModifier signature that changes through each class so we just cast to any :(
-            vexFlowVoiceEntry.vfStaveNote.addModifier((i as any), (fretFinger as any));
+            if (vexFlowVoiceEntry.vfStaveNote.getCategory() === "tabnotes") {
+                // TODO this doesn't work yet. don't add fingering for tabs for now.
+                // vexFlowVoiceEntry.vfStaveNote.addModifier(fretFinger, fingeringIndex);
+            } else {
+                // Vexflow made a mess with the addModifier signature that changes through each class so we just cast to any :(
+                vexFlowVoiceEntry.vfStaveNote.addModifier((fingeringIndex as any), (fretFinger as any));
+            }
         }
     }
 
@@ -1120,7 +1138,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
      * After re-running the formatting on the VexFlow Stave, update the
      * space needed by Instructions (in VexFlow: StaveModifiers)
      */
-    private updateInstructionWidth(): void {
+    protected updateInstructionWidth(): void {
         let vfBeginInstructionsWidth: number = 0;
         let vfEndInstructionsWidth: number = 0;
         const modifiers: Vex.Flow.StaveModifier[] = this.stave.getModifiers();

+ 35 - 27
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -99,11 +99,12 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
    * @returns the minimum required x width of the source measure (=list of staff measures)
    */
   protected calculateMeasureXLayout(measures: GraphicalMeasure[]): number {
-    // Finalize beams
-    /*for (let measure of measures) {
-     (measure as VexFlowMeasure).finalizeBeams();
-     (measure as VexFlowMeasure).finalizeTuplets();
-     }*/
+    const visibleMeasures: GraphicalMeasure[] = [];
+    for (const measure of measures) {
+      visibleMeasures.push(measure);
+    }
+    measures = visibleMeasures;
+
     // Format the voices
     const allVoices: Vex.Flow.Voice[] = [];
     const formatter: Vex.Flow.Formatter = new Vex.Flow.Formatter();
@@ -117,6 +118,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
           allVoices.push(mvoices[voiceID]);
         }
       }
+
       if (voices.length === 0) {
         log.debug("Found a measure with no voices. Continuing anyway.", mvoices);
         // no need to log this, measures with no voices/notes are fine. see OSMDOptions.fillEmptyMeasuresWithWholeRest
@@ -439,29 +441,35 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
 
     if (tieIsAtSystemBreak) {
       // split tie into two ties:
-      const vfTie1: Vex.Flow.StaveTie = new Vex.Flow.StaveTie({
-        first_indices: [startNoteIndexInTie],
-        first_note: vfStartNote
-      });
-      const measure1: VexFlowMeasure = (startNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
-      measure1.vfTies.push(vfTie1);
-
-      const vfTie2: Vex.Flow.StaveTie = new Vex.Flow.StaveTie({
-        last_indices: [endNoteIndexInTie],
-        last_note: vfEndNote
-      });
-      const measure2: VexFlowMeasure = (endNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
-      measure2.vfTies.push(vfTie2);
+      if (vfStartNote) { // first_note or last_note must be not null in Vexflow
+        const vfTie1: Vex.Flow.StaveTie = new Vex.Flow.StaveTie({
+          first_indices: [startNoteIndexInTie],
+          first_note: vfStartNote
+        });
+        const measure1: VexFlowMeasure = (startNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
+        measure1.vfTies.push(vfTie1);
+      }
+
+      if (vfEndNote) {
+        const vfTie2: Vex.Flow.StaveTie = new Vex.Flow.StaveTie({
+          last_indices: [endNoteIndexInTie],
+          last_note: vfEndNote
+        });
+        const measure2: VexFlowMeasure = (endNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
+        measure2.vfTies.push(vfTie2);
+      }
     } else {
       // normal case
-      const vfTie: Vex.Flow.StaveTie = new Vex.Flow.StaveTie({
-        first_indices: [startNoteIndexInTie],
-        first_note: vfStartNote,
-        last_indices: [endNoteIndexInTie],
-        last_note: vfEndNote
-      });
-      const measure: VexFlowMeasure = (endNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
-      measure.vfTies.push(vfTie);
+      if (vfStartNote || vfEndNote) { // one of these must be not null in Vexflow
+        const vfTie: Vex.Flow.StaveTie = new Vex.Flow.StaveTie({
+          first_indices: [startNoteIndexInTie],
+          first_note: vfStartNote,
+          last_indices: [endNoteIndexInTie],
+          last_note: vfEndNote
+        });
+        const measure: VexFlowMeasure = (endNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
+        measure.vfTies.push(vfTie);
+      }
     }
   }
 
@@ -505,7 +513,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
 
       if (!graphicalContinuousDynamic.IsVerbal && continuousDynamic.EndMultiExpression) {
         try {
-          this.calculateGraphicalContinuousDynamic(graphicalContinuousDynamic, dynamicStartPosition);
+        this.calculateGraphicalContinuousDynamic(graphicalContinuousDynamic, dynamicStartPosition);
         } catch (e) {
           // TODO this sometimes fails when the measure range to draw doesn't include all the dynamic's measures, method needs to be adjusted
           //   see calculateGraphicalContinuousDynamic(), also in MusicSheetCalculator.

+ 8 - 2
src/MusicalScore/Graphical/VexFlow/VexFlowStaffEntry.ts

@@ -30,11 +30,17 @@ export class VexFlowStaffEntry extends GraphicalStaffEntry {
         for (const gve of this.graphicalVoiceEntries as VexFlowVoiceEntry[]) {
             if (gve.vfStaveNote) {
                 gve.vfStaveNote.setStave(stave);
-                if (!gve.vfStaveNote.preFormatted || gve.vfStaveNote.getBoundingBox() === null) {
+                if (!gve.vfStaveNote.preFormatted) {
                     continue;
                 }
                 gve.applyBordersFromVexflow();
-                this.PositionAndShape.RelativePosition.x = gve.vfStaveNote.getBoundingBox().getX() / unitInPixels;
+                if (this.parentMeasure.ParentStaff.isTab) {
+                    // the x-position could be finetuned for the cursor.
+                    // somehow, gve.vfStaveNote.getBoundingBox() is null for a TabNote (which is a StemmableNote).
+                    this.PositionAndShape.RelativePosition.x = (gve.vfStaveNote.getAbsoluteX() + (<any>gve.vfStaveNote).glyph.getWidth()) / unitInPixels;
+                } else {
+                    this.PositionAndShape.RelativePosition.x = gve.vfStaveNote.getBoundingBox().getX() / unitInPixels;
+                }
                 const sourceNote: Note = gve.notes[0].sourceNote;
                 if (sourceNote.isRest() && sourceNote.Length.RealValue === this.parentMeasure.parentSourceMeasure.ActiveTimeSignature.RealValue) {
                     // whole rest: length = measure length. (4/4 in a 4/4 time signature, 3/4 in a 3/4 time signature, 1/4 in a 1/4 time signature, etc.)

+ 116 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowTabMeasure.ts

@@ -0,0 +1,116 @@
+import Vex = require("vexflow");
+import { Staff } from "../../VoiceData/Staff";
+import { SourceMeasure } from "../../VoiceData/SourceMeasure";
+import { VexFlowMeasure } from "./VexFlowMeasure";
+import { VexFlowStaffEntry } from "./VexFlowStaffEntry";
+import { VexFlowConverter } from "./VexFlowConverter";
+import { StaffLine } from "../StaffLine";
+import { GraphicalVoiceEntry } from "../GraphicalVoiceEntry";
+import { VexFlowVoiceEntry } from "./VexFlowVoiceEntry";
+import { EngravingRules } from "../EngravingRules";
+import { Arpeggio } from "../../VoiceData/Arpeggio";
+import { Voice } from "../../VoiceData/Voice";
+import * as log from "loglevel";
+
+export class VexFlowTabMeasure extends VexFlowMeasure {
+    constructor(staff: Staff, sourceMeasure: SourceMeasure = undefined, staffLine: StaffLine = undefined) {
+        super(staff, sourceMeasure, staffLine);
+    }
+
+    /**
+     * Reset all the geometric values and parameters of this measure and put it in an initialized state.
+     * This is needed to evaluate a measure a second time by system builder.
+     */
+    public resetLayout(): void {
+        // Take into account some space for the begin and end lines of the stave
+        // Will be changed when repetitions will be implemented
+        //this.beginInstructionsWidth = 20 / UnitInPixels;
+        //this.endInstructionsWidth = 20 / UnitInPixels;
+        this.stave = new Vex.Flow.TabStave(0, 0, 0, {
+            space_above_staff_ln: 0,
+            space_below_staff_ln: 0,
+        });
+        this.updateInstructionWidth();
+    }
+
+    public graphicalMeasureCreatedCalculations(): void {
+        for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
+            const graphicalStaffEntry: VexFlowStaffEntry = (this.staffEntries[idx] as VexFlowStaffEntry);
+
+            // create vex flow Notes:
+            for (const gve of graphicalStaffEntry.graphicalVoiceEntries) {
+                if (gve.notes[0].sourceNote.isRest()) {
+                    (gve as VexFlowVoiceEntry).vfStaveNote = VexFlowConverter.GhostNote(gve.notes[0].sourceNote.Length);
+                } else {
+                    (gve as VexFlowVoiceEntry).vfStaveNote = VexFlowConverter.CreateTabNote(gve);
+                }
+            }
+        }
+
+        this.finalizeTuplets();
+
+        const voices: Voice[] = this.getVoicesWithinMeasure();
+
+        for (const voice of voices) {
+            if (voice === undefined) {
+                continue;
+            }
+
+            // add a vexFlow voice for this voice:
+            this.vfVoices[voice.VoiceId] = new Vex.Flow.Voice({
+                        beat_value: this.parentSourceMeasure.Duration.Denominator,
+                        num_beats: this.parentSourceMeasure.Duration.Numerator,
+                        resolution: Vex.Flow.RESOLUTION,
+                    }).setMode(Vex.Flow.Voice.Mode.SOFT);
+
+            const restFilledEntries: GraphicalVoiceEntry[] =  this.getRestFilledVexFlowStaveNotesPerVoice(voice);
+            // create vex flow voices and add tickables to it:
+            for (const voiceEntry of restFilledEntries) {
+                if (voiceEntry.parentVoiceEntry) {
+                    if (voiceEntry.parentVoiceEntry.IsGrace && !voiceEntry.parentVoiceEntry.GraceAfterMainNote) {
+                        continue;
+                    }
+                }
+
+                const vexFlowVoiceEntry: VexFlowVoiceEntry = voiceEntry as VexFlowVoiceEntry;
+                if (voiceEntry.notes.length === 0 || !voiceEntry.notes[0] || !voiceEntry.notes[0].sourceNote.PrintObject) {
+                    // GhostNote, don't add modifiers like in-measure clefs
+                    this.vfVoices[voice.VoiceId].addTickable(vexFlowVoiceEntry.vfStaveNote);
+                    continue;
+                }
+
+                // add fingering
+                if (voiceEntry.parentVoiceEntry && EngravingRules.Rules.RenderFingerings) {
+                    this.createFingerings(voiceEntry);
+                }
+
+                // add Arpeggio
+                if (voiceEntry.parentVoiceEntry && voiceEntry.parentVoiceEntry.Arpeggio !== undefined) {
+                    const arpeggio: Arpeggio = voiceEntry.parentVoiceEntry.Arpeggio;
+                    // TODO right now our arpeggio object has all arpeggio notes from arpeggios across all voices.
+                    // see VoiceGenerator. Doesn't matter for Vexflow for now though
+                    if (voiceEntry.notes && voiceEntry.notes.length > 1) {
+                        const type: Vex.Flow.Stroke.Type = VexFlowConverter.StrokeTypeFromArpeggioType(arpeggio.type);
+                        const stroke: Vex.Flow.Stroke = new Vex.Flow.Stroke(type, {
+                            all_voices: EngravingRules.Rules.ArpeggiosGoAcrossVoices
+                            // default: false. This causes arpeggios to always go across all voices, which is often unwanted.
+                            // also, this can cause infinite height of stroke, see #546
+                        });
+                        //if (arpeggio.notes.length === vexFlowVoiceEntry.notes.length) { // different workaround for endless y bug
+                        if (EngravingRules.Rules.RenderArpeggios) {
+                            vexFlowVoiceEntry.vfStaveNote.addStroke(0, stroke);
+                        }
+                    } else {
+                        log.debug(`[OSMD] arpeggio in measure ${this.MeasureNumber} could not be drawn.
+                        voice entry had less than two notes, arpeggio is likely between voice entries, not currently supported in Vexflow.`);
+                        // TODO: create new arpeggio with all the arpeggio's notes (arpeggio.notes), perhaps with GhostNotes in a new vfStaveNote. not easy.
+                    }
+                }
+
+                this.vfVoices[voice.VoiceId].addTickable(vexFlowVoiceEntry.vfStaveNote);
+            }
+        }
+        //this.createArticulations();
+        //this.createOrnaments();
+    }
+}

+ 2 - 0
src/MusicalScore/Interfaces/IGraphicalSymbolFactory.ts

@@ -23,6 +23,8 @@ export interface IGraphicalSymbolFactory {
 
     createGraphicalMeasure(sourceMeasure: SourceMeasure, staff: Staff): GraphicalMeasure;
 
+    createTabStaffMeasure(sourceMeasure: SourceMeasure, staff: Staff): GraphicalMeasure;
+
     createExtraGraphicalMeasure(staffLine: StaffLine): GraphicalMeasure;
 
     createStaffEntry(sourceStaffEntry: SourceStaffEntry, measure: GraphicalMeasure): GraphicalStaffEntry;

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

@@ -762,9 +762,6 @@ export class InstrumentReader {
           try {
             clefEnum = ClefEnum[signNode.value];
             if (!ClefInstruction.isSupportedClef(clefEnum)) {
-              if (clefEnum === ClefEnum.TAB && guitarPro) {
-                clefOctaveOffset = -1;
-              }
               errorMsg = ITextTranslation.translateText(
                 "ReaderErrorMessages/ClefError",
                 "Unsupported clef found -> using default clef."
@@ -773,6 +770,9 @@ export class InstrumentReader {
               clefEnum = ClefEnum.G;
               line = 2;
             }
+            if (clefEnum === ClefEnum.TAB) {
+              clefOctaveOffset = -1;
+            }
           } catch (e) {
             errorMsg = ITextTranslation.translateText(
               "ReaderErrorMessages/ClefError",

+ 28 - 1
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -28,6 +28,7 @@ import { SlurReader } from "./MusicSymbolModules/SlurReader";
 import { Notehead } from "../VoiceData/Notehead";
 import { Arpeggio, ArpeggioType } from "../VoiceData/Arpeggio";
 import { NoteType } from "../VoiceData/NoteType";
+import { TabNote } from "../VoiceData/TabNote";
 
 export class VoiceGenerator {
   constructor(instrument: Instrument, voiceId: number, slurReader: SlurReader, mainVoice: Voice = undefined) {
@@ -436,7 +437,33 @@ export class VoiceGenerator {
     noteOctave -= Pitch.OctaveXmlDifference;
     const pitch: Pitch = new Pitch(noteStep, noteOctave, noteAccidental);
     const noteLength: Fraction = Fraction.createFromFraction(noteDuration);
-    const note: Note = new Note(this.currentVoiceEntry, this.currentStaffEntry, noteLength, pitch);
+    let note: Note = undefined;
+    let stringNumber: number = -1;
+    let fretNumber: number = -1;
+    // check for guitar tabs:
+    const notationNode: IXmlElement = node.element("notations");
+    if (notationNode !== undefined) {
+      const technicalNode: IXmlElement = notationNode.element("technical");
+      if (technicalNode !== undefined) {
+        const stringNode: IXmlElement = technicalNode.element("string");
+        if (stringNode !== undefined) {
+          stringNumber = parseInt(stringNode.value, 10);
+        }
+        const fretNode: IXmlElement = technicalNode.element("fret");
+        if (fretNode !== undefined) {
+          fretNumber = parseInt(fretNode.value, 10);
+        }
+      }
+    }
+
+    if (stringNumber < 0 || fretNumber < 0) {
+      // create normal Note
+      note = new Note(this.currentVoiceEntry, this.currentStaffEntry, noteLength, pitch);
+    } else {
+      // create TabNote
+      note = new TabNote(this.currentVoiceEntry, this.currentStaffEntry, noteLength, pitch, stringNumber, fretNumber);
+    }
+
     note.TypeLength = typeDuration;
     note.NoteTypeXml = noteTypeXml;
     note.NormalNotes = normalNotes;

+ 6 - 1
src/MusicalScore/VoiceData/Instructions/ClefInstruction.ts

@@ -66,6 +66,7 @@ export class ClefInstruction extends AbstractNotationInstruction {
             case ClefEnum.F:
             case ClefEnum.C:
             case ClefEnum.percussion:
+            case ClefEnum.TAB:
                 return true;
             default:
                 return false;
@@ -119,7 +120,7 @@ export class ClefInstruction extends AbstractNotationInstruction {
         if (this === undefined || other === undefined) {
             return false;
         }
-        return (this.ClefPitch === other.ClefPitch && this.Line === other.Line);
+        return (this.clefPitch === other.clefPitch && this.Line === other.Line);
     }
 
     public NotEqual(clef2: ClefInstruction): boolean {
@@ -148,6 +149,10 @@ export class ClefInstruction extends AbstractNotationInstruction {
                 this.clefPitch = new Pitch(NoteEnum.C, 2, AccidentalEnum.NONE);
                 this.referenceCyPosition = 2;
                 break;
+            case ClefEnum.TAB:
+                this.clefPitch = new Pitch(NoteEnum.G, 0, AccidentalEnum.NONE);
+                this.referenceCyPosition = 0;
+                break;
             default:
                 throw new ArgumentOutOfRangeException("clefType");
         }

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

@@ -13,6 +13,7 @@ export class Staff {
     public idInMusicSheet: number;
     public audible: boolean;
     public following: boolean;
+    public isTab: boolean = false;
 
     private parentInstrument: Instrument;
     private voices: Voice[] = [];

+ 24 - 0
src/MusicalScore/VoiceData/TabNote.ts

@@ -0,0 +1,24 @@
+import { Note } from "./Note";
+import { Fraction } from "../../Common/DataObjects/Fraction";
+import { VoiceEntry } from "./VoiceEntry";
+import { SourceStaffEntry } from "./SourceStaffEntry";
+import { Pitch } from "../../Common/DataObjects/Pitch";
+
+export class TabNote extends Note {
+    constructor(voiceEntry: VoiceEntry, parentStaffEntry: SourceStaffEntry, length: Fraction, pitch: Pitch, stringNumber: number, fretNumber: number) {
+        super(voiceEntry, parentStaffEntry, length, pitch);
+        this.stringNumber = stringNumber;
+        this.fretNumber = fretNumber;
+    }
+
+    private stringNumber: number;
+    private fretNumber: number;
+
+    public get StringNumber(): number {
+        return this.stringNumber;
+    }
+
+    public get FretNumber(): number {
+        return this.fretNumber;
+    }
+}

+ 4 - 3
src/OpenSheetMusicDisplay/Cursor.ts

@@ -8,7 +8,7 @@ import {GraphicalMusicSheet} from "../MusicalScore/Graphical/GraphicalMusicSheet
 import {Instrument} from "../MusicalScore/Instrument";
 import {Note} from "../MusicalScore/VoiceData/Note";
 import {EngravingRules, Fraction} from "..";
-import {SourceMeasure} from "../MusicalScore";
+import {SourceMeasure, StaffLine} from "../MusicalScore";
 
 /**
  * A cursor which can iterate through the music sheet.
@@ -101,10 +101,11 @@ export class Cursor {
     const gse: VexFlowStaffEntry =
           gseArr.sort((a, b) => a.PositionAndShape.AbsolutePosition.x <= b.PositionAndShape.AbsolutePosition.x ? -1 : 1 )[0];
     x = gse.PositionAndShape.AbsolutePosition.x;
-    const musicSystem: MusicSystem = gse.parentMeasure.parentMusicSystem;
+    const musicSystem: MusicSystem = gse.parentMeasure.ParentMusicSystem;
     y = musicSystem.PositionAndShape.AbsolutePosition.y + musicSystem.StaffLines[0].PositionAndShape.RelativePosition.y;
+    const bottomStaffline: StaffLine = musicSystem.StaffLines[musicSystem.StaffLines.length - 1];
     const endY: number = musicSystem.PositionAndShape.AbsolutePosition.y +
-      musicSystem.StaffLines[musicSystem.StaffLines.length - 1].PositionAndShape.RelativePosition.y + 4.0;
+    bottomStaffline.PositionAndShape.RelativePosition.y + bottomStaffline.StaffHeight;
     height = endY - y;
 
     // The following code is not necessary (for now, but it could come useful later):