Browse Source

feat(Tabs): Add Slides, Bends, Hammer-on and Pull-offs (PR #839)

code and PR by momolarson

needs a few refactors and polishes to merge to develop.

squashed commits:

* trying to add bends

* fixed lint errors

* made bends array, check for release

* added proper phrase text

* package.json changes

* fixed bends and added vibrato

* changes for adding a tie

* hammerons, pull offs and slides

* missing file

* calculate slide up and down

* fixed number
Gregg Larson 4 năm trước cách đây
mục cha
commit
8de1400ce0

+ 10 - 0
src/Common/Enums/TieTypes.ts

@@ -0,0 +1,10 @@
+/**
+ * The types of ties available
+ */
+export enum TieTypes {
+    "SIMPLE" = "",
+    "HAMMERON" = "H",
+    "PULLOFF" = "P",
+    "SLIDE" = "S",
+    "TAPPING" = "T"
+}

+ 1 - 0
src/Common/Enums/index.ts

@@ -3,3 +3,4 @@
 export * from "./FontStyles";
 export * from "./Fonts";
 export * from "./TextAlignment";
+export * from "./TieTypes";

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

@@ -21,6 +21,9 @@ export class GraphicalTie {
     public get StartNote(): GraphicalNote {
         return this.startNote;
     }
+    public get Tie(): Tie {
+        return this.tie;
+    }
     public set StartNote(value: GraphicalNote) {
         this.startNote = value;
     }

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

@@ -438,7 +438,7 @@ export abstract class MusicSheetCalculator {
      * @param tie
      * @param tieIsAtSystemBreak
      */
-    protected layoutGraphicalTie(tie: GraphicalTie, tieIsAtSystemBreak: boolean): void {
+    protected layoutGraphicalTie(tie: GraphicalTie, tieIsAtSystemBreak: boolean, isTab: boolean): void {
         throw new Error("abstract, not implemented");
     }
 
@@ -2436,7 +2436,7 @@ export abstract class MusicSheetCalculator {
                                     graphicalTie.StartNote.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentStaffLine !==
                                     graphicalTie.EndNote.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentStaffLine
                                 );
-                                this.layoutGraphicalTie(graphicalTie, tieIsAtSystemBreak);
+                                this.layoutGraphicalTie(graphicalTie, tieIsAtSystemBreak, measure.ParentStaff.isTab);
                             }
                         }
                     }

+ 30 - 21
src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts

@@ -544,6 +544,7 @@ export class VexFlowConverter {
      */
     public static CreateTabNote(gve: GraphicalVoiceEntry): Vex.Flow.TabNote {
         const tabPositions: {str: number, fret: number}[] = [];
+        const notes: GraphicalNote[] = gve.notes.reverse();
         const tabPhrases: { type: number, text: string, width: number }[] = [];
         const frac: Fraction = gve.notes[0].graphicalNoteLength;
         const isTuplet: boolean = gve.notes[0].sourceNote.NoteTuplet !== undefined;
@@ -554,26 +555,33 @@ export class VexFlowConverter {
             const tabNote: TabNote = note.sourceNote as TabNote;
             const tabPosition: {str: number, fret: number} = {str: tabNote.StringNumber, fret: tabNote.FretNumber};
             tabPositions.push(tabPosition);
-            tabNote.BendArray.forEach( function( bend: {bendalter: number, direction: string} ): void {
-                let phraseText: string;
-                const phraseStep: number = bend.bendalter - tabPosition.fret;
-                if (phraseStep > 1) {
-                    phraseText = "Full";
-                } else if (phraseStep === 1) {
-                    phraseText = "1/2";
-                } else {
-                    phraseText = "1/4";
-                }
-                if (bend.direction === "up") {
-                    tabPhrases.push({type: Vex.Flow.Bend.UP, text: phraseText, width: 10});
-                } else {
-                    tabPhrases.push({type: Vex.Flow.Bend.DOWN, text: phraseText, width: 10});
-                }
-            });
+            if (tabNote.BendArray) {
+                tabNote.BendArray.forEach( function( bend: {bendalter: number, direction: string} ): void {
+                    let phraseText: string;
+                    const phraseStep: number = bend.bendalter - tabPosition.fret;
+                    if (phraseStep > 1) {
+                        phraseText = "Full";
+                    } else if (phraseStep === 1) {
+                        phraseText = "1/2";
+                    } else {
+                        phraseText = "1/4";
+                    }
+                    if (bend.direction === "up") {
+                        tabPhrases.push({type: Vex.Flow.Bend.UP, text: phraseText, width: 10});
+                    } else {
+                        tabPhrases.push({type: Vex.Flow.Bend.DOWN, text: phraseText, width: 10});
+                    }
+                });
+            }
+
+          /*  if (tabNote.NoteTie) {
+                tabTies = tabNote.NoteTie;
+            } */
 
             if (tabNote.VibratoStroke) {
                 tabVibrato = true;
             }
+
             if (numDots < note.numberOfDots) {
                 numDots = note.numberOfDots;
             }
@@ -581,10 +589,16 @@ export class VexFlowConverter {
         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,
         });
+
+        for (let i: number = 0, len: number = notes.length; i < len; i += 1) {
+            (notes[i] as VexFlowGraphicalNote).setIndex(vfnote, i);
+        }
+
         tabPhrases.forEach(function(phrase: { type: number, text: string, width: number }): void {
             if (phrase.type === Vex.Flow.Bend.UP) {
                 vfnote.addModifier (new Vex.Flow.Bend(phrase.text, false));
@@ -593,11 +607,6 @@ export class VexFlowConverter {
             }
         });
         // does not work well to add phrases as array
-        /*
-        if (tabPhrases.length > 0) {
-           vfnote.addModifier (new Vex.Flow.Bend(undefined, undefined, tabPhrases), 1);
-        }
-        */
         if (tabVibrato) {
             vfnote.addModifier(new Vex.Flow.Vibrato());
         }

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

@@ -30,7 +30,7 @@ export class VexFlowGraphicalNote extends GraphicalNote {
     // 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)
-    public vfnote: [Vex.Flow.StaveNote, number];
+    public vfnote: [Vex.Flow.StemmableNote, number];
     // The current clef
     private clef: ClefInstruction;
 
@@ -67,7 +67,7 @@ export class VexFlowGraphicalNote extends GraphicalNote {
      * @param note
      * @param index
      */
-    public setIndex(note: Vex.Flow.StaveNote, index: number): void {
+    public setIndex(note: Vex.Flow.StemmableNote, index: number): void {
         this.vfnote = [note, index];
     }
 

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

@@ -1161,7 +1161,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
         for ( const vfStaffEntry of this.staffEntries ) {
             for ( const gVoiceEntry of vfStaffEntry.graphicalVoiceEntries) {
                 for ( const gnote of gVoiceEntry.notes) {
-                    const vfnote: [StaveNote, number] = (gnote as VexFlowGraphicalNote).vfnote;
+                    const vfnote: [StemmableNote , number] = (gnote as VexFlowGraphicalNote).vfnote;
                     if (!vfnote || !vfnote[0]) {
                         continue;
                     }

+ 51 - 9
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -51,6 +51,7 @@ import { EngravingRules } from "../EngravingRules";
 import { VexflowStafflineNoteCalculator } from "./VexflowStafflineNoteCalculator";
 import { NoteTypeHandler } from "../../VoiceData/NoteType";
 import { VexFlowConverter } from "./VexFlowConverter";
+import { TabNote } from "../../VoiceData/TabNote";
 
 export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
   /** space needed for a dash for lyrics spacing, calculated once */
@@ -136,6 +137,13 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
       softmaxFactor: this.rules.SoftmaxFactorVexFlow // this setting is only applied in Vexflow 3.x. also this needs @types/vexflow ^3.0.0
     });
 
+    /*
+    {
+      // maxIterations: 2,
+      softmaxFactor: this.rules.SoftmaxFactorVexFlow // this setting is only applied in Vexflow 3.x. also this needs @types/vexflow ^3.0.0
+    }
+    */
+
     for (const measure of measures) {
       if (!measure) {
         continue;
@@ -467,18 +475,18 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
    * @param tie
    * @param tieIsAtSystemBreak
    */
-  protected layoutGraphicalTie(tie: GraphicalTie, tieIsAtSystemBreak: boolean): void {
+  protected layoutGraphicalTie(tie: GraphicalTie, tieIsAtSystemBreak: boolean, isTab: boolean): void {
     const startNote: VexFlowGraphicalNote = (tie.StartNote as VexFlowGraphicalNote);
     const endNote: VexFlowGraphicalNote = (tie.EndNote as VexFlowGraphicalNote);
 
-    let vfStartNote: Vex.Flow.StaveNote = undefined;
+    let vfStartNote: Vex.Flow.StemmableNote  = undefined;
     let startNoteIndexInTie: number = 0;
     if (startNote && startNote.vfnote && startNote.vfnote.length >= 2) {
       vfStartNote = startNote.vfnote[0];
       startNoteIndexInTie = startNote.vfnote[1];
     }
 
-    let vfEndNote: Vex.Flow.StaveNote = undefined;
+    let vfEndNote: Vex.Flow.StemmableNote  = undefined;
     let endNoteIndexInTie: number = 0;
     if (endNote && endNote.vfnote && endNote.vfnote.length >= 2) {
       vfEndNote = endNote.vfnote[0];
@@ -507,12 +515,46 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     } else {
       // normal case
       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
-        });
+        let vfTie: any;
+        if (isTab) {
+          if (tie.Tie.Type === "S") {
+            //calculate direction
+            const startTieNote: TabNote = <TabNote> tie.StartNote.sourceNote;
+            const endTieNote: TabNote = <TabNote> tie.EndNote.sourceNote;
+            let slideDirection: number = 1;
+            if (startTieNote.FretNumber > endTieNote.FretNumber) {
+              slideDirection = -1;
+            }
+            vfTie = new Vex.Flow.TabSlide(
+              {
+                first_indices: [startNoteIndexInTie],
+                first_note: vfStartNote,
+                last_indices: [endNoteIndexInTie],
+                last_note: vfEndNote,
+              },
+              slideDirection
+            );
+          } else {
+            vfTie = new Vex.Flow.TabTie(
+              {
+                first_indices: [startNoteIndexInTie],
+                first_note: vfStartNote,
+                last_indices: [endNoteIndexInTie],
+                last_note: vfEndNote,
+              },
+              tie.Tie.Type
+            );
+          }
+
+        } else {
+          vfTie = 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);
       }

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

@@ -8,6 +8,7 @@ import { SourceMeasure } from "../VoiceData/SourceMeasure";
 import { SourceStaffEntry } from "../VoiceData/SourceStaffEntry";
 import { Beam } from "../VoiceData/Beam";
 import { Tie } from "../VoiceData/Tie";
+import { TieTypes } from "../../Common/Enums/";
 import { Tuplet } from "../VoiceData/Tuplet";
 import { Fraction } from "../../Common/DataObjects/Fraction";
 import { IXmlElement } from "../../Common/FileIO/Xml";
@@ -190,9 +191,23 @@ export class VoiceGenerator {
         // check for Ties - must be the last check
         const tiedNodeList: IXmlElement[] = notationNode.elements("tied");
         if (tiedNodeList.length > 0) {
-          this.addTie(tiedNodeList, measureStartAbsoluteTimestamp, maxTieNoteFraction);
+          this.addTie(tiedNodeList, measureStartAbsoluteTimestamp, maxTieNoteFraction, TieTypes.SIMPLE);
+        }
+        //check for slides, they are the same as Ties but with a different connection
+        const slideNodeList: IXmlElement[] = notationNode.elements("slide");
+        if (slideNodeList.length > 0) {
+          this.addTie(slideNodeList, measureStartAbsoluteTimestamp, maxTieNoteFraction, TieTypes.SLIDE);
+        }
+        //check for slides, they are the same as Ties but with a different connection
+        const technicalNode: IXmlElement = notationNode.element("technical");
+        const hammerNodeList: IXmlElement[] = technicalNode.elements("hammer-on");
+        if (hammerNodeList.length > 0) {
+          this.addTie(hammerNodeList, measureStartAbsoluteTimestamp, maxTieNoteFraction, TieTypes.HAMMERON);
+        }
+        const pulloffNodeList: IXmlElement[] = technicalNode.elements("pull-off");
+        if (pulloffNodeList.length > 0) {
+          this.addTie(pulloffNodeList, measureStartAbsoluteTimestamp, maxTieNoteFraction, TieTypes.PULLOFF);
         }
-
         // remove open ties, if there is already a gap between the last tie note and now.
         const openTieDict: { [_: number]: Tie; } = this.openTieDict;
         for (const key in openTieDict) {
@@ -757,7 +772,7 @@ export class VoiceGenerator {
     }
   }
 
-  private addTie(tieNodeList: IXmlElement[], measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction): void {
+  private addTie(tieNodeList: IXmlElement[], measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, tieType: TieTypes): void {
     if (tieNodeList) {
       if (tieNodeList.length === 1) {
         const tieNode: IXmlElement = tieNodeList[0];
@@ -770,7 +785,7 @@ export class VoiceGenerator {
                 delete this.openTieDict[num];
               }
               const newTieNumber: number = this.getNextAvailableNumberForTie();
-              const tie: Tie = new Tie(this.currentNote);
+              const tie: Tie = new Tie(this.currentNote, tieType);
               this.openTieDict[newTieNumber] = tie;
             } else if (type === "stop") {
               const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
@@ -830,8 +845,14 @@ export class VoiceGenerator {
     for (const key in openTieDict) {
       if (openTieDict.hasOwnProperty(key)) {
         const tie: Tie = openTieDict[key];
+        const tieTabNote: TabNote = tie.Notes[0] as TabNote;
+        const tieCandidateNote: TabNote = candidateNote as TabNote;
         if (tie.Pitch.FundamentalNote === candidateNote.Pitch.FundamentalNote && tie.Pitch.Octave === candidateNote.Pitch.Octave) {
           return +key;
+        } else {
+          if (tieTabNote.StringNumber === tieCandidateNote.StringNumber) {
+            return +key;
+          }
         }
       }
     }

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

@@ -1,22 +1,29 @@
 import {Note} from "./Note";
 import { Fraction } from "../../Common/DataObjects/Fraction";
 import { Pitch } from "../../Common/DataObjects/Pitch";
+import { TieTypes } from "../../Common/Enums/";
 
 /**
  * A [[Tie]] connects two notes of the same pitch and name, indicating that they have to be played as a single note.
  */
 export class Tie {
 
-    constructor(note: Note) {
+    constructor(note: Note, type: TieTypes) {
         this.AddNote(note);
+        this.type = type;
     }
 
     private notes: Note[] = [];
+    private type: TieTypes;
 
     public get Notes(): Note[] {
         return this.notes;
     }
 
+    public get Type(): TieTypes {
+        return this.type;
+    }
+
     public get StartNote(): Note {
         return this.notes[0];
     }