Sfoglia il codice sorgente

feat(cue notes): add cue notes as smaller notes. fix(gracenotes): grace notes can have articulations

cue notes for now are simply StaveNotes with smaller scale like Grace Notes.
One-line code to make them grace notes with smaller stem, modifiers etc. is already there, just needs a small fix/PR in Vexflow.

grace notes are now set as gve.vfstavenote in the VexflowVoiceEntry, so that they don't need to be skipped for createArticulations anymore

resolves #349
sschmidTU 6 anni fa
parent
commit
0094f1f1dd

+ 3 - 0
external/vexflow/vexflow.d.ts

@@ -4,6 +4,7 @@ declare namespace Vex {
 
     export module Flow {
         const RESOLUTION: any;
+        const DEFAULT_NOTATION_FONT_SCALE: number;
 
         export class Formatter {
             constructor();
@@ -127,6 +128,8 @@ declare namespace Vex {
         }
 
         export class GraceNote extends StaveNote {
+            static SCALE: number;
+            static LEDGER_LINE_OFFSET: number;
             constructor(note_struct: any);
         }
 

+ 12 - 3
src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts

@@ -262,10 +262,16 @@ export class VexFlowConverter {
             slash: gve.parentVoiceEntry.GraceNoteSlash,
         };
 
-        if (!gve.parentVoiceEntry.IsGrace) {
-            vfnote = new Vex.Flow.StaveNote(vfnoteStruct);
-        } else {
+        if (gve.notes[0].sourceNote.IsCueNote) {
+            (<any>vfnoteStruct).glyph_font_scale = Vex.Flow.DEFAULT_NOTATION_FONT_SCALE * Vex.Flow.GraceNote.SCALE;
+            (<any>vfnoteStruct).stroke_px = Vex.Flow.GraceNote.LEDGER_LINE_OFFSET;
+        }
+
+        // if (gve.parentVoiceEntry.IsGrace || gve.notes[0].sourceNote.IsCueNote) { // needs Vexflow PR
+        if (gve.parentVoiceEntry.IsGrace) {
             vfnote = new Vex.Flow.GraceNote(vfnoteStruct);
+        } else {
+            vfnote = new Vex.Flow.StaveNote(vfnoteStruct);
         }
 
         vfnote.x_shift = xShift;
@@ -306,6 +312,9 @@ export class VexFlowConverter {
     }
 
     public static generateArticulations(vfnote: Vex.Flow.StemmableNote, articulations: ArticulationEnum[]): void {
+        if (vfnote === undefined) {
+            return; // needed because grace notes after main note currently not implemented. maybe safer in any case
+        }
         // Articulations:
         let vfArtPosition: number = Vex.Flow.Modifier.Position.ABOVE;
 

+ 8 - 5
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -486,6 +486,9 @@ export class VexFlowMeasure extends GraphicalMeasure {
      * @param beam
      */
     public handleBeam(graphicalNote: GraphicalNote, beam: Beam): void {
+        if (graphicalNote.parentVoiceEntry.parentVoiceEntry.IsGrace) {
+            return; // grace note beams are handled by Vexflow.GraceNoteGroup
+        }
         const voiceID: number = graphicalNote.sourceNote.ParentVoiceEntry.ParentVoice.VoiceId;
         let beams: [Beam, VexFlowVoiceEntry[]][] = this.beams[voiceID];
         if (beams === undefined) {
@@ -644,8 +647,11 @@ export class VexFlowMeasure extends GraphicalMeasure {
                 if (graceGVoiceEntriesBefore.length > 0) {
                     const graceNotes: Vex.Flow.GraceNote[] = [];
                     for (let i: number = 0; i < graceGVoiceEntriesBefore.length; i++) {
-                        if (graceGVoiceEntriesBefore[i].notes[0].sourceNote.PrintObject) {
-                            graceNotes.push(VexFlowConverter.StaveNote(graceGVoiceEntriesBefore[i]));
+                        const gveGrace: VexFlowVoiceEntry = <VexFlowVoiceEntry>graceGVoiceEntriesBefore[i];
+                        if (gveGrace.notes[0].sourceNote.PrintObject) {
+                            const vfStaveNote: StaveNote = VexFlowConverter.StaveNote(gveGrace);
+                            gveGrace.vfStaveNote = vfStaveNote;
+                            graceNotes.push(vfStaveNote);
                         }
                     }
                     const graceNoteGroup: Vex.Flow.GraceNoteGroup = new Vex.Flow.GraceNoteGroup(graceNotes, graceSlur);
@@ -752,9 +758,6 @@ export class VexFlowMeasure extends GraphicalMeasure {
             // create vex flow articulation:
             const graphicalVoiceEntries: GraphicalVoiceEntry[] = graphicalStaffEntry.graphicalVoiceEntries;
             for (const gve of graphicalVoiceEntries) {
-                if (gve.parentVoiceEntry.IsGrace) {
-                    continue;
-                }
                 const vfStaveNote: StemmableNote = (gve as VexFlowVoiceEntry).vfStaveNote;
                 VexFlowConverter.generateArticulations(vfStaveNote, gve.notes[0].sourceNote.ParentVoiceEntry.Articulations);
             }

+ 73 - 65
src/MusicalScore/ScoreIO/InstrumentReader.ts

@@ -211,6 +211,18 @@ export class InstrumentReader {
             }
           }
 
+          // check for cue note
+          let isCueNote: boolean = false;
+          const typeNode: IXmlElement = xmlNode.element("type");
+          if (typeNode !== undefined) {
+            const sizeAttr: Attr = typeNode.attribute("size");
+            if (sizeAttr !== undefined && sizeAttr !== null) {
+              if (sizeAttr.value === "cue") {
+                isCueNote = true;
+              }
+            }
+          }
+
           let musicTimestamp: Fraction = currentFraction.clone();
           if (isChord) {
             musicTimestamp = previousFraction.clone();
@@ -264,7 +276,7 @@ export class InstrumentReader {
             xmlNode, noteDuration, restNote,
             this.currentStaffEntry, this.currentMeasure,
             measureStartAbsoluteTimestamp,
-            this.maxTieNoteFraction, isChord, guitarPro, printObject
+            this.maxTieNoteFraction, isChord, guitarPro, printObject, isCueNote
           );
 
           const notationsNode: IXmlElement = xmlNode.element("notations");
@@ -568,12 +580,10 @@ export class InstrumentReader {
    * @returns {Fraction}
    */
   private getNoteDurationFromTypeNode(xmlNode: IXmlElement): Fraction {
-    if (xmlNode.element("type") !== undefined) {
-      const typeNode: IXmlElement = xmlNode.element("type");
-      if (typeNode !== undefined) {
-        const type: string = typeNode.value;
-        return this.currentVoiceGenerator.getNoteDurationFromType(type);
-      }
+    const typeNode: IXmlElement = xmlNode.element("type");
+    if (typeNode !== undefined) {
+      const type: string = typeNode.value;
+      return this.currentVoiceGenerator.getNoteDurationFromType(type);
     }
     return new Fraction(0, 4 * this.divisions);
   }
@@ -1037,65 +1047,63 @@ export class InstrumentReader {
       for (let idx: number = 0, len: number = xmlMeasureListArr.length; idx < len; ++idx) {
         const xmlNode: IXmlElement = xmlMeasureListArr[idx];
         if (xmlNode.name === "note" && xmlNode.element("time-modification") === undefined) {
-          if (xmlNode.element("duration") !== undefined && xmlNode.element("type") !== undefined) {
-            const durationNode: IXmlElement = xmlNode.element("duration");
-            const typeNode: IXmlElement = xmlNode.element("type");
-            if (durationNode !== undefined && typeNode !== undefined) {
-              const type: string = typeNode.value;
-              let noteDuration: number = 0;
-              try {
-                noteDuration = parseInt(durationNode.value, 10);
-              } catch (ex) {
-                log.debug("InstrumentReader.readDivisionsFromNotes", ex);
-                continue;
-              }
+          const durationNode: IXmlElement = xmlNode.element("duration");
+          const typeNode: IXmlElement = xmlNode.element("type");
+          if (durationNode !== undefined && typeNode !== undefined) {
+            const type: string = typeNode.value;
+            let noteDuration: number = 0;
+            try {
+              noteDuration = parseInt(durationNode.value, 10);
+            } catch (ex) {
+              log.debug("InstrumentReader.readDivisionsFromNotes", ex);
+              continue;
+            }
 
-              switch (type) {
-                case "1024th":
-                  divisionsFromNote = (noteDuration / 4) * 1024;
-                  break;
-                case "512th":
-                  divisionsFromNote = (noteDuration / 4) * 512;
-                  break;
-                case "256th":
-                  divisionsFromNote = (noteDuration / 4) * 256;
-                  break;
-                case "128th":
-                  divisionsFromNote = (noteDuration / 4) * 128;
-                  break;
-                case "64th":
-                  divisionsFromNote = (noteDuration / 4) * 64;
-                  break;
-                case "32nd":
-                  divisionsFromNote = (noteDuration / 4) * 32;
-                  break;
-                case "16th":
-                  divisionsFromNote = (noteDuration / 4) * 16;
-                  break;
-                case "eighth":
-                  divisionsFromNote = (noteDuration / 4) * 8;
-                  break;
-                case "quarter":
-                  divisionsFromNote = (noteDuration / 4) * 4;
-                  break;
-                case "half":
-                  divisionsFromNote = (noteDuration / 4) * 2;
-                  break;
-                case "whole":
-                  divisionsFromNote = (noteDuration / 4);
-                  break;
-                case "breve":
-                  divisionsFromNote = (noteDuration / 4) / 2;
-                  break;
-                case "long":
-                  divisionsFromNote = (noteDuration / 4) / 4;
-                  break;
-                case "maxima":
-                  divisionsFromNote = (noteDuration / 4) / 8;
-                  break;
-                default:
-                  break;
-              }
+            switch (type) {
+              case "1024th":
+                divisionsFromNote = (noteDuration / 4) * 1024;
+                break;
+              case "512th":
+                divisionsFromNote = (noteDuration / 4) * 512;
+                break;
+              case "256th":
+                divisionsFromNote = (noteDuration / 4) * 256;
+                break;
+              case "128th":
+                divisionsFromNote = (noteDuration / 4) * 128;
+                break;
+              case "64th":
+                divisionsFromNote = (noteDuration / 4) * 64;
+                break;
+              case "32nd":
+                divisionsFromNote = (noteDuration / 4) * 32;
+                break;
+              case "16th":
+                divisionsFromNote = (noteDuration / 4) * 16;
+                break;
+              case "eighth":
+                divisionsFromNote = (noteDuration / 4) * 8;
+                break;
+              case "quarter":
+                divisionsFromNote = (noteDuration / 4) * 4;
+                break;
+              case "half":
+                divisionsFromNote = (noteDuration / 4) * 2;
+                break;
+              case "whole":
+                divisionsFromNote = (noteDuration / 4);
+                break;
+              case "breve":
+                divisionsFromNote = (noteDuration / 4) / 2;
+                break;
+              case "long":
+                divisionsFromNote = (noteDuration / 4) / 4;
+                break;
+              case "maxima":
+                divisionsFromNote = (noteDuration / 4) / 8;
+                break;
+              default:
+                break;
             }
           }
         }

+ 4 - 4
src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts

@@ -72,7 +72,7 @@ export class ExpressionReader {
         }
 
         const placeAttr: IXmlAttribute = xmlNode.attribute("placement");
-        if (placeAttr !== undefined) {
+        if (placeAttr !== undefined && placeAttr !== null) {
             try {
                 const placementString: string = placeAttr.value;
                 if (placementString === "below") {
@@ -96,21 +96,21 @@ export class ExpressionReader {
                     const dynamicsNode: IXmlElement = directionTypeNode.element("dynamics");
                     if (dynamicsNode !== undefined) {
                         const defAttr: IXmlAttribute = dynamicsNode.attribute("default-y");
-                        if (defAttr !== undefined) {
+                        if (defAttr !== undefined && defAttr !== null) {
                             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) {
+                        if (defAttr !== undefined && defAttr !== null) {
                             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) {
+                        if (defAttr !== undefined && defAttr !== null) {
                             this.readExpressionPlacement(defAttr, "read words y pos");
                         }
                     }

+ 8 - 6
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -105,14 +105,14 @@ export class VoiceGenerator {
   public read(noteNode: IXmlElement, noteDuration: Fraction, restNote: boolean,
               parentStaffEntry: SourceStaffEntry, parentMeasure: SourceMeasure,
               measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, chord: boolean, guitarPro: boolean,
-              printObject: boolean = true): Note {
+              printObject: boolean = true, isCueNote: boolean = false): Note {
     this.currentStaffEntry = parentStaffEntry;
     this.currentMeasure = parentMeasure;
     //log.debug("read called:", restNote);
     try {
       this.currentNote = restNote
-        ? this.addRestNote(noteDuration, printObject)
-        : this.addSingleNote(noteNode, noteDuration, chord, guitarPro, printObject);
+        ? this.addRestNote(noteDuration, printObject, isCueNote)
+        : this.addSingleNote(noteNode, noteDuration, chord, guitarPro, printObject, isCueNote);
       // read lyrics
       const lyricElements: IXmlElement[] = noteNode.elements("lyric");
       if (this.lyricsReader !== undefined && lyricElements !== undefined) {
@@ -297,7 +297,7 @@ export class VoiceGenerator {
    * @returns {Note}
    */
   private addSingleNote(node: IXmlElement, noteDuration: Fraction, chord: boolean, guitarPro: boolean,
-                        printObject: boolean = true): Note {
+                        printObject: boolean = true, isCueNote: boolean = false): Note {
     //log.debug("addSingleNote called");
     let noteAlter: number = 0;
     let noteAccidental: AccidentalEnum = AccidentalEnum.NONE;
@@ -391,10 +391,11 @@ export class VoiceGenerator {
     const noteLength: Fraction = Fraction.createFromFraction(noteDuration);
     const note: Note = new Note(this.currentVoiceEntry, this.currentStaffEntry, noteLength, pitch);
     note.PrintObject = printObject;
+    note.IsCueNote = isCueNote;
     note.PlaybackInstrumentId = playbackInstrumentId;
     if (noteHeadShapeXml !== undefined && noteHeadShapeXml !== "normal") {
       note.NoteHead = new NoteHead(note, noteHeadShapeXml, noteHeadFilledXml);
-    } // if normal, leave note head undefined to save performance
+    } // if normal, leave note head undefined to save processing/runtime
     this.currentVoiceEntry.Notes.push(note);
     if (node.elements("beam") && !chord) {
       this.createBeam(node, note);
@@ -408,10 +409,11 @@ export class VoiceGenerator {
    * @param divisions
    * @returns {Note}
    */
-  private addRestNote(noteDuration: Fraction, printObject: boolean = true): Note {
+  private addRestNote(noteDuration: Fraction, printObject: boolean = true, isCueNote: boolean = false): Note {
     const restFraction: Fraction = Fraction.createFromFraction(noteDuration);
     const restNote: Note = new Note(this.currentVoiceEntry, this.currentStaffEntry, restFraction, undefined);
     restNote.PrintObject = printObject;
+    restNote.IsCueNote = isCueNote;
     this.currentVoiceEntry.Notes.push(restNote);
     if (this.openBeam !== undefined) {
       this.openBeam.ExtendedNoteList.push(restNote);

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

@@ -47,6 +47,8 @@ export class Note {
     private noteHead: NoteHead = undefined;
     /** States whether the note should be displayed. False if xmlNode.attribute("print-object").value = "no". */
     private printObject: boolean = true;
+    /** States whether this is a cue note (Stichnote) (smaller size). */
+    private isCueNote: boolean;
 
 
     public get ParentVoiceEntry(): VoiceEntry {
@@ -109,10 +111,15 @@ export class Note {
     public get PrintObject(): boolean {
         return this.printObject;
     }
-
     public set PrintObject(value: boolean) {
         this.printObject = value;
     }
+    public get IsCueNote(): boolean {
+        return this.isCueNote;
+    }
+    public set IsCueNote(value: boolean) {
+        this.isCueNote = value;
+    }
 
     public isRest(): boolean {
         return this.Pitch === undefined;