Selaa lähdekoodia

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 vuotta sitten
vanhempi
commit
e0a215e87c

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "osmd-extended",
   "name": "osmd-extended",
-  "version": "1.4.2",
+  "version": "1.4.3",
   "description": "Private / sponsor exclusive OSMD mirror/audio player.",
   "description": "Private / sponsor exclusive OSMD mirror/audio player.",
   "main": "build/opensheetmusicdisplay.min.js",
   "main": "build/opensheetmusicdisplay.min.js",
   "types": "build/dist/src/OpenSheetMusicDisplay/index.d.ts",
   "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 {Tie} from "../VoiceData/Tie";
 import {GraphicalNote} from "./GraphicalNote";
 import {GraphicalNote} from "./GraphicalNote";
+import Vex from "vexflow";
 
 
 /**
 /**
  * The graphical counterpart of a [[Tie]].
  * The graphical counterpart of a [[Tie]].
@@ -8,6 +9,7 @@ export class GraphicalTie {
     private tie: Tie;
     private tie: Tie;
     private startNote: GraphicalNote;
     private startNote: GraphicalNote;
     private endNote: GraphicalNote;
     private endNote: GraphicalNote;
+    public vfTie: Vex.Flow.StaveTie;
 
 
     constructor(tie: Tie, start: GraphicalNote = undefined, end: GraphicalNote = undefined) {
     constructor(tie: Tie, start: GraphicalNote = undefined, end: GraphicalNote = undefined) {
         this.tie = tie;
         this.tie = tie;
@@ -15,6 +17,10 @@ export class GraphicalTie {
         this.endNote = end;
         this.endNote = end;
     }
     }
 
 
+    public get SVGElement(): HTMLElement {
+        return (this.vfTie as any).getAttribute("el");
+    }
+
     public get GetTie(): Tie {
     public get GetTie(): Tie {
         return this.tie;
         return this.tie;
     }
     }

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

@@ -827,9 +827,11 @@ export abstract class MusicSheetCalculator {
 
 
         // build the MusicSystems
         // build the MusicSystems
         let musicSystemBuilder: MusicSystemBuilder;
         let musicSystemBuilder: MusicSystemBuilder;
-        if (this.rules.RenderSingleHorizontalStaffline) {
+        const measureCount: number = allMeasures.length;
+        if (measureCount === 1 || this.rules.RenderSingleHorizontalStaffline) {
             musicSystemBuilder = new MusicSystemBuilder();
             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 {
         } else {
             musicSystemBuilder = new JustifiedMusicSystemBuilder();
             musicSystemBuilder = new JustifiedMusicSystemBuilder();
         }
         }
@@ -2142,13 +2144,25 @@ export abstract class MusicSheetCalculator {
                         const startStaffEntry: GraphicalStaffEntry = this.graphicalMusicSheet.findGraphicalStaffEntryFromMeasureList(
                         const startStaffEntry: GraphicalStaffEntry = this.graphicalMusicSheet.findGraphicalStaffEntryFromMeasureList(
                             staffIndex, measureIndex, sourceStaffEntry
                             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) {
                         for (let idx: number = 0, len: number = sourceStaffEntry.VoiceEntries.length; idx < len; ++idx) {
                             const voiceEntry: VoiceEntry = sourceStaffEntry.VoiceEntries[idx];
                             const voiceEntry: VoiceEntry = sourceStaffEntry.VoiceEntries[idx];
                             for (let idx2: number = 0, len2: number = voiceEntry.Notes.length; idx2 < len2; ++idx2) {
                             for (let idx2: number = 0, len2: number = voiceEntry.Notes.length; idx2 < len2; ++idx2) {
                                 const note: Note = voiceEntry.Notes[idx2];
                                 const note: Note = voiceEntry.Notes[idx2];
-                                if (note.NoteTie &&
-                                    note === note.NoteTie.StartNote) {
+                                if (note.NoteTie) {
                                     const tie: Tie = 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);
                                     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 {SkyBottomLineCalculator} from "../SkyBottomLineCalculator";
 import { NoteType } from "../../VoiceData/NoteType";
 import { NoteType } from "../../VoiceData/NoteType";
 import { Arpeggio } from "../../VoiceData/Arpeggio";
 import { Arpeggio } from "../../VoiceData/Arpeggio";
+import { GraphicalTie } from "../GraphicalTie";
 
 
 // type StemmableNote = Vex.Flow.StemmableNote;
 // type StemmableNote = Vex.Flow.StemmableNote;
 
 
@@ -1570,6 +1571,11 @@ export class VexFlowMeasure extends GraphicalMeasure {
         this.beginInstructionsWidth = (vfBeginInstructionsWidth ?? 0) / unitInPixels;
         this.beginInstructionsWidth = (vfBeginInstructionsWidth ?? 0) / unitInPixels;
         this.endInstructionsWidth = (vfEndInstructionsWidth ?? 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
 // 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
           first_note: vfStartNote
         });
         });
         const measure1: VexFlowMeasure = (startNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
         const measure1: VexFlowMeasure = (startNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
-        measure1.vfTies.push(vfTie1);
+        measure1.addStaveTie(vfTie1, tie);
       }
       }
 
 
       if (vfEndNote) {
       if (vfEndNote) {
@@ -635,7 +635,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
           last_note: vfEndNote
           last_note: vfEndNote
         });
         });
         const measure2: VexFlowMeasure = (endNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
         const measure2: VexFlowMeasure = (endNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
-        measure2.vfTies.push(vfTie2);
+        measure2.addStaveTie(vfTie2, tie);
       }
       }
     } else {
     } else {
       // normal case
       // normal case
@@ -678,15 +678,16 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
             last_indices: [endNoteIndexInTie],
             last_indices: [endNoteIndexInTie],
             last_note: vfEndNote
             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
             vfTie.setDirection(1); // + is down in vexflow
-          } else if (tie.Tie.TieDirection === PlacementEnum.Above) {
+          } else if (tieDirection === PlacementEnum.Above) {
             vfTie.setDirection(-1);
             vfTie.setDirection(-1);
           }
           }
         }
         }
 
 
         const measure: VexFlowMeasure = (endNote.parentVoiceEntry.parentStaffEntry.parentMeasure as VexFlowMeasure);
         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 {
   private addTie(tieNodeList: IXmlElement[], measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, tieType: TieTypes): void {
     if (tieNodeList) {
     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()) {
         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;
           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 {
           try {
             if (type === "start") {
             if (type === "start") {
               const num: number = this.findCurrentNoteInTieDict(this.currentNote);
               const num: number = this.findCurrentNoteInTieDict(this.currentNote);
@@ -1033,7 +1010,7 @@ export class VoiceGenerator {
               const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
               const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
               const tie: Tie = this.openTieDict[tieNumber];
               const tie: Tie = this.openTieDict[tieNumber];
               if (tie) {
               if (tie) {
-                tie.AddNote(this.currentNote, false);
+                tie.AddNote(this.currentNote);
                 delete this.openTieDict[tieNumber];
                 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 { Pitch } from "../../Common/DataObjects/Pitch";
 import { TieTypes } from "../../Common/Enums/";
 import { TieTypes } from "../../Common/Enums/";
 import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
 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.
  * 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;
     private type: TieTypes;
     public TieNumber: number = 1;
     public TieNumber: number = 1;
     public TieDirection: PlacementEnum = PlacementEnum.NotYetDefined;
     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[] {
     public get Notes(): Note[] {
         return this.notes;
         return this.notes;
@@ -43,10 +70,12 @@ export class Tie {
         return this.StartNote.Pitch;
         return this.StartNote.Pitch;
     }
     }
 
 
-    public AddNote(note: Note, isStartNote: boolean = true): void {
+    public AddNote(note: Note): void {
         this.notes.push(note);
         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.
  * After the constructor, use load() and render() to load and render a MusicXML file.
  */
  */
 export class OpenSheetMusicDisplay {
 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
     // 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
 add flat_beams, flat_beam_offset, flat_beam_offset_per_beam render_option
 able to add svg node id+class to beam
 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):
 pedalmarking.js (custom addition):
 Add rendering options for pedals that break across systems.
 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):
 stave.js (custom addition):
 prevent a bug where a modifier width is NaN, leading to a VexFlow error
 prevent a bug where a modifier width is NaN, leading to a VexFlow error
@@ -36,6 +44,9 @@ stavesection.js (custom addition):
 stavesection.draw():
 stavesection.draw():
 adjust rectangle positioning, make height depend on text height
 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):
 stavevolta.js (merged Vexflow 3.x):
 Fix the length of voltas for first measures in a system
 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)
 (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):
 tremolo.js (custom addition):
 Add extra_stroke_scale, y_spacing_scale
 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
 Currently, we are using Vexflow 1.2.93, because of some formatter advantages
 compared to Vexflow 3.x versions, see this issue:
 compared to Vexflow 3.x versions, see this issue:
 https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/915
 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");
+  }
+}