123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065 |
- import Vex from "vexflow";
- import {ClefEnum} from "../../VoiceData/Instructions/ClefInstruction";
- import {ClefInstruction} from "../../VoiceData/Instructions/ClefInstruction";
- import {Pitch} from "../../../Common/DataObjects/Pitch";
- import {Fraction} from "../../../Common/DataObjects/Fraction";
- import {RhythmInstruction} from "../../VoiceData/Instructions/RhythmInstruction";
- import {RhythmSymbolEnum} from "../../VoiceData/Instructions/RhythmInstruction";
- import {KeyInstruction} from "../../VoiceData/Instructions/KeyInstruction";
- import {KeyEnum} from "../../VoiceData/Instructions/KeyInstruction";
- import {AccidentalEnum} from "../../../Common/DataObjects/Pitch";
- import {NoteEnum} from "../../../Common/DataObjects/Pitch";
- import {VexFlowGraphicalNote} from "./VexFlowGraphicalNote";
- import {GraphicalNote} from "../GraphicalNote";
- import {SystemLinesEnum} from "../SystemLinesEnum";
- import {FontStyles} from "../../../Common/Enums/FontStyles";
- import {Fonts} from "../../../Common/Enums/Fonts";
- import {OutlineAndFillStyleEnum, OUTLINE_AND_FILL_STYLE_DICT} from "../DrawingEnums";
- import log from "loglevel";
- import { ArticulationEnum, StemDirectionType, VoiceEntry } from "../../VoiceData/VoiceEntry";
- import { SystemLinePosition } from "../SystemLinePosition";
- import { GraphicalVoiceEntry } from "../GraphicalVoiceEntry";
- import { OrnamentEnum, OrnamentContainer } from "../../VoiceData/OrnamentContainer";
- import { Notehead, NoteHeadShape } from "../../VoiceData/Notehead";
- import { unitInPixels } from "./VexFlowMusicSheetDrawer";
- import { EngravingRules } from "../EngravingRules";
- import { Note } from "../../../MusicalScore/VoiceData/Note";
- import StaveNote = Vex.Flow.StaveNote;
- import { ArpeggioType } from "../../VoiceData/Arpeggio";
- import { TabNote } from "../../VoiceData/TabNote";
- import { PlacementEnum } from "../../VoiceData/Expressions/AbstractExpression";
- import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
- import { Articulation } from "../../VoiceData/Articulation";
- /**
- * Helper class, which contains static methods which actually convert
- * from OSMD objects to VexFlow objects.
- */
- export class VexFlowConverter {
- /**
- * Mapping from numbers of alterations on the key signature to major keys
- * @type {[alterationsNo: number]: string; }
- */
- private static majorMap: {[_: number]: string } = {
- "-1": "F", "-2": "Bb", "-3": "Eb", "-4": "Ab", "-5": "Db", "-6": "Gb", "-7": "Cb", "-8": "Fb",
- "0": "C", "1": "G", "2": "D", "3": "A", "4": "E", "5": "B", "6": "F#", "7": "C#", "8": "G#"
- };
- /**
- * Mapping from numbers of alterations on the key signature to minor keys
- * @type {[alterationsNo: number]: string; }
- */
- private static minorMap: {[_: number]: string } = {
- "-1": "D", "-2": "G", "-3": "C", "-4": "F", "-5": "Bb", "-6": "Eb", "-7": "Ab", "-8": "Db",
- "0": "A", "1": "E", "2": "B", "3": "F#", "4": "C#", "5": "G#", "6": "D#", "7": "A#", "8": "E#"
- };
- /**
- * Convert a fraction to Vexflow string durations.
- * A duration like 5/16 (5 16th notes) can't be represented by a single (dotted) note,
- * so we need to return multiple durations (e.g. for 5/16th ghost notes).
- * Currently, for a dotted quarter ghost note, we return a quarter and an eighth ghost note.
- * We could return a dotted quarter instead, but then the code would need to distinguish between
- * notes that can be represented as dotted notes and notes that can't, which would complicate things.
- * We could e.g. add a parameter "allowSingleDottedNote" which makes it possible to return single dotted notes instead.
- * But currently, this is only really used for Ghost notes, so it doesn't make a difference visually.
- * (for other uses like StaveNotes, we calculate the dots separately)
- * @param fraction a fraction representing the duration of a note
- * @returns {string[]} Vexflow note type strings (e.g. "h" = half note)
- */
- public static durations(fraction: Fraction, isTuplet: boolean): string[] {
- const durations: string[] = [];
- const remainingFraction: Fraction = fraction.clone();
- while (remainingFraction.RealValue > 0) {
- const dur: number = remainingFraction.RealValue;
- // TODO consider long (dur=4) and maxima (dur=8), though Vexflow doesn't seem to support them
- if (dur >= 2) { // Breve
- durations.push("1/2");
- remainingFraction.Sub(new Fraction(2, 1));
- } else if (dur >= 1) {
- durations.push("w");
- remainingFraction.Sub(new Fraction(1, 1));
- } else if (dur < 1 && dur >= 0.5) {
- // change to the next higher straight note to get the correct note display type
- if (isTuplet && dur > 0.5) {
- return ["w"];
- } else {
- durations.push("h");
- remainingFraction.Sub(new Fraction(1, 2));
- }
- } else if (dur < 0.5 && dur >= 0.25) {
- // change to the next higher straight note to get the correct note display type
- if (isTuplet && dur > 0.25) {
- return ["h"];
- } else {
- durations.push("q");
- remainingFraction.Sub(new Fraction(1, 4));
- }
- } else if (dur < 0.25 && dur >= 0.125) {
- // change to the next higher straight note to get the correct note display type
- if (isTuplet && dur > 0.125) {
- return ["q"];
- } else {
- durations.push("8");
- remainingFraction.Sub(new Fraction(1, 8));
- }
- } else if (dur < 0.125 && dur >= 0.0625) {
- // change to the next higher straight note to get the correct note display type
- if (isTuplet && dur > 0.0625) {
- return ["8"];
- } else {
- durations.push("16");
- remainingFraction.Sub(new Fraction(1, 16));
- }
- } else if (dur < 0.0625 && dur >= 0.03125) {
- // change to the next higher straight note to get the correct note display type
- if (isTuplet && dur > 0.03125) {
- return ["16"];
- } else {
- durations.push("32");
- remainingFraction.Sub(new Fraction(1, 32));
- }
- } else if (dur < 0.03125 && dur >= 0.015625) {
- // change to the next higher straight note to get the correct note display type
- if (isTuplet && dur > 0.015625) {
- return ["32"];
- } else {
- durations.push("64");
- remainingFraction.Sub(new Fraction(1, 64));
- }
- } else {
- if (isTuplet) {
- return ["64"];
- } else {
- durations.push("128");
- remainingFraction.Sub(new Fraction(1, 128));
- }
- }
- }
- // if (isTuplet) {
- // dots = 0; // TODO (different) calculation?
- // } else {
- // dots = fraction.calculateNumberOfNeededDots();
- // }
- return durations;
- }
- /**
- * Takes a Pitch and returns a string representing a VexFlow pitch,
- * which has the form "b/4", plus its alteration (accidental)
- * @param pitch
- * @returns {string[]}
- */
- public static pitch(pitch: Pitch, isRest: boolean, clef: ClefInstruction,
- notehead: Notehead = undefined, octaveOffsetGiven: number = undefined): [string, string, ClefInstruction] {
- //FIXME: The octave seems to need a shift of three?
- //FIXME: Also rests seem to use different offsets depending on the clef.
- let octaveOffset: number = octaveOffsetGiven;
- if (octaveOffsetGiven === undefined) {
- octaveOffset = 3;
- }
- if (isRest && octaveOffsetGiven === undefined) {
- octaveOffset = 0;
- if (clef.ClefType === ClefEnum.F) {
- octaveOffset = 2;
- }
- if (clef.ClefType === ClefEnum.C) {
- octaveOffset = 2;
- }
- // TODO the pitch for rests will be the start position, for eights rests it will be the bottom point
- // maybe we want to center on the display position instead of having the bottom there?
- }
- const fund: string = NoteEnum[pitch.FundamentalNote].toLowerCase();
- const acc: string = Pitch.accidentalVexflow(pitch.Accidental);
- const octave: number = pitch.Octave - clef.OctaveOffset + octaveOffset;
- let noteheadCode: string = "";
- if (notehead) {
- noteheadCode = this.NoteHeadCode(notehead);
- }
- return [fund + "n/" + octave + noteheadCode, acc, clef];
- }
- public static restToNotePitch(pitch: Pitch, clefType: ClefEnum): Pitch {
- let octave: number = pitch.Octave;
- // offsets see pitch()
- switch (clefType) {
- case ClefEnum.C:
- case ClefEnum.F: {
- octave += 2;
- break;
- }
- case ClefEnum.G:
- default:
- }
- return new Pitch(pitch.FundamentalNote, octave, AccidentalEnum.NONE);
- }
- /** returns the Vexflow code for a note head. Some are still unsupported, see Vexflow/tables.js */
- public static NoteHeadCode(notehead: Notehead): string {
- const codeStart: string = "/";
- const codeFilled: string = notehead.Filled ? "2" : "1"; // filled/unfilled notehead code in most vexflow glyphs
- switch (notehead.Shape) {
- case NoteHeadShape.NORMAL:
- return "";
- case NoteHeadShape.DIAMOND:
- return codeStart + "D" + codeFilled;
- case NoteHeadShape.TRIANGLE:
- return codeStart + "T" + codeFilled;
- case NoteHeadShape.X:
- return codeStart + "X" + codeFilled;
- case NoteHeadShape.CIRCLEX:
- return codeStart + "X3";
- case NoteHeadShape.RECTANGLE:
- return codeStart + "R" + codeFilled;
- case NoteHeadShape.SQUARE:
- return codeStart + "S" + codeFilled;
- case NoteHeadShape.SLASH:
- return ""; // slash is specified at end of duration string in Vexflow
- default:
- return "";
- }
- }
- public static GhostNotes(frac: Fraction): Vex.Flow.GhostNote[] {
- const ghostNotes: Vex.Flow.GhostNote[] = [];
- const durations: string[] = VexFlowConverter.durations(frac, false);
- for (const duration of durations) {
- ghostNotes.push(new Vex.Flow.GhostNote({
- duration: duration,
- //dots: dots
- }));
- }
- return ghostNotes;
- }
- /**
- * Convert a GraphicalVoiceEntry to a VexFlow StaveNote
- * @param gve the GraphicalVoiceEntry which can hold a note or a chord on the staff belonging to one voice
- * @returns {Vex.Flow.StaveNote}
- */
- public static StaveNote(gve: GraphicalVoiceEntry): Vex.Flow.StaveNote {
- // if (gve.octaveShiftValue !== OctaveEnum.NONE) { // gves with accidentals in octave shift brackets can be unsorted
- gve.sortForVexflow(); // also necessary for some other cases, see test_sorted_notes... sample
- // sort and reverse replace the array anyways, so we might as well directly sort them reversely for now.
- // otherwise we should copy the array, see the commented GraphicalVoiceEntry.sortedNotesCopyForVexflow()
- // another alternative: don't sort gve notes, instead collect and sort tickables in an array,
- // then iterate over the array by addTickable() in VexFlowMeasure.graphicalMeasureCreatedCalculations()
- const notes: GraphicalNote[] = gve.notes;
- // for (const note of gve.notes) { // debug
- // const pitch: Pitch = note.sourceNote.Pitch;
- // console.log('note: ' + pitch?.ToString() + ', halftone: ' + pitch?.getHalfTone());
- // }
- const rules: EngravingRules = gve.parentStaffEntry.parentMeasure.parentSourceMeasure.Rules;
- const baseNote: GraphicalNote = notes[0];
- let keys: string[] = [];
- const accidentals: string[] = [];
- const baseNoteLength: Fraction = baseNote.graphicalNoteLength;
- const isTuplet: boolean = baseNote.sourceNote.NoteTuplet !== undefined;
- let duration: string = VexFlowConverter.durations(baseNoteLength, isTuplet)[0];
- if (baseNote.sourceNote.TypeLength !== undefined &&
- baseNote.sourceNote.TypeLength !== baseNoteLength &&
- baseNote.sourceNote.TypeLength.RealValue !== 0) {
- duration = VexFlowConverter.durations(baseNote.sourceNote.TypeLength, isTuplet)[0];
- baseNote.numberOfDots = baseNote.sourceNote.DotsXml;
- }
- let vfClefType: string = undefined;
- let numDots: number = baseNote.numberOfDots;
- let alignCenter: boolean = false;
- let xShift: number = 0;
- let slashNoteHead: boolean = false;
- let isRest: boolean = false;
- let restYPitch: Pitch;
- for (const note of notes) {
- if (numDots < note.numberOfDots) {
- numDots = note.numberOfDots;
- }
- // if it is a rest:
- if (note.sourceNote.isRest()) {
- isRest = true;
- if (note.sourceNote.Pitch) {
- const restVfPitch: [string, string, ClefInstruction] = (note as VexFlowGraphicalNote).vfpitch;
- keys = [restVfPitch[0]];
- break;
- } else {
- keys = ["b/4"]; // default placement
- // pause rest encircled by two beamed notes: place rest just below previous note
- const pauseVoiceEntry: VoiceEntry = note.parentVoiceEntry?.parentVoiceEntry;
- if (pauseVoiceEntry) {
- const neighborGSEs: GraphicalStaffEntry[] = note.parentVoiceEntry?.parentStaffEntry.parentMeasure.staffEntries;
- let previousVoiceEntry: VoiceEntry, followingVoiceEntry: VoiceEntry;
- let pauseVEIndex: number = -1;
- for (let i: number = 0; i < neighborGSEs.length; i++) {
- if (neighborGSEs[i]?.graphicalVoiceEntries[0].parentVoiceEntry === pauseVoiceEntry) {
- pauseVEIndex = i;
- break;
- }
- }
- if (pauseVEIndex >= 1 && (neighborGSEs.length - 1) >= (pauseVEIndex + 1)) {
- previousVoiceEntry = neighborGSEs[pauseVEIndex - 1]?.graphicalVoiceEntries[0]?.parentVoiceEntry;
- followingVoiceEntry = neighborGSEs[pauseVEIndex + 1]?.graphicalVoiceEntries[0]?.parentVoiceEntry;
- if (previousVoiceEntry && followingVoiceEntry) {
- const previousNote: Note = previousVoiceEntry.Notes[0];
- const followingNote: Note = followingVoiceEntry.Notes[0];
- if (previousNote.NoteBeam?.Notes.includes(followingNote)) {
- const previousNotePitch: Pitch = previousVoiceEntry.Notes.last().Pitch;
- const clef: ClefInstruction = (note as VexFlowGraphicalNote).Clef();
- const vfpitch: [string, string, ClefInstruction] = VexFlowConverter.pitch(
- VexFlowConverter.restToNotePitch(previousNotePitch.getTransposedPitch(-2), clef.ClefType),
- false, clef);
- keys = [vfpitch[0]];
- }
- }
- }
- }
- }
- // TODO do collision checking, place rest e.g. either below staff (A3, for stem direction below voice) or above (C5)
- // if it is a full measure rest:
- // (a whole rest note signifies a whole measure duration, unless the time signature is longer than 4 quarter notes, e.g. 6/4 or 3/2.
- // Note: this should not apply to most pickup measures, e.g. with an 8th pickup measure in a 3/4 time signature)
- // const measureDuration: number = note.sourceNote.SourceMeasure.Duration.RealValue;
- const isWholeMeasureRest: boolean = baseNoteLength.RealValue === note.sourceNote.SourceMeasure.ActiveTimeSignature.RealValue;
- if (isWholeMeasureRest) {
- keys = ["d/5"];
- duration = "w";
- numDots = 0;
- // If it's a whole rest we want it smack in the middle. Apparently there is still an issue in vexflow:
- // https://github.com/0xfe/vexflow/issues/579 The author reports that he needs to add some negative x shift
- // if the measure has no modifiers.
- alignCenter = true;
- xShift = rules.WholeRestXShiftVexflow * unitInPixels; // TODO find way to make dependent on the modifiers
- // affects VexFlowStaffEntry.calculateXPosition()
- }
- //If we have more than one visible voice entry, shift the rests so no collision occurs
- if (note.sourceNote.ParentStaff.Voices.length > 1) {
- const staffGves: GraphicalVoiceEntry[] = note.parentVoiceEntry.parentStaffEntry.graphicalVoiceEntries;
- //Find all visible voice entries (don't want invisible rests/notes causing visible shift)
- const restVoiceId: number = note.parentVoiceEntry.parentVoiceEntry.ParentVoice.VoiceId;
- let maxHalftone: number;
- let linesShift: number;
- for (const staffGve of staffGves) {
- for (const gveNote of staffGve.notes) {
- if (gveNote === note || gveNote.sourceNote.isRest() || !gveNote.sourceNote.PrintObject) {
- continue;
- }
- // unfortunately, we don't have functional note bounding boxes at this point,
- // so we have to infer the note positions and sizes manually.
- const wantedStemDirection: StemDirectionType = gveNote.parentVoiceEntry.parentVoiceEntry.WantedStemDirection;
- const isUpperVoiceRest: boolean = restVoiceId === 1 || restVoiceId === 5;
- const lineShiftDirection: number = isUpperVoiceRest ? 1 : -1; // voice 1: put rest above (-y). other voices: below
- const gveNotePitch: Pitch = gveNote.sourceNote.Pitch;
- const noteHalftone: number = gveNotePitch.getHalfTone();
- const newHigh: boolean = lineShiftDirection === 1 && noteHalftone > maxHalftone;
- const newLow: boolean = lineShiftDirection === -1 && noteHalftone < maxHalftone;
- if (!maxHalftone || newHigh || newLow) {
- maxHalftone = noteHalftone;
- linesShift = 0;
- // add stem length if necessary
- if (isUpperVoiceRest && wantedStemDirection === StemDirectionType.Up) {
- linesShift += 7; // rest should be above notes with up stem
- } else if (!isUpperVoiceRest && wantedStemDirection === StemDirectionType.Down) {
- linesShift += 7; // rest should be below notes with down stem
- } else if (isUpperVoiceRest) {
- linesShift += 1;
- } else {
- linesShift += 2;
- }
- if (!duration.includes("8")) { // except for 8th rests, rests are middle-aligned in vexflow (?)
- //linesShift += 3;
- if (wantedStemDirection === StemDirectionType.Up && lineShiftDirection === -1) {
- linesShift += 1; // quarter rests need a little more below upwards stems. over downwards stems it's fine.
- }
- }
- if (gveNote.sourceNote.NoteBeam) {
- linesShift += 1; // TODO this is of course rather a workaround, but the beams aren't completed yet here.
- // instead, we could calculate how many lines are between the notes of the beam,
- // and which stem of which note is longer, so its rest needs that many lines more.
- // this is more of "reverse engineering" or rather "advance engineering" the graphical notes,
- // which are unfortunately not built/drawn yet here.
- }
- if (duration.includes("w")) {
- linesShift /= 2; // TODO maybe a different fix, whole notes may need another look
- }
- linesShift += (Math.ceil(rules.RestCollisionYPadding) * 0.5); // 0.5 is smallest unit
- linesShift *= lineShiftDirection;
- note.lineShift = linesShift;
- }
- }
- }
- if (maxHalftone > 0) {
- let octaveOffset: number = 3;
- const restClefInstruction: ClefInstruction = (note as VexFlowGraphicalNote).Clef();
- switch (restClefInstruction.ClefType) {
- case ClefEnum.F:
- octaveOffset = 5;
- break;
- case ClefEnum.C:
- octaveOffset = 4;
- // if (restClefInstruction.Line == 4) // tenor clef quarter rests can be off
- break;
- default:
- break;
- }
- restYPitch = Pitch.fromHalftone(maxHalftone);
- keys = [VexFlowConverter.pitch(restYPitch, true, restClefInstruction, undefined, octaveOffset)[0]];
- }
- }
- // vfClefType seems to be undefined for rest notes, but setting it seems to break rest positioning.
- // if (!vfClefType) {
- // const clef = (note as VexFlowGraphicalNote).Clef();
- // const vexClef: any = VexFlowConverter.Clef(clef);
- // vfClefType = vexClef.type;
- // }
- break;
- }
- if (note.sourceNote.Notehead) {
- if (note.sourceNote.Notehead.Shape === NoteHeadShape.SLASH) {
- slashNoteHead = true;
- // if we have slash heads and other heads in the voice entry, this will create the same head for all.
- // same problem with numDots. The slash case should be extremely rare though.
- }
- }
- const pitch: [string, string, ClefInstruction] = (note as VexFlowGraphicalNote).vfpitch;
- keys.push(pitch[0]);
- accidentals.push(pitch[1]);
- if (!vfClefType) {
- const vfClef: {type: string, annotation: string} = VexFlowConverter.Clef(pitch[2]);
- vfClefType = vfClef.type;
- }
- }
- for (let i: number = 0, len: number = numDots; i < len; ++i) {
- duration += "d";
- }
- if (slashNoteHead) {
- duration += "s"; // we have to specify a slash note head like this in Vexflow
- }
- if (isRest) {
- // "r" has to be put after the "d"s for rest notes.
- duration += "r";
- }
- let vfnote: Vex.Flow.StaveNote;
- const vfnoteStruct: any = {
- align_center: alignCenter,
- auto_stem: true,
- clef: vfClefType,
- duration: duration,
- keys: keys,
- slash: gve.parentVoiceEntry.GraceNoteSlash,
- };
- const firstNote: Note = gve.notes[0].sourceNote;
- if (firstNote.IsCueNote) {
- vfnoteStruct.glyph_font_scale = Vex.Flow.DEFAULT_NOTATION_FONT_SCALE * Vex.Flow.GraceNote.SCALE;
- vfnoteStruct.stroke_px = Vex.Flow.GraceNote.LEDGER_LINE_OFFSET;
- }
- if (gve.parentVoiceEntry.IsGrace || gve.notes[0].sourceNote.IsCueNote) {
- vfnote = new Vex.Flow.GraceNote(vfnoteStruct);
- } else {
- vfnote = new Vex.Flow.StaveNote(vfnoteStruct);
- }
- const lineShift: number = gve.notes[0].lineShift;
- if (lineShift !== 0) {
- vfnote.getKeyProps()[0].line += lineShift;
- }
- // Annotate GraphicalNote with which line of the staff it appears on
- vfnote.getKeyProps().forEach(({ line }, i) => gve.notes[i].staffLine = line);
- if (rules.LedgerLineWidth || rules.LedgerLineStrokeStyle) {
- // FIXME should probably use vfnote.setLedgerLineStyle. this doesn't seem to do anything.
- // however, this is also set in VexFlowVoiceEntry.color() anyways.
- if (!((vfnote as any).ledgerLineStyle)) {
- (vfnote as any).ledgerLineStyle = {};
- }
- if (rules.LedgerLineWidth) {
- (vfnote as any).ledgerLineStyle.lineWidth = rules.LedgerLineWidth;
- }
- if (rules.LedgerLineStrokeStyle) {
- (vfnote as any).ledgerLineStyle.strokeStyle = rules.LedgerLineStrokeStyle;
- }
- }
- if (rules.ColoringEnabled) {
- const defaultColorStem: string = rules.DefaultColorStem;
- let stemColor: string = gve.parentVoiceEntry.StemColor;
- if (!stemColor && defaultColorStem) {
- stemColor = defaultColorStem;
- }
- const stemStyle: Object = { fillStyle: stemColor, strokeStyle: stemColor };
- if (stemColor) {
- gve.parentVoiceEntry.StemColor = stemColor;
- vfnote.setStemStyle(stemStyle);
- if (vfnote.flag && rules.ColorFlags) {
- vfnote.setFlagStyle(stemStyle);
- }
- }
- }
- vfnote.x_shift = xShift;
- if (gve.parentVoiceEntry.IsGrace && gve.notes[0].sourceNote.NoteBeam) {
- // Vexflow seems to have issues with wanted stem direction for beamed grace notes,
- // when the stem is connected to a beamed main note (e.g. Haydn Concertante bar 57)
- gve.parentVoiceEntry.WantedStemDirection = gve.notes[0].sourceNote.NoteBeam.Notes[0].ParentVoiceEntry.WantedStemDirection;
- }
- if (gve.parentVoiceEntry) {
- const wantedStemDirection: StemDirectionType = gve.parentVoiceEntry.WantedStemDirection;
- switch (wantedStemDirection) {
- case(StemDirectionType.Up):
- vfnote.setStemDirection(Vex.Flow.Stem.UP);
- gve.parentVoiceEntry.StemDirection = StemDirectionType.Up;
- break;
- case (StemDirectionType.Down):
- vfnote.setStemDirection(Vex.Flow.Stem.DOWN);
- gve.parentVoiceEntry.StemDirection = StemDirectionType.Down;
- break;
- default:
- }
- }
- // add accidentals
- for (let i: number = 0, len: number = notes.length; i < len; i += 1) {
- (notes[i] as VexFlowGraphicalNote).setIndex(vfnote, i);
- if (accidentals[i]) {
- if (accidentals[i] === "###") { // triple sharp
- vfnote.addAccidental(i, new Vex.Flow.Accidental("##"));
- vfnote.addAccidental(i, new Vex.Flow.Accidental("#"));
- continue;
- } else if (accidentals[i] === "bbs") { // triple flat
- vfnote.addAccidental(i, new Vex.Flow.Accidental("bb"));
- vfnote.addAccidental(i, new Vex.Flow.Accidental("b"));
- continue;
- }
- vfnote.addAccidental(i, new Vex.Flow.Accidental(accidentals[i])); // normal accidental
- }
- // add Tremolo strokes (only single note tremolos for now, Vexflow doesn't have beams for two-note tremolos yet)
- const tremoloStrokes: number = notes[i].sourceNote.TremoloStrokes;
- if (tremoloStrokes > 0) {
- const tremolo: Vex.Flow.Tremolo = new Vex.Flow.Tremolo(tremoloStrokes);
- (tremolo as any).extra_stroke_scale = rules.TremoloStrokeScale;
- (tremolo as any).y_spacing_scale = rules.TremoloYSpacingScale;
- vfnote.addModifier(i, tremolo);
- }
- }
- // half note tremolo: set notehead to half note (Vexflow otherwise takes the notehead from duration) (Hack)
- if (firstNote.Length.RealValue === 0.25 && firstNote.Notehead && firstNote.Notehead.Filled === false) {
- const keyProps: Object[] = vfnote.getKeyProps();
- for (let i: number = 0; i < keyProps.length; i++) {
- (<any>keyProps[i]).code = "v81";
- }
- }
- for (let i: number = 0, len: number = numDots; i < len; ++i) {
- vfnote.addDotToAll();
- }
- return vfnote;
- }
- public static generateArticulations(vfnote: Vex.Flow.StemmableNote, articulations: Articulation[],
- rules: EngravingRules): void {
- if (!vfnote || vfnote.getAttribute("type") === "GhostNote") {
- return;
- }
- for (const articulation of articulations) {
- let vfArtPosition: number = Vex.Flow.Modifier.Position.ABOVE;
- if (vfnote.getStemDirection() === Vex.Flow.Stem.UP) {
- vfArtPosition = Vex.Flow.Modifier.Position.BELOW;
- }
- let vfArt: Vex.Flow.Articulation = undefined;
- const articulationEnum: ArticulationEnum = articulation.articulationEnum;
- if (rules.ArticulationPlacementFromXML) {
- if (articulation.placement === PlacementEnum.Above) {
- vfArtPosition = Vex.Flow.Modifier.Position.ABOVE;
- } else if (articulation.placement === PlacementEnum.Below) {
- vfArtPosition = Vex.Flow.Modifier.Position.BELOW;
- } // else if undefined: don't change
- }
- switch (articulationEnum) {
- case ArticulationEnum.accent: {
- vfArt = new Vex.Flow.Articulation("a>");
- break;
- }
- case ArticulationEnum.downbow: {
- vfArt = new Vex.Flow.Articulation("am");
- if (articulation.placement === undefined) { // downbow/upbow should be above by default
- vfArtPosition = Vex.Flow.Modifier.Position.ABOVE;
- }
- break;
- }
- case ArticulationEnum.fermata: {
- vfArt = new Vex.Flow.Articulation("a@a");
- vfArtPosition = Vex.Flow.Modifier.Position.ABOVE;
- break;
- }
- case ArticulationEnum.marcatodown: {
- vfArt = new Vex.Flow.Articulation("a|"); // Vexflow only knows marcato up, so we use a down stroke here.
- break;
- }
- case ArticulationEnum.marcatoup: {
- vfArt = new Vex.Flow.Articulation("a^");
- break;
- }
- case ArticulationEnum.invertedfermata: {
- vfArt = new Vex.Flow.Articulation("a@u");
- vfArtPosition = Vex.Flow.Modifier.Position.BELOW;
- break;
- }
- case ArticulationEnum.lefthandpizzicato: {
- vfArt = new Vex.Flow.Articulation("a+");
- break;
- }
- case ArticulationEnum.naturalharmonic: {
- vfArt = new Vex.Flow.Articulation("ah");
- break;
- }
- case ArticulationEnum.snappizzicato: {
- vfArt = new Vex.Flow.Articulation("ao");
- break;
- }
- case ArticulationEnum.staccatissimo: {
- vfArt = new Vex.Flow.Articulation("av");
- break;
- }
- case ArticulationEnum.staccato: {
- vfArt = new Vex.Flow.Articulation("a.");
- break;
- }
- case ArticulationEnum.tenuto: {
- vfArt = new Vex.Flow.Articulation("a-");
- break;
- }
- case ArticulationEnum.upbow: {
- vfArt = new Vex.Flow.Articulation("a|");
- if (articulation.placement === undefined) { // downbow/upbow should be above by default
- vfArtPosition = Vex.Flow.Modifier.Position.ABOVE;
- }
- break;
- }
- case ArticulationEnum.strongaccent: {
- vfArt = new Vex.Flow.Articulation("a^");
- break;
- }
- default: {
- break;
- }
- }
- if (vfArt) {
- vfArt.setPosition(vfArtPosition);
- (vfnote as StaveNote).addModifier(0, vfArt);
- }
- }
- }
- public static generateOrnaments(vfnote: Vex.Flow.StemmableNote, oContainer: OrnamentContainer): void {
- let vfPosition: number = Vex.Flow.Modifier.Position.ABOVE;
- if (oContainer.placement === PlacementEnum.Below) {
- vfPosition = Vex.Flow.Modifier.Position.BELOW;
- }
- let vfOrna: Vex.Flow.Ornament = undefined;
- switch (oContainer.GetOrnament) {
- case OrnamentEnum.DelayedInvertedTurn: {
- vfOrna = new Vex.Flow.Ornament("turn_inverted");
- vfOrna.setDelayed(true);
- break;
- }
- case OrnamentEnum.DelayedTurn: {
- vfOrna = new Vex.Flow.Ornament("turn");
- vfOrna.setDelayed(true);
- break;
- }
- case OrnamentEnum.InvertedMordent: {
- vfOrna = new Vex.Flow.Ornament("mordent"); // Vexflow uses baroque, not MusicXML definition
- vfOrna.setDelayed(false);
- break;
- }
- case OrnamentEnum.InvertedTurn: {
- vfOrna = new Vex.Flow.Ornament("turn_inverted");
- vfOrna.setDelayed(false);
- break;
- }
- case OrnamentEnum.Mordent: {
- vfOrna = new Vex.Flow.Ornament("mordent_inverted");
- vfOrna.setDelayed(false);
- break;
- }
- case OrnamentEnum.Trill: {
- vfOrna = new Vex.Flow.Ornament("tr");
- vfOrna.setDelayed(false);
- break;
- }
- case OrnamentEnum.Turn: {
- vfOrna = new Vex.Flow.Ornament("turn");
- vfOrna.setDelayed(false);
- break;
- }
- default: {
- log.warn("unhandled OrnamentEnum type: " + oContainer.GetOrnament);
- return;
- }
- }
- if (vfOrna) {
- if (oContainer.AccidentalBelow !== AccidentalEnum.NONE) {
- vfOrna.setLowerAccidental(Pitch.accidentalVexflow(oContainer.AccidentalBelow));
- }
- if (oContainer.AccidentalAbove !== AccidentalEnum.NONE) {
- vfOrna.setUpperAccidental(Pitch.accidentalVexflow(oContainer.AccidentalAbove));
- }
- vfOrna.setPosition(vfPosition); // Vexflow draws it above right now in any case, never below
- (vfnote as StaveNote).addModifier(0, vfOrna);
- }
- }
- public static StrokeTypeFromArpeggioType(arpeggioType: ArpeggioType): Vex.Flow.Stroke.Type {
- switch (arpeggioType) {
- case ArpeggioType.ARPEGGIO_DIRECTIONLESS:
- return Vex.Flow.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
- case ArpeggioType.BRUSH_DOWN:
- return Vex.Flow.Stroke.Type.BRUSH_UP; // TODO somehow up and down are mixed up in Vexflow right now
- case ArpeggioType.BRUSH_UP:
- return Vex.Flow.Stroke.Type.BRUSH_DOWN; // TODO somehow up and down are mixed up in Vexflow right now
- case ArpeggioType.RASQUEDO_DOWN:
- return Vex.Flow.Stroke.Type.RASQUEDO_UP;
- case ArpeggioType.RASQUEDO_UP:
- return Vex.Flow.Stroke.Type.RASQUEDO_DOWN;
- case ArpeggioType.ROLL_DOWN:
- return Vex.Flow.Stroke.Type.ROLL_UP; // TODO somehow up and down are mixed up in Vexflow right now
- case ArpeggioType.ROLL_UP:
- return Vex.Flow.Stroke.Type.ROLL_DOWN; // TODO somehow up and down are mixed up in Vexflow right now
- default:
- return Vex.Flow.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
- }
- }
- /**
- * Convert a set of GraphicalNotes to a VexFlow StaveNote
- * @param notes form a chord on the staff
- * @returns {Vex.Flow.StaveNote}
- */
- public static CreateTabNote(gve: GraphicalVoiceEntry): Vex.Flow.TabNote {
- const tabPositions: {str: number, fret: number}[] = [];
- const notes: GraphicalNote[] = gve.notes.reverse();
- const tabPhrases: { type: number, text: string, width: number }[] = [];
- const frac: Fraction = gve.notes[0].graphicalNoteLength;
- const isTuplet: boolean = gve.notes[0].sourceNote.NoteTuplet !== undefined;
- let duration: string = VexFlowConverter.durations(frac, isTuplet)[0];
- let numDots: number = 0;
- for (const note of gve.notes) {
- const tabNote: TabNote = note.sourceNote as TabNote;
- const tabPosition: {str: number, fret: number} = {str: tabNote.StringNumberTab, fret: tabNote.FretNumber};
- tabPositions.push(tabPosition);
- if (tabNote.BendArray) {
- tabNote.BendArray.forEach( function( bend: {bendalter: number, direction: string} ): void {
- let phraseText: string;
- const phraseStep: number = bend.bendalter - tabPosition.fret;
- if (phraseStep > 1) {
- phraseText = "Full";
- } else if (phraseStep === 1) {
- phraseText = "1/2";
- } else {
- phraseText = "1/4";
- }
- if (bend.direction === "up") {
- tabPhrases.push({type: Vex.Flow.Bend.UP, text: phraseText, width: 10});
- } else {
- tabPhrases.push({type: Vex.Flow.Bend.DOWN, text: phraseText, width: 10});
- }
- });
- }
- if (numDots < note.numberOfDots) {
- numDots = note.numberOfDots;
- }
- }
- for (let i: number = 0, len: number = numDots; i < len; ++i) {
- duration += "d";
- }
- const vfnote: Vex.Flow.TabNote = new Vex.Flow.TabNote({
- duration: duration,
- positions: tabPositions,
- });
- for (let i: number = 0, len: number = notes.length; i < len; i += 1) {
- (notes[i] as VexFlowGraphicalNote).setIndex(vfnote, i);
- }
- tabPhrases.forEach(function(phrase: { type: number, text: string, width: number }): void {
- if (phrase.type === Vex.Flow.Bend.UP) {
- vfnote.addModifier (new Vex.Flow.Bend(phrase.text, false));
- } else {
- vfnote.addModifier (new Vex.Flow.Bend(phrase.text, true));
- }
- });
- return vfnote;
- }
- /**
- * Convert a ClefInstruction to a string represention of a clef type in VexFlow.
- *
- * @param clef The OSMD object to be converted representing the clef
- * @param size The VexFlow size to be used. Can be `default` or `small`.
- * As soon as #118 is done, this parameter will be dispensable.
- * @returns A string representation of a VexFlow clef
- * @see https://github.com/0xfe/vexflow/blob/master/src/clef.js
- * @see https://github.com/0xfe/vexflow/blob/master/tests/clef_tests.js
- */
- public static Clef(clef: ClefInstruction, size: string = "default"): { type: string, size: string, annotation: string } {
- let type: string;
- let annotation: string;
- // Make sure size is either "default" or "small"
- if (size !== "default" && size !== "small") {
- log.warn(`Invalid VexFlow clef size "${size}" specified. Using "default".`);
- size = "default";
- }
- /*
- * For all of the following conversions, OSMD uses line numbers 1-5 starting from
- * the bottom, while VexFlow uses 0-4 starting from the top.
- */
- switch (clef.ClefType) {
- // G Clef
- case ClefEnum.G:
- switch (clef.Line) {
- case 1:
- type = "french"; // VexFlow line 4
- break;
- case 2:
- type = "treble"; // VexFlow line 3
- break;
- default:
- type = "treble";
- log.error(`Clef ${ClefEnum[clef.ClefType]} on line ${clef.Line} not supported by VexFlow. Using default value "${type}".`);
- }
- break;
- // F Clef
- case ClefEnum.F:
- switch (clef.Line) {
- case 4:
- type = "bass"; // VexFlow line 1
- break;
- case 3:
- type = "baritone-f"; // VexFlow line 2
- break;
- case 5:
- type = "subbass"; // VexFlow line 0
- break;
- default:
- type = "bass";
- log.error(`Clef ${ClefEnum[clef.ClefType]} on line ${clef.Line} not supported by VexFlow. Using default value "${type}".`);
- }
- break;
- // C Clef
- case ClefEnum.C:
- switch (clef.Line) {
- case 3:
- type = "alto"; // VexFlow line 2
- break;
- case 4:
- type = "tenor"; // VexFlow line 1
- break;
- case 1:
- type = "soprano"; // VexFlow line 4
- break;
- case 2:
- type = "mezzo-soprano"; // VexFlow line 3
- break;
- default:
- type = "alto";
- log.error(`Clef ${ClefEnum[clef.ClefType]} on line ${clef.Line} not supported by VexFlow. Using default value "${type}".`);
- }
- break;
- // Percussion Clef
- case ClefEnum.percussion:
- type = "percussion";
- break;
- // TAB Clef
- case ClefEnum.TAB:
- // only used currently for creating the notes in the normal stave: There we need a normal treble clef
- type = "treble";
- break;
- default:
- log.info("bad clef type: " + clef.ClefType);
- type = "treble";
- }
- // annotations in vexflow don't allow bass and 8va. No matter the offset :(
- if (clef.OctaveOffset === 1 && type !== "bass" ) {
- annotation = "8va";
- } else if (clef.OctaveOffset === -1) {
- annotation = "8vb";
- }
- return { type, size, annotation };
- }
- /**
- * Convert a RhythmInstruction to a VexFlow TimeSignature object
- * @param rhythm
- * @returns {Vex.Flow.TimeSignature}
- * @constructor
- */
- public static TimeSignature(rhythm: RhythmInstruction): Vex.Flow.TimeSignature {
- let timeSpec: string;
- switch (rhythm.SymbolEnum) {
- case RhythmSymbolEnum.NONE:
- timeSpec = rhythm.Rhythm.Numerator + "/" + rhythm.Rhythm.Denominator;
- break;
- case RhythmSymbolEnum.COMMON:
- timeSpec = "C";
- break;
- case RhythmSymbolEnum.CUT:
- timeSpec = "C|";
- break;
- default:
- }
- return new Vex.Flow.TimeSignature(timeSpec);
- }
- /**
- * Convert a KeyInstruction to a string representing in VexFlow a key
- * @param key
- * @returns {string}
- */
- public static keySignature(key: KeyInstruction): string {
- if (!key) {
- return undefined;
- }
- let ret: string;
- switch (key.Mode) {
- case KeyEnum.minor:
- ret = VexFlowConverter.minorMap[key.Key] + "m";
- break;
- case KeyEnum.major:
- ret = VexFlowConverter.majorMap[key.Key];
- break;
- // some XMLs don't have the mode set despite having a key signature.
- case KeyEnum.none:
- ret = VexFlowConverter.majorMap[key.Key];
- break;
- default:
- ret = "C";
- }
- return ret;
- }
- /**
- * Converts a lineType to a VexFlow StaveConnector type
- * @param lineType
- * @returns {any}
- */
- public static line(lineType: SystemLinesEnum, linePosition: SystemLinePosition): any {
- switch (lineType) {
- case SystemLinesEnum.SingleThin:
- if (linePosition === SystemLinePosition.MeasureBegin) {
- return Vex.Flow.StaveConnector.type.SINGLE;
- }
- return Vex.Flow.StaveConnector.type.SINGLE_RIGHT;
- case SystemLinesEnum.DoubleThin:
- return Vex.Flow.StaveConnector.type.THIN_DOUBLE;
- case SystemLinesEnum.ThinBold:
- return Vex.Flow.StaveConnector.type.BOLD_DOUBLE_RIGHT;
- case SystemLinesEnum.BoldThinDots:
- return Vex.Flow.StaveConnector.type.BOLD_DOUBLE_LEFT;
- case SystemLinesEnum.DotsThinBold:
- return Vex.Flow.StaveConnector.type.BOLD_DOUBLE_RIGHT;
- case SystemLinesEnum.DotsBoldBoldDots:
- return Vex.Flow.StaveConnector.type.BOLD_DOUBLE_RIGHT;
- case SystemLinesEnum.None:
- return Vex.Flow.StaveConnector.type.NONE;
- default:
- }
- }
- /**
- * Construct a string which can be used in a CSS font property
- * @param fontSize
- * @param fontStyle
- * @param font
- * @returns {string}
- */
- public static font(fontSize: number, fontStyle: FontStyles = FontStyles.Regular,
- font: Fonts = Fonts.TimesNewRoman, rules: EngravingRules, fontFamily: string = undefined): string {
- let style: string = "normal";
- let weight: string = "normal";
- let family: string = `'${rules.DefaultFontFamily}'`; // default "'Times New Roman'"
- switch (fontStyle) {
- case FontStyles.Bold:
- weight = "bold";
- break;
- case FontStyles.Italic:
- style = "italic";
- break;
- case FontStyles.BoldItalic:
- style = "italic";
- weight = "bold";
- break;
- case FontStyles.Underlined:
- // TODO
- break;
- default:
- break;
- }
- switch (font) { // currently not used
- case Fonts.Kokila:
- // TODO Not Supported
- break;
- default:
- }
- if (fontFamily && fontFamily !== "default") {
- family = `'${fontFamily}'`;
- }
- return style + " " + weight + " " + Math.floor(fontSize) + "px " + family;
- }
- /**
- * Converts the style into a string that VexFlow RenderContext can understand
- * as the weight of the font
- */
- public static fontStyle(style: FontStyles): string {
- switch (style) {
- case FontStyles.Bold:
- return "bold";
- case FontStyles.Italic:
- return "italic";
- case FontStyles.BoldItalic:
- return "italic bold";
- default:
- return "normal";
- }
- }
- /**
- * Convert OutlineAndFillStyle to CSS properties
- * @param styleId
- * @returns {string}
- */
- public static style(styleId: OutlineAndFillStyleEnum): string {
- const ret: string = OUTLINE_AND_FILL_STYLE_DICT.getValue(styleId);
- return ret;
- }
- }
|