소스 검색

update to osmd 1.4.3: fix all tie notes played, notes missing tie, add SVG groups/classes for clef/keysignature/timesignature, etc

sschmidTU 3 년 전
부모
커밋
e0a215e87c

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "osmd-extended",
-  "version": "1.4.2",
+  "version": "1.4.3",
   "description": "Private / sponsor exclusive OSMD mirror/audio player.",
   "main": "build/opensheetmusicdisplay.min.js",
   "types": "build/dist/src/OpenSheetMusicDisplay/index.d.ts",

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

@@ -1,5 +1,6 @@
 import {Tie} from "../VoiceData/Tie";
 import {GraphicalNote} from "./GraphicalNote";
+import Vex from "vexflow";
 
 /**
  * The graphical counterpart of a [[Tie]].
@@ -8,6 +9,7 @@ export class GraphicalTie {
     private tie: Tie;
     private startNote: GraphicalNote;
     private endNote: GraphicalNote;
+    public vfTie: Vex.Flow.StaveTie;
 
     constructor(tie: Tie, start: GraphicalNote = undefined, end: GraphicalNote = undefined) {
         this.tie = tie;
@@ -15,6 +17,10 @@ export class GraphicalTie {
         this.endNote = end;
     }
 
+    public get SVGElement(): HTMLElement {
+        return (this.vfTie as any).getAttribute("el");
+    }
+
     public get GetTie(): Tie {
         return this.tie;
     }

+ 18 - 4
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -827,9 +827,11 @@ export abstract class MusicSheetCalculator {
 
         // build the MusicSystems
         let musicSystemBuilder: MusicSystemBuilder;
-        if (this.rules.RenderSingleHorizontalStaffline) {
+        const measureCount: number = allMeasures.length;
+        if (measureCount === 1 || this.rules.RenderSingleHorizontalStaffline) {
             musicSystemBuilder = new MusicSystemBuilder();
-            // JustifiedMusicSystemBuilder makes measures way too large with this option.
+            // JustifiedMusicSystemBuilder makes measures way too large with
+            //   only one measure or RenderSingleHorizontalStaffline.
         } else {
             musicSystemBuilder = new JustifiedMusicSystemBuilder();
         }
@@ -2142,13 +2144,25 @@ export abstract class MusicSheetCalculator {
                         const startStaffEntry: GraphicalStaffEntry = this.graphicalMusicSheet.findGraphicalStaffEntryFromMeasureList(
                             staffIndex, measureIndex, sourceStaffEntry
                         );
+                        if (startStaffEntry) {
+                            startStaffEntry.GraphicalTies.clear(); // don't duplicate ties when calling render() again
+                            startStaffEntry.ties.clear();
+                        }
+
                         for (let idx: number = 0, len: number = sourceStaffEntry.VoiceEntries.length; idx < len; ++idx) {
                             const voiceEntry: VoiceEntry = sourceStaffEntry.VoiceEntries[idx];
                             for (let idx2: number = 0, len2: number = voiceEntry.Notes.length; idx2 < len2; ++idx2) {
                                 const note: Note = voiceEntry.Notes[idx2];
-                                if (note.NoteTie &&
-                                    note === note.NoteTie.StartNote) {
+                                if (note.NoteTie) {
                                     const tie: Tie = note.NoteTie;
+                                    if (note === note.NoteTie.Notes.last()) {
+                                        continue; // nothing to do on last note. don't create last tie twice.
+                                    }
+                                    for (const gTie of startStaffEntry.GraphicalTies) {
+                                        if (gTie.Tie === tie) {
+                                            continue; // don't handle the same tie on the same startStaffEntry twice
+                                        }
+                                    }
                                     this.handleTie(tie, startStaffEntry, staffIndex, measureIndex);
                                 }
                             }

+ 6 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -35,6 +35,7 @@ import {AutoBeamOptions} from "../../../OpenSheetMusicDisplay/OSMDOptions";
 import {SkyBottomLineCalculator} from "../SkyBottomLineCalculator";
 import { NoteType } from "../../VoiceData/NoteType";
 import { Arpeggio } from "../../VoiceData/Arpeggio";
+import { GraphicalTie } from "../GraphicalTie";
 
 // type StemmableNote = Vex.Flow.StemmableNote;
 
@@ -1570,6 +1571,11 @@ export class VexFlowMeasure extends GraphicalMeasure {
         this.beginInstructionsWidth = (vfBeginInstructionsWidth ?? 0) / unitInPixels;
         this.endInstructionsWidth = (vfEndInstructionsWidth ?? 0) / unitInPixels;
     }
+
+    public addStaveTie(stavetie: Vex.Flow.StaveTie, graphicalTie: GraphicalTie): void {
+        this.vfTies.push(stavetie);
+        graphicalTie.vfTie = stavetie;
+    }
 }
 
 // Gives the position of the Stave - replaces the function get Position() in the description of class StaveModifier in vexflow.d.ts

+ 6 - 5
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -626,7 +626,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
           first_note: vfStartNote
         });
         const measure1: VexFlowMeasure = (startNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
-        measure1.vfTies.push(vfTie1);
+        measure1.addStaveTie(vfTie1, tie);
       }
 
       if (vfEndNote) {
@@ -635,7 +635,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
           last_note: vfEndNote
         });
         const measure2: VexFlowMeasure = (endNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
-        measure2.vfTies.push(vfTie2);
+        measure2.addStaveTie(vfTie2, tie);
       }
     } else {
       // normal case
@@ -678,15 +678,16 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
             last_indices: [endNoteIndexInTie],
             last_note: vfEndNote
           });
-          if (tie.Tie.TieDirection === PlacementEnum.Below) {
+          const tieDirection: PlacementEnum = tie.Tie.getTieDirection(startNote.sourceNote);
+          if (tieDirection === PlacementEnum.Below) {
             vfTie.setDirection(1); // + is down in vexflow
-          } else if (tie.Tie.TieDirection === PlacementEnum.Above) {
+          } else if (tieDirection === PlacementEnum.Above) {
             vfTie.setDirection(-1);
           }
         }
 
         const measure: VexFlowMeasure = (endNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
-        measure.vfTies.push(vfTie);
+        measure.addStaveTie(vfTie, tie);
       }
     }
   }

+ 40 - 34
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -989,35 +989,12 @@ export class VoiceGenerator {
 
   private addTie(tieNodeList: IXmlElement[], measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, tieType: TieTypes): void {
     if (tieNodeList) {
-      for (let i: number = 0; i < tieNodeList.length; i++) {
-        const tieNode: IXmlElement = tieNodeList[i];
+      if (tieNodeList.length === 1) {
+        const tieNode: IXmlElement = tieNodeList[0];
         if (tieNode !== undefined && tieNode.attributes()) {
-          let tieDirection: PlacementEnum = PlacementEnum.NotYetDefined;
-          // read tie direction/placement from XML
-          const placementAttr: IXmlAttribute = tieNode.attribute("placement");
-          if (placementAttr) {
-            if (placementAttr.value === "above") {
-              tieDirection = PlacementEnum.Above;
-            } else if (placementAttr.value === "below") {
-              tieDirection = PlacementEnum.Below;
-            }
-          }
-          // tie direction also be given like this:
-          const orientationAttr: IXmlAttribute = tieNode.attribute("orientation");
-          if (orientationAttr) {
-            if (orientationAttr.value === "over") {
-              tieDirection = PlacementEnum.Above;
-            } else if (orientationAttr.value === "under") {
-              tieDirection = PlacementEnum.Below;
-            }
-          }
+          const tieDirection: PlacementEnum = this.getTieDirection(tieNode);
 
           const type: string = tieNode.attribute("type").value;
-          if (type === "start" && i === 0) {
-            // handle this after the stop node, so that we don't start a new tie before the old one has ended.
-            tieNodeList.push(tieNode);
-            continue;
-          }
           try {
             if (type === "start") {
               const num: number = this.findCurrentNoteInTieDict(this.currentNote);
@@ -1033,7 +1010,7 @@ export class VoiceGenerator {
               const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
               const tie: Tie = this.openTieDict[tieNumber];
               if (tie) {
-                tie.AddNote(this.currentNote, false);
+                tie.AddNote(this.currentNote);
                 delete this.openTieDict[tieNumber];
               }
             }
@@ -1043,15 +1020,44 @@ export class VoiceGenerator {
           }
 
         }
+      } else if (tieNodeList.length === 2) { // stop+start
+        const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
+        if (tieNumber >= 0) {
+          const tie: Tie = this.openTieDict[tieNumber];
+          tie.AddNote(this.currentNote);
+          for (const tieNode of tieNodeList) {
+            const type: string = tieNode.attribute("type").value;
+            if (type === "start") {
+              const placement: PlacementEnum = this.getTieDirection(tieNode);
+              tie.NoteIndexToTieDirection[tie.Notes.length - 1] = placement;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  private getTieDirection(tieNode: IXmlElement): PlacementEnum {
+    let tieDirection: PlacementEnum = PlacementEnum.NotYetDefined;
+    // read tie direction/placement from XML
+    const placementAttr: IXmlAttribute = tieNode.attribute("placement");
+    if (placementAttr) {
+      if (placementAttr.value === "above") {
+        tieDirection = PlacementEnum.Above;
+      } else if (placementAttr.value === "below") {
+        tieDirection = PlacementEnum.Below;
+      }
+    }
+    // tie direction can also be given like this:
+    const orientationAttr: IXmlAttribute = tieNode.attribute("orientation");
+    if (orientationAttr) {
+      if (orientationAttr.value === "over") {
+        tieDirection = PlacementEnum.Above;
+      } else if (orientationAttr.value === "under") {
+        tieDirection = PlacementEnum.Below;
       }
-      // } else if (tieNodeList.length === 2) {
-      //   const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
-      //   if (tieNumber >= 0) {
-      //     const tie: Tie = this.openTieDict[tieNumber];
-      //     tie.AddNote(this.currentNote);
-      //   }
-      // }
     }
+    return tieDirection;
   }
 
   /**

+ 33 - 4
src/MusicalScore/VoiceData/Tie.ts

@@ -3,6 +3,7 @@ import { Fraction } from "../../Common/DataObjects/Fraction";
 import { Pitch } from "../../Common/DataObjects/Pitch";
 import { TieTypes } from "../../Common/Enums/";
 import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
+import log from "loglevel";
 
 /**
  * A [[Tie]] connects two notes of the same pitch and name, indicating that they have to be played as a single note.
@@ -18,6 +19,32 @@ export class Tie {
     private type: TieTypes;
     public TieNumber: number = 1;
     public TieDirection: PlacementEnum = PlacementEnum.NotYetDefined;
+    /** Can contain tie directions at certain note indices.
+     *  For example, if it contains {2: PlacementEnum.Below}, then
+     *  the tie should go downwards from Tie.Notes[2] onwards,
+     *  even if tie.TieDirection is PlacementEnum.Above (tie starts going up on Notes[0]).
+     */
+    public NoteIndexToTieDirection: NoteIndexToPlacementEnum = {};
+
+    public getTieDirection(startNote?: Note): PlacementEnum {
+        if (!startNote) {
+            return this.TieDirection;
+        }
+        for (let i: number = 0; i < this.Notes.length; i++) {
+            const tieNote: Note = this.Notes[i];
+            if (tieNote === startNote) {
+                const directionAtIndex: PlacementEnum = this.NoteIndexToTieDirection[i];
+                if (directionAtIndex) {
+                    return directionAtIndex;
+                } else {
+                    return this.TieDirection;
+                }
+            }
+        }
+        log.debug("tie.getTieDuration note not in tie.Notes");
+        // ^ happens in Christbaum measure 19 - probably note sharing stem
+        return this.TieDirection;
+    }
 
     public get Notes(): Note[] {
         return this.notes;
@@ -43,10 +70,12 @@ export class Tie {
         return this.StartNote.Pitch;
     }
 
-    public AddNote(note: Note, isStartNote: boolean = true): void {
+    public AddNote(note: Note): void {
         this.notes.push(note);
-        if (isStartNote) {
-            note.NoteTie = this; // be careful not to overwrite note.NoteTie wrongly, saves only one tie
-        }
+        note.NoteTie = this;
     }
 }
+
+export interface NoteIndexToPlacementEnum {
+    [key: number]: PlacementEnum;
+  }

+ 1 - 1
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -35,7 +35,7 @@ import { DynamicsCalculator } from "../MusicalScore/ScoreIO/MusicSymbolModules/D
  * After the constructor, use load() and render() to load and render a MusicXML file.
  */
 export class OpenSheetMusicDisplay {
-    private version: string = "1.4.2-audio-extended"; // getter: this.Version
+    private version: string = "1.4.3-audio-extended"; // getter: this.Version
     // at release, bump version and change to -release, afterwards to -dev again
 
     /**

+ 14 - 0
src/VexFlowPatch/readme.txt

@@ -14,8 +14,16 @@ beam.js (custom addition):
 add flat_beams, flat_beam_offset, flat_beam_offset_per_beam render_option
 able to add svg node id+class to beam
 
+clef.js (custom addition):
+open group to get SVG group+class for clef
+
+keysignature.js (custom addition):
+open group to get SVG group+class for key signature
+
 pedalmarking.js (custom addition):
 Add rendering options for pedals that break across systems.
+clef.js (custom addition):
+open group to get SVG group+class for clef
 
 stave.js (custom addition):
 prevent a bug where a modifier width is NaN, leading to a VexFlow error
@@ -36,6 +44,9 @@ stavesection.js (custom addition):
 stavesection.draw():
 adjust rectangle positioning, make height depend on text height
 
+stavetie.js (custom addition):
+context opens group for stavetie, can get stavetie SVG element via getAttribute("el")
+
 stavevolta.js (merged Vexflow 3.x):
 Fix the length of voltas for first measures in a system
 (whose lengths were wrongly extended by the width of the clef, key signature, etc. (beginInstructions) in Vexflow 1.2.93)
@@ -55,6 +66,9 @@ Add a context group for each tabnote, so that it can be found in the SVG DOM ("v
 tremolo.js (custom addition):
 Add extra_stroke_scale, y_spacing_scale
 
+timesignature.js (custom addition):
+open group to get SVG group+class for key signature
+
 Currently, we are using Vexflow 1.2.93, because of some formatter advantages
 compared to Vexflow 3.x versions, see this issue:
 https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/915

+ 262 - 0
src/VexFlowPatch/src/clef.js

@@ -0,0 +1,262 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna Cheppudira 2013.
+// Co-author: Benjamin W. Bohl
+//
+// ## Description
+//
+// This file implements various types of clefs that can be rendered on a stave.
+//
+// See `tests/clef_tests.js` for usage examples.
+
+import { Vex } from './vex';
+import { StaveModifier } from './stavemodifier';
+import { Glyph } from './glyph';
+
+// To enable logging for this class, set `Vex.Flow.Clef.DEBUG` to `true`.
+function L(...args) { if (Clef.DEBUG) Vex.L('Vex.Flow.Clef', args); }
+
+export class Clef extends StaveModifier {
+  static get CATEGORY() { return 'clefs'; }
+
+  // Every clef name is associated with a glyph code from the font file
+  // and a default stave line number.
+  static get types() {
+    return {
+      'treble': {
+        code: 'v83',
+        line: 3,
+      },
+      'bass': {
+        code: 'v79',
+        line: 1,
+      },
+      'alto': {
+        code: 'vad',
+        line: 2,
+      },
+      'tenor': {
+        code: 'vad',
+        line: 1,
+      },
+      'percussion': {
+        code: 'v59',
+        line: 2,
+      },
+      'soprano': {
+        code: 'vad',
+        line: 4,
+      },
+      'mezzo-soprano': {
+        code: 'vad',
+        line: 3,
+      },
+      'baritone-c': {
+        code: 'vad',
+        line: 0,
+      },
+      'baritone-f': {
+        code: 'v79',
+        line: 2,
+      },
+      'subbass': {
+        code: 'v79',
+        line: 0,
+      },
+      'french': {
+        code: 'v83',
+        line: 4,
+      },
+      'tab': {
+        code: 'v2f',
+      },
+    };
+  }
+
+  // Sizes affect the point-size of the clef.
+  static get sizes() {
+    return {
+      'default': {
+        point: 40,
+        width: 26
+      },
+      'small': {
+        point: 32,
+        width: 20,
+      },
+    };
+  }
+
+  // Annotations attach to clefs -- such as "8" for octave up or down.
+  static get annotations() {
+    return {
+      '8va': {
+        code: 'v8',
+        sizes: {
+          'default': {
+            point: 20,
+            attachments: {
+              'treble': {
+                line: -1.2,
+                x_shift: 11,
+              },
+            },
+          },
+          'small': {
+            point: 18,
+            attachments: {
+              'treble': {
+                line: -0.4,
+                x_shift: 8,
+              },
+            },
+          },
+        },
+      },
+      '8vb': {
+        code: 'v8',
+        sizes: {
+          'default': {
+            point: 20,
+            attachments: {
+              'treble': {
+                line: 6.3,
+                x_shift: 10,
+              },
+              'bass': {
+                line: 4,
+                x_shift: 1,
+              },
+            },
+          },
+          'small': {
+            point: 18,
+            attachments: {
+              'treble': {
+                line: 5.8,
+                x_shift: 6,
+              },
+              'bass': {
+                line: 3.5,
+                x_shift: 0.5,
+              },
+            },
+          },
+        },
+      },
+    };
+  }
+
+  // Create a new clef. The parameter `clef` must be a key from
+  // `Clef.types`.
+  constructor(type, size, annotation) {
+    super();
+    this.setAttribute('type', 'Clef');
+
+    this.setPosition(StaveModifier.Position.BEGIN);
+    this.setType(type, size, annotation);
+    this.setWidth(Clef.sizes[this.size].width);
+    L('Creating clef:', type);
+  }
+
+  getCategory() { return Clef.CATEGORY; }
+
+  setType(type, size, annotation) {
+    this.type = type;
+    this.clef = Clef.types[type];
+    if (size === undefined) {
+      this.size = 'default';
+    } else {
+      this.size = size;
+    }
+    this.clef.point = Clef.sizes[this.size].point;
+    this.glyph = new Glyph(this.clef.code, this.clef.point);
+
+    // If an annotation, such as 8va, is specified, add it to the Clef object.
+    if (annotation !== undefined) {
+      const anno_dict = Clef.annotations[annotation];
+      this.annotation = {
+        code: anno_dict.code,
+        point: anno_dict.sizes[this.size].point,
+        line: anno_dict.sizes[this.size].attachments[this.type].line,
+        x_shift: anno_dict.sizes[this.size].attachments[this.type].x_shift,
+      };
+
+      this.attachment = new Glyph(this.annotation.code, this.annotation.point);
+      this.attachment.metrics.x_max = 0;
+      this.attachment.setXShift(this.annotation.x_shift);
+    } else {
+      this.annotation = undefined;
+    }
+
+    return this;
+  }
+
+  getWidth() {
+    if (this.type === 'tab' && !this.stave) {
+      throw new Vex.RERR('ClefError', "Can't get width without stave.");
+    }
+
+    return this.width;
+  }
+
+  setStave(stave) {
+    this.stave = stave;
+
+    if (this.type !== 'tab') return this;
+
+    let glyphScale;
+    let glyphOffset;
+    const numLines = this.stave.getOptions().num_lines;
+    switch (numLines) {
+      case 8:
+        glyphScale = 55;
+        glyphOffset = 14;
+        break;
+      case 7:
+        glyphScale = 47;
+        glyphOffset = 8;
+        break;
+      case 6:
+        glyphScale = 40;
+        glyphOffset = 1;
+        break;
+      case 5:
+        glyphScale = 30;
+        glyphOffset = -6;
+        break;
+      case 4:
+        glyphScale = 23;
+        glyphOffset = -12;
+        break;
+      default:
+        throw new Vex.RERR('ClefError', `Invalid number of lines: ${numLines}`);
+    }
+
+    this.glyph.setPoint(glyphScale);
+    this.glyph.setYShift(glyphOffset);
+
+    return this;
+  }
+
+  draw() {
+    if (!this.x) throw new Vex.RERR('ClefError', "Can't draw clef without x.");
+    if (!this.stave) throw new Vex.RERR('ClefError', "Can't draw clef without stave.");
+    this.setRendered();
+
+    this.glyph.setStave(this.stave);
+    this.glyph.setContext(this.stave.context);
+    if (this.clef.line !== undefined) {
+      this.placeGlyphOnLine(this.glyph, this.stave, this.clef.line);
+    }
+
+    this.stave.context.openGroup("clef");
+    this.glyph.renderToStave(this.x);
+
+    if (this.annotation !== undefined) {
+      this.placeGlyphOnLine(this.attachment, this.stave, this.annotation.line);
+      this.attachment.setStave(this.stave);
+      this.attachment.setContext(this.stave.context);
+      this.attachment.renderToStave(this.x);
+    }
+    this.stave.context.closeGroup();
+  }
+}

+ 329 - 0
src/VexFlowPatch/src/keysignature.js

@@ -0,0 +1,329 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+// Author: Cyril Silverman
+//
+// ## Description
+//
+// This file implements key signatures. A key signature sits on a stave
+// and indicates the notes with implicit accidentals.
+
+import { Vex } from './vex';
+import { Flow } from './tables';
+import { StaveModifier } from './stavemodifier';
+import { Glyph } from './glyph';
+
+export class KeySignature extends StaveModifier {
+  static get CATEGORY() { return 'keysignatures'; }
+
+  // Space between natural and following accidental depending
+  // on vertical position
+  static get accidentalSpacing() {
+    return {
+      '#': {
+        above: 6,
+        below: 4,
+      },
+      'b': {
+        above: 4,
+        below: 7,
+      },
+      'n': {
+        above: 4,
+        below: 1,
+      },
+      '##': {
+        above: 6,
+        below: 4,
+      },
+      'bb': {
+        above: 4,
+        below: 7,
+      },
+      'db': {
+        above: 4,
+        below: 7,
+      },
+      'd': {
+        above: 4,
+        below: 7,
+      },
+      'bbs': {
+        above: 4,
+        below: 7,
+      },
+      '++': {
+        above: 6,
+        below: 4,
+      },
+      '+': {
+        above: 6,
+        below: 4,
+      },
+      '+-': {
+        above: 6,
+        below: 4,
+      },
+      '++-': {
+        above: 6,
+        below: 4,
+      },
+      'bs': {
+        above: 4,
+        below: 10,
+      },
+      'bss': {
+        above: 4,
+        below: 10,
+      },
+    };
+  }
+
+  // Create a new Key Signature based on a `key_spec`
+  constructor(keySpec, cancelKeySpec, alterKeySpec) {
+    super();
+    this.setAttribute('type', 'KeySignature');
+
+    this.setKeySig(keySpec, cancelKeySpec, alterKeySpec);
+    this.setPosition(StaveModifier.Position.BEGIN);
+    this.glyphFontScale = 38; // TODO(0xFE): Should this match StaveNote?
+    this.glyphs = [];
+    this.xPositions = []; // relative to this.x
+    this.paddingForced = false;
+  }
+
+  getCategory() { return KeySignature.CATEGORY; }
+
+  // Add an accidental glyph to the `KeySignature` instance which represents
+  // the provided `acc`. If `nextAcc` is also provided, the appropriate
+  // spacing will be included in the glyph's position
+  convertToGlyph(acc, nextAcc) {
+    const accGlyphData = Flow.accidentalCodes(acc.type);
+    const glyph = new Glyph(accGlyphData.code, this.glyphFontScale);
+
+    // Determine spacing between current accidental and the next accidental
+    let extraWidth = 1;
+    if (acc.type === 'n' && nextAcc) {
+      const spacing = KeySignature.accidentalSpacing[nextAcc.type];
+      if (spacing) {
+        const isAbove = nextAcc.line >= acc.line;
+        extraWidth = isAbove ? spacing.above : spacing.below;
+      }
+    }
+
+    // Place the glyph on the stave
+    this.placeGlyphOnLine(glyph, this.stave, acc.line);
+    this.glyphs.push(glyph);
+
+    const xPosition = this.xPositions[this.xPositions.length - 1];
+    const glyphWidth = glyph.getMetrics().width + extraWidth;
+    // Store the next accidental's x position
+    this.xPositions.push(xPosition + glyphWidth);
+    // Expand size of key signature
+    this.width += glyphWidth;
+  }
+
+  // Cancel out a key signature provided in the `spec` parameter. This will
+  // place appropriate natural accidentals before the key signature.
+  cancelKey(spec) {
+    this.formatted = false;
+    this.cancelKeySpec = spec;
+
+    return this;
+  }
+
+  convertToCancelAccList(spec) {
+    // Get the accidental list for the cancelled key signature
+    const cancel_accList = Flow.keySignature(spec);
+
+    // If the cancelled key has a different accidental type, ie: # vs b
+    const different_types = this.accList.length > 0
+      && cancel_accList.length > 0
+      && cancel_accList[0].type !== this.accList[0].type;
+
+    // Determine how many naturals needed to add
+    const naturals = different_types
+      ? cancel_accList.length
+      : cancel_accList.length - this.accList.length;
+
+    // Return if no naturals needed
+    if (naturals < 1) return undefined;
+
+    // Get the line position for each natural
+    const cancelled = [];
+    for (let i = 0; i < naturals; i++) {
+      let index = i;
+      if (!different_types) {
+        index = cancel_accList.length - naturals + i;
+      }
+
+      const acc = cancel_accList[index];
+      cancelled.push({ type: 'n', line: acc.line });
+    }
+
+    // Combine naturals with main accidental list for the key signature
+    this.accList = cancelled.concat(this.accList);
+
+    return {
+      accList: cancelled,
+      type: cancel_accList[0].type
+    };
+  }
+
+  // Deprecated
+  addToStave(stave) {
+    this.paddingForced = true;
+    stave.addModifier(this);
+
+    return this;
+  }
+
+  // Apply the accidental staff line placement based on the `clef` and
+  // the  accidental `type` for the key signature ('# or 'b').
+  convertAccLines(clef, type, accList = this.accList) {
+    let offset = 0.0; // if clef === "treble"
+    let customLines; // when clef doesn't follow treble key sig shape
+
+    switch (clef) {
+      // Treble & Subbass both have offsets of 0, so are not included.
+      case 'soprano':
+        if (type === '#') customLines = [2.5, 0.5, 2, 0, 1.5, -0.5, 1];
+        else offset = -1;
+        break;
+      case 'mezzo-soprano':
+        if (type === 'b') customLines = [0, 2, 0.5, 2.5, 1, 3, 1.5];
+        else offset = 1.5;
+        break;
+      case 'alto':
+        offset = 0.5;
+        break;
+      case 'tenor':
+        if (type === '#') customLines = [3, 1, 2.5, 0.5, 2, 0, 1.5];
+        else offset = -0.5;
+        break;
+      case 'baritone-f':
+      case 'baritone-c':
+        if (type === 'b') customLines = [0.5, 2.5, 1, 3, 1.5, 3.5, 2];
+        else offset = 2;
+        break;
+      case 'bass':
+      case 'french':
+        offset = 1;
+        break;
+      default:
+        break;
+    }
+
+    // If there's a special case, assign those lines/spaces:
+    let i;
+    if (typeof customLines !== 'undefined') {
+      for (i = 0; i < accList.length; ++i) {
+        accList[i].line = customLines[i];
+      }
+    } else if (offset !== 0) {
+      for (i = 0; i < accList.length; ++i) {
+        accList[i].line += offset;
+      }
+    }
+  }
+
+  getPadding(index) {
+    if (!this.formatted) this.format();
+
+    return (
+      this.glyphs.length === 0 || (!this.paddingForced && index < 2) ?
+        0 : this.padding
+    );
+  }
+
+  getWidth() {
+    if (!this.formatted) this.format();
+
+    return this.width;
+  }
+
+  setKeySig(keySpec, cancelKeySpec, alterKeySpec) {
+    this.formatted = false;
+    this.keySpec = keySpec;
+    this.cancelKeySpec = cancelKeySpec;
+    this.alterKeySpec = alterKeySpec;
+
+    return this;
+  }
+
+  // Alter the accidentals of a key spec one by one.
+  // Each alteration is a new accidental that replaces the
+  // original accidental (or the canceled one).
+  alterKey(alterKeySpec) {
+    this.formatted = false;
+    this.alterKeySpec = alterKeySpec;
+
+    return this;
+  }
+
+  convertToAlterAccList(alterKeySpec) {
+    const max = Math.min(alterKeySpec.length, this.accList.length);
+    for (let i = 0; i < max; ++i) {
+      if (alterKeySpec[i]) {
+        this.accList[i].type = alterKeySpec[i];
+      }
+    }
+  }
+
+  format() {
+    if (!this.stave) {
+      throw new Vex.RERR('KeySignatureError', "Can't draw key signature without stave.");
+    }
+
+    this.width = 0;
+    this.glyphs = [];
+    this.xPositions = [0]; // initialize with initial x position
+    this.accList = Flow.keySignature(this.keySpec);
+    const accList = this.accList;
+    const firstAccidentalType = accList.length > 0 ? accList[0].type : null;
+    let cancelAccList;
+    if (this.cancelKeySpec) {
+      cancelAccList = this.convertToCancelAccList(this.cancelKeySpec);
+    }
+    if (this.alterKeySpec) {
+      this.convertToAlterAccList(this.alterKeySpec);
+    }
+
+    if (this.accList.length > 0) {
+      const clef = ((this.position === StaveModifier.Position.END) ?
+        this.stave.endClef : this.stave.clef) || this.stave.clef;
+      if (cancelAccList) {
+        this.convertAccLines(clef, cancelAccList.type, cancelAccList.accList);
+      }
+      this.convertAccLines(clef, firstAccidentalType, accList);
+      for (let i = 0; i < this.accList.length; ++i) {
+        this.convertToGlyph(this.accList[i], this.accList[i + 1]);
+      }
+    }
+
+    this.formatted = true;
+  }
+
+  draw() {
+    if (!this.x) {
+      throw new Vex.RERR('KeySignatureError', "Can't draw key signature without x.");
+    }
+
+    if (!this.stave) {
+      throw new Vex.RERR('KeySignatureError', "Can't draw key signature without stave.");
+    }
+
+    if (!this.formatted) this.format();
+    this.setRendered();
+
+    if (this.glyphs.length > 0) {
+        this.stave.context.openGroup("keysignature");
+        for (let i = 0; i < this.glyphs.length; i++) {
+          const glyph = this.glyphs[i];
+          const x = this.x + this.xPositions[i];
+          glyph.setStave(this.stave);
+          glyph.setContext(this.stave.context);
+          glyph.renderToStave(x);
+        }
+        this.stave.context.closeGroup();
+    }
+  }
+}

+ 189 - 0
src/VexFlowPatch/src/stavetie.js

@@ -0,0 +1,189 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+//
+// ## Description
+// This class implements varies types of ties between contiguous notes. The
+// ties include: regular ties, hammer ons, pull offs, and slides.
+
+import { Vex } from './vex';
+import { Element } from './element';
+
+export class StaveTie extends Element {
+  constructor(notes, text) {
+    /**
+     * Notes is a struct that has:
+     *
+     *  {
+     *    first_note: Note,
+     *    last_note: Note,
+     *    first_indices: [n1, n2, n3],
+     *    last_indices: [n1, n2, n3]
+     *  }
+     *
+     **/
+    super();
+    this.setAttribute('type', 'StaveTie');
+    this.notes = notes;
+    this.context = null;
+    this.text = text;
+    this.direction = null;
+
+    this.render_options = {
+      cp1: 8,      // Curve control point 1
+      cp2: 12,      // Curve control point 2
+      text_shift_x: 0,
+      first_x_shift: 0,
+      last_x_shift: 0,
+      y_shift: 7,
+      tie_spacing: 0,
+      font: { family: 'Arial', size: 10, style: '' },
+    };
+
+    this.font = this.render_options.font;
+    this.setNotes(notes);
+  }
+
+  setFont(font) { this.font = font; return this; }
+  setDirection(direction) { this.direction = direction; return this; }
+
+  /**
+   * Set the notes to attach this tie to.
+   *
+   * @param {!Object} notes The notes to tie up.
+   */
+  setNotes(notes) {
+    if (!notes.first_note && !notes.last_note) {
+      throw new Vex.RuntimeError(
+        'BadArguments', 'Tie needs to have either first_note or last_note set.'
+      );
+    }
+
+    if (!notes.first_indices) notes.first_indices = [0];
+    if (!notes.last_indices) notes.last_indices = [0];
+
+    if (notes.first_indices.length !== notes.last_indices.length) {
+      throw new Vex.RuntimeError('BadArguments', 'Tied notes must have similar index sizes');
+    }
+
+    // Success. Lets grab 'em notes.
+    this.first_note = notes.first_note;
+    this.first_indices = notes.first_indices;
+    this.last_note = notes.last_note;
+    this.last_indices = notes.last_indices;
+    return this;
+  }
+
+  /**
+   * @return {boolean} Returns true if this is a partial bar.
+   */
+  isPartial() {
+    return (!this.first_note || !this.last_note);
+  }
+
+  renderTie(params) {
+    if (params.first_ys.length === 0 || params.last_ys.length === 0) {
+      throw new Vex.RERR('BadArguments', 'No Y-values to render');
+    }
+
+    const ctx = this.context;
+    let cp1 = this.render_options.cp1;
+    let cp2 = this.render_options.cp2;
+
+    if (Math.abs(params.last_x_px - params.first_x_px) < 10) {
+      cp1 = 2; cp2 = 8;
+    }
+
+    const first_x_shift = this.render_options.first_x_shift;
+    const last_x_shift = this.render_options.last_x_shift;
+    const y_shift = this.render_options.y_shift * params.direction;
+
+    for (let i = 0; i < this.first_indices.length; ++i) {
+      const cp_x = ((params.last_x_px + last_x_shift) +
+          (params.first_x_px + first_x_shift)) / 2;
+      const first_y_px = params.first_ys[this.first_indices[i]] + y_shift;
+      const last_y_px = params.last_ys[this.last_indices[i]] + y_shift;
+
+      if (isNaN(first_y_px) || isNaN(last_y_px)) {
+        throw new Vex.RERR('BadArguments', 'Bad indices for tie rendering.');
+      }
+
+      const top_cp_y = ((first_y_px + last_y_px) / 2) + (cp1 * params.direction);
+      const bottom_cp_y = ((first_y_px + last_y_px) / 2) + (cp2 * params.direction);
+
+      let id;
+      if (this.first_note) { // ?. would be shorter, but fails appveyor build
+        id = this.first_note.getAttribute('id') + "-tie";
+      }
+      this.setAttribute('el', ctx.openGroup('stavetie', id));
+      ctx.beginPath();
+      ctx.moveTo(params.first_x_px + first_x_shift, first_y_px);
+      ctx.quadraticCurveTo(cp_x, top_cp_y, params.last_x_px + last_x_shift, last_y_px);
+      ctx.quadraticCurveTo(cp_x, bottom_cp_y, params.first_x_px + first_x_shift, first_y_px);
+      ctx.closePath();
+      ctx.fill();
+      ctx.closeGroup();
+    }
+  }
+
+  renderText(first_x_px, last_x_px) {
+    if (!this.text) return;
+    let center_x = (first_x_px + last_x_px) / 2;
+    center_x -= this.context.measureText(this.text).width / 2;
+
+    this.context.save();
+    this.context.setFont(this.font.family, this.font.size, this.font.style);
+    this.context.fillText(
+      this.text,
+      center_x + this.render_options.text_shift_x,
+      (this.first_note || this.last_note).getStave().getYForTopText() - 1
+    );
+    this.context.restore();
+  }
+
+  draw() {
+    this.checkContext();
+    this.setRendered();
+
+    const first_note = this.first_note;
+    const last_note = this.last_note;
+
+    let first_x_px;
+    let last_x_px;
+    let first_ys;
+    let last_ys;
+    let stem_direction;
+    if (first_note) {
+      first_x_px = first_note.getTieRightX() + this.render_options.tie_spacing;
+      stem_direction = first_note.getStemDirection();
+      first_ys = first_note.getYs();
+    } else {
+      first_x_px = last_note.getStave().getTieStartX();
+      first_ys = last_note.getYs();
+      this.first_indices = this.last_indices;
+    }
+
+    if (last_note) {
+      last_x_px = last_note.getTieLeftX() + this.render_options.tie_spacing;
+      stem_direction = last_note.getStemDirection();
+      last_ys = last_note.getYs();
+    } else {
+      last_x_px = first_note.getStave().getTieEndX();
+      last_ys = first_note.getYs();
+      this.last_indices = this.first_indices;
+    }
+
+    if (this.direction) {
+      stem_direction = this.direction;
+    }
+
+    this.renderTie({
+      first_x_px,
+      last_x_px,
+      first_ys,
+      last_ys,
+      direction: stem_direction,
+    });
+
+    this.renderText(first_x_px, last_x_px);
+    return true;
+  }
+}

+ 187 - 0
src/VexFlowPatch/src/timesignature.js

@@ -0,0 +1,187 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+//
+// ## Description
+// Implements time signatures glyphs for staffs
+// See tables.js for the internal time signatures
+// representation
+
+import { Vex } from './vex';
+import { Glyph } from './glyph';
+import { StaveModifier } from './stavemodifier';
+
+const assertIsValidFraction = (timeSpec) => {
+  const numbers = timeSpec.split('/').filter(number => number !== '');
+
+  if (numbers.length !== 2) {
+    throw new Vex.RERR(
+      'BadTimeSignature',
+      `Invalid time spec: ${timeSpec}. Must be in the form "<numerator>/<denominator>"`
+    );
+  }
+
+  numbers.forEach(number => {
+    if (isNaN(Number(number))) {
+      throw new Vex.RERR(
+        'BadTimeSignature', `Invalid time spec: ${timeSpec}. Must contain two valid numbers.`
+      );
+    }
+  });
+};
+
+export class TimeSignature extends StaveModifier {
+  static get CATEGORY() { return 'timesignatures'; }
+
+  static get glyphs() {
+    return {
+      'C': {
+        code: 'v41',
+        point: 40,
+        line: 2,
+      },
+      'C|': {
+        code: 'vb6',
+        point: 40,
+        line: 2,
+      },
+    };
+  }
+
+  constructor(timeSpec = null, customPadding = 15, validate_args = true) {
+    super();
+    this.setAttribute('type', 'TimeSignature');
+    this.validate_args = validate_args;
+
+    if (timeSpec === null) return;
+
+    const padding = customPadding;
+
+    this.point = 40;
+    this.topLine = 2;
+    this.bottomLine = 4;
+    this.setPosition(StaveModifier.Position.BEGIN);
+    this.setTimeSig(timeSpec);
+    this.setWidth(this.timeSig.glyph.getMetrics().width);
+    this.setPadding(padding);
+  }
+
+  getCategory() { return TimeSignature.CATEGORY; }
+
+  parseTimeSpec(timeSpec) {
+    if (timeSpec === 'C' || timeSpec === 'C|') {
+      const { line, code, point } = TimeSignature.glyphs[timeSpec];
+      return {
+        line,
+        num: false,
+        glyph: new Glyph(code, point),
+      };
+    }
+
+    if (this.validate_args) {
+      assertIsValidFraction(timeSpec);
+    }
+
+    const [topDigits, botDigits] = timeSpec
+      .split('/')
+      .map(number => number.split(''));
+
+    return {
+      num: true,
+      glyph: this.makeTimeSignatureGlyph(topDigits, botDigits),
+    };
+  }
+
+  makeTimeSignatureGlyph(topDigits, botDigits) {
+    const glyph = new Glyph('v0', this.point);
+    glyph.topGlyphs = [];
+    glyph.botGlyphs = [];
+
+    let topWidth = 0;
+    for (let i = 0; i < topDigits.length; ++i) {
+      const num = topDigits[i];
+      const topGlyph = new Glyph('v' + num, this.point);
+
+      glyph.topGlyphs.push(topGlyph);
+      topWidth += topGlyph.getMetrics().width;
+    }
+
+    let botWidth = 0;
+    for (let i = 0; i < botDigits.length; ++i) {
+      const num = botDigits[i];
+      const botGlyph = new Glyph('v' + num, this.point);
+
+      glyph.botGlyphs.push(botGlyph);
+      botWidth += botGlyph.getMetrics().width;
+    }
+
+    const width = topWidth > botWidth ? topWidth : botWidth;
+    const xMin = glyph.getMetrics().x_min;
+
+    glyph.getMetrics = () => ({
+      x_min: xMin,
+      x_max: xMin + width,
+      width,
+    });
+
+    const topStartX = (width - topWidth) / 2.0;
+    const botStartX = (width - botWidth) / 2.0;
+
+    const that = this;
+    glyph.renderToStave = function renderToStave(x) {
+      let start_x = x + topStartX;
+      for (let i = 0; i < this.topGlyphs.length; ++i) {
+        const glyph = this.topGlyphs[i];
+        Glyph.renderOutline(
+          this.context,
+          glyph.metrics.outline,
+          glyph.scale,
+          start_x + glyph.x_shift,
+          this.stave.getYForLine(that.topLine)
+        );
+        start_x += glyph.getMetrics().width;
+      }
+
+      start_x = x + botStartX;
+      for (let i = 0; i < this.botGlyphs.length; ++i) {
+        const glyph = this.botGlyphs[i];
+        that.placeGlyphOnLine(glyph, this.stave, glyph.line);
+        Glyph.renderOutline(
+          this.context,
+          glyph.metrics.outline,
+          glyph.scale,
+          start_x + glyph.x_shift,
+          this.stave.getYForLine(that.bottomLine)
+        );
+        start_x += glyph.getMetrics().width;
+      }
+    };
+
+    return glyph;
+  }
+
+  getTimeSig() {
+    return this.timeSig;
+  }
+
+  setTimeSig(timeSpec) {
+    this.timeSig = this.parseTimeSpec(timeSpec);
+    return this;
+  }
+
+  draw() {
+    if (!this.x) {
+      throw new Vex.RERR('TimeSignatureError', "Can't draw time signature without x.");
+    }
+
+    if (!this.stave) {
+      throw new Vex.RERR('TimeSignatureError', "Can't draw time signature without stave.");
+    }
+
+    this.setRendered();
+    this.timeSig.glyph.setStave(this.stave);
+    this.timeSig.glyph.setContext(this.stave.context);
+    this.placeGlyphOnLine(this.timeSig.glyph, this.stave, this.timeSig.line);
+    this.stave.context.openGroup("timesignature");
+    this.timeSig.glyph.renderToStave(this.x);
+    this.stave.context.closeGroup("timesignature");
+  }
+}