VexFlowConverter.ts 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245
  1. import Vex from "vexflow";
  2. import VF = Vex.Flow;
  3. import {ClefEnum} from "../../VoiceData/Instructions/ClefInstruction";
  4. import {ClefInstruction} from "../../VoiceData/Instructions/ClefInstruction";
  5. import {Pitch} from "../../../Common/DataObjects/Pitch";
  6. import {Fraction} from "../../../Common/DataObjects/Fraction";
  7. import {RhythmInstruction} from "../../VoiceData/Instructions/RhythmInstruction";
  8. import {RhythmSymbolEnum} from "../../VoiceData/Instructions/RhythmInstruction";
  9. import {KeyInstruction} from "../../VoiceData/Instructions/KeyInstruction";
  10. import {KeyEnum} from "../../VoiceData/Instructions/KeyInstruction";
  11. import {AccidentalEnum} from "../../../Common/DataObjects/Pitch";
  12. import {NoteEnum} from "../../../Common/DataObjects/Pitch";
  13. import {VexFlowGraphicalNote} from "./VexFlowGraphicalNote";
  14. import {GraphicalNote} from "../GraphicalNote";
  15. import {SystemLinesEnum} from "../SystemLinesEnum";
  16. import {FontStyles} from "../../../Common/Enums/FontStyles";
  17. import {Fonts} from "../../../Common/Enums/Fonts";
  18. import {OutlineAndFillStyleEnum, OUTLINE_AND_FILL_STYLE_DICT} from "../DrawingEnums";
  19. import log from "loglevel";
  20. import { ArticulationEnum, StemDirectionType, VoiceEntry } from "../../VoiceData/VoiceEntry";
  21. import { SystemLinePosition } from "../SystemLinePosition";
  22. import { GraphicalVoiceEntry } from "../GraphicalVoiceEntry";
  23. import { OrnamentEnum, OrnamentContainer } from "../../VoiceData/OrnamentContainer";
  24. import { Notehead, NoteHeadShape } from "../../VoiceData/Notehead";
  25. import { unitInPixels } from "./VexFlowMusicSheetDrawer";
  26. import { EngravingRules } from "../EngravingRules";
  27. import { Note } from "../../../MusicalScore/VoiceData/Note";
  28. import StaveNote = VF.StaveNote;
  29. import { ArpeggioType } from "../../VoiceData/Arpeggio";
  30. import { TabNote } from "../../VoiceData/TabNote";
  31. import { PlacementEnum } from "../../VoiceData/Expressions/AbstractExpression";
  32. import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
  33. import { Slur } from "../../VoiceData/Expressions/ContinuousExpressions/Slur";
  34. import { GraphicalLyricEntry } from "../GraphicalLyricEntry";
  35. import { GraphicalMeasure } from "../GraphicalMeasure";
  36. /**
  37. * Helper class, which contains static methods which actually convert
  38. * from OSMD objects to VexFlow objects.
  39. */
  40. export class VexFlowConverter {
  41. /**
  42. * Mapping from numbers of alterations on the key signature to major keys
  43. * @type {[alterationsNo: number]: string; }
  44. */
  45. private static majorMap: {[_: number]: string } = {
  46. "-1": "F", "-2": "Bb", "-3": "Eb", "-4": "Ab", "-5": "Db", "-6": "Gb", "-7": "Cb", "-8": "Fb",
  47. "0": "C", "1": "G", "2": "D", "3": "A", "4": "E", "5": "B", "6": "F#", "7": "C#", "8": "G#"
  48. };
  49. /**
  50. * Mapping from numbers of alterations on the key signature to minor keys
  51. * @type {[alterationsNo: number]: string; }
  52. */
  53. private static minorMap: {[_: number]: string } = {
  54. "-1": "D", "-2": "G", "-3": "C", "-4": "F", "-5": "Bb", "-6": "Eb", "-7": "Ab", "-8": "Db",
  55. "0": "A", "1": "E", "2": "B", "3": "F#", "4": "C#", "5": "G#", "6": "D#", "7": "A#", "8": "E#"
  56. };
  57. /**
  58. * Convert a fraction to Vexflow string durations.
  59. * A duration like 5/16 (5 16th notes) can't be represented by a single (dotted) note,
  60. * so we need to return multiple durations (e.g. for 5/16th ghost notes).
  61. * Currently, for a dotted quarter ghost note, we return a quarter and an eighth ghost note.
  62. * We could return a dotted quarter instead, but then the code would need to distinguish between
  63. * notes that can be represented as dotted notes and notes that can't, which would complicate things.
  64. * We could e.g. add a parameter "allowSingleDottedNote" which makes it possible to return single dotted notes instead.
  65. * But currently, this is only really used for Ghost notes, so it doesn't make a difference visually.
  66. * (for other uses like StaveNotes, we calculate the dots separately)
  67. * @param fraction a fraction representing the duration of a note
  68. * @returns {string[]} Vexflow note type strings (e.g. "h" = half note)
  69. */
  70. public static durations(fraction: Fraction, isTuplet: boolean): string[] {
  71. const durations: string[] = [];
  72. const remainingFraction: Fraction = fraction.clone();
  73. while (remainingFraction.RealValue > 0) {
  74. const dur: number = remainingFraction.RealValue;
  75. // TODO consider long (dur=4) and maxima (dur=8), though Vexflow doesn't seem to support them
  76. if (dur >= 2) { // Breve
  77. durations.push("1/2");
  78. remainingFraction.Sub(new Fraction(2, 1));
  79. } else if (dur >= 1) {
  80. durations.push("w");
  81. remainingFraction.Sub(new Fraction(1, 1));
  82. } else if (dur < 1 && dur >= 0.5) {
  83. // change to the next higher straight note to get the correct note display type
  84. if (isTuplet && dur > 0.5) {
  85. return ["w"];
  86. } else {
  87. durations.push("h");
  88. remainingFraction.Sub(new Fraction(1, 2));
  89. }
  90. } else if (dur < 0.5 && dur >= 0.25) {
  91. // change to the next higher straight note to get the correct note display type
  92. if (isTuplet && dur > 0.25) {
  93. return ["h"];
  94. } else {
  95. durations.push("q");
  96. remainingFraction.Sub(new Fraction(1, 4));
  97. }
  98. } else if (dur < 0.25 && dur >= 0.125) {
  99. // change to the next higher straight note to get the correct note display type
  100. if (isTuplet && dur > 0.125) {
  101. return ["q"];
  102. } else {
  103. durations.push("8");
  104. remainingFraction.Sub(new Fraction(1, 8));
  105. }
  106. } else if (dur < 0.125 && dur >= 0.0625) {
  107. // change to the next higher straight note to get the correct note display type
  108. if (isTuplet && dur > 0.0625) {
  109. return ["8"];
  110. } else {
  111. durations.push("16");
  112. remainingFraction.Sub(new Fraction(1, 16));
  113. }
  114. } else if (dur < 0.0625 && dur >= 0.03125) {
  115. // change to the next higher straight note to get the correct note display type
  116. if (isTuplet && dur > 0.03125) {
  117. return ["16"];
  118. } else {
  119. durations.push("32");
  120. remainingFraction.Sub(new Fraction(1, 32));
  121. }
  122. } else if (dur < 0.03125 && dur >= 0.015625) {
  123. // change to the next higher straight note to get the correct note display type
  124. if (isTuplet && dur > 0.015625) {
  125. return ["32"];
  126. } else {
  127. durations.push("64");
  128. remainingFraction.Sub(new Fraction(1, 64));
  129. }
  130. } else {
  131. if (isTuplet) {
  132. return ["64"];
  133. } else {
  134. durations.push("128");
  135. remainingFraction.Sub(new Fraction(1, 128));
  136. }
  137. }
  138. }
  139. // if (isTuplet) {
  140. // dots = 0; // TODO (different) calculation?
  141. // } else {
  142. // dots = fraction.calculateNumberOfNeededDots();
  143. // }
  144. return durations;
  145. }
  146. /**
  147. * Takes a Pitch and returns a string representing a VexFlow pitch,
  148. * which has the form "b/4", plus its alteration (accidental)
  149. * @param pitch
  150. * @returns {string[]}
  151. */
  152. public static pitch(pitch: Pitch, isRest: boolean, clef: ClefInstruction,
  153. notehead: Notehead = undefined, octaveOffsetGiven: number = undefined): [string, string, ClefInstruction] {
  154. //FIXME: The octave seems to need a shift of three?
  155. //FIXME: Also rests seem to use different offsets depending on the clef.
  156. let octaveOffset: number = octaveOffsetGiven;
  157. if (octaveOffsetGiven === undefined) {
  158. octaveOffset = 3;
  159. }
  160. if (isRest && octaveOffsetGiven === undefined) {
  161. octaveOffset = 0;
  162. if (clef.ClefType === ClefEnum.F) {
  163. octaveOffset = 2;
  164. }
  165. if (clef.ClefType === ClefEnum.C) {
  166. octaveOffset = 2;
  167. }
  168. // TODO the pitch for rests will be the start position, for eights rests it will be the bottom point
  169. // maybe we want to center on the display position instead of having the bottom there?
  170. }
  171. const fund: string = NoteEnum[pitch.FundamentalNote].toLowerCase();
  172. const acc: string = Pitch.accidentalVexflow(pitch.Accidental);
  173. const octave: number = pitch.Octave - clef.OctaveOffset + octaveOffset;
  174. let noteheadCode: string = "";
  175. if (notehead) {
  176. noteheadCode = this.NoteHeadCode(notehead);
  177. }
  178. return [fund + "n/" + octave + noteheadCode, acc, clef];
  179. }
  180. public static restToNotePitch(pitch: Pitch, clefType: ClefEnum): Pitch {
  181. let octave: number = pitch.Octave;
  182. // offsets see pitch()
  183. switch (clefType) {
  184. case ClefEnum.C:
  185. case ClefEnum.F: {
  186. octave += 2;
  187. break;
  188. }
  189. case ClefEnum.G:
  190. default:
  191. }
  192. return new Pitch(pitch.FundamentalNote, octave, AccidentalEnum.NONE);
  193. }
  194. /** returns the Vexflow code for a note head. Some are still unsupported, see Vexflow/tables.js */
  195. public static NoteHeadCode(notehead: Notehead): string {
  196. const codeStart: string = "/";
  197. const codeFilled: string = notehead.Filled ? "2" : "1"; // filled/unfilled notehead code in most vexflow glyphs
  198. switch (notehead.Shape) {
  199. case NoteHeadShape.NORMAL:
  200. return "";
  201. case NoteHeadShape.DIAMOND:
  202. return codeStart + "D" + codeFilled;
  203. case NoteHeadShape.TRIANGLE:
  204. return codeStart + "T" + codeFilled;
  205. case NoteHeadShape.TRIANGLE_INVERTED:
  206. return codeStart + "TI";
  207. case NoteHeadShape.X:
  208. return codeStart + "X" + codeFilled;
  209. case NoteHeadShape.CIRCLEX:
  210. return codeStart + "X3";
  211. case NoteHeadShape.RECTANGLE:
  212. return codeStart + "R" + codeFilled;
  213. case NoteHeadShape.SQUARE:
  214. return codeStart + "S" + codeFilled;
  215. case NoteHeadShape.SLASH:
  216. return ""; // slash is specified at end of duration string in Vexflow
  217. default:
  218. return "";
  219. }
  220. }
  221. public static GhostNotes(frac: Fraction): VF.GhostNote[] {
  222. const ghostNotes: VF.GhostNote[] = [];
  223. const durations: string[] = VexFlowConverter.durations(frac, false);
  224. for (const duration of durations) {
  225. ghostNotes.push(new VF.GhostNote({
  226. duration: duration,
  227. //dots: dots
  228. }));
  229. }
  230. return ghostNotes;
  231. }
  232. /**
  233. * Convert a GraphicalVoiceEntry to a VexFlow StaveNote
  234. * @param gve the GraphicalVoiceEntry which can hold a note or a chord on the staff belonging to one voice
  235. * @returns {VF.StaveNote}
  236. */
  237. public static StaveNote(gve: GraphicalVoiceEntry): VF.StaveNote {
  238. // if (gve.octaveShiftValue !== OctaveEnum.NONE) { // gves with accidentals in octave shift brackets can be unsorted
  239. gve.sortForVexflow(); // also necessary for some other cases, see test_sorted_notes... sample
  240. // sort and reverse replace the array anyways, so we might as well directly sort them reversely for now.
  241. // otherwise we should copy the array, see the commented GraphicalVoiceEntry.sortedNotesCopyForVexflow()
  242. // another alternative: don't sort gve notes, instead collect and sort tickables in an array,
  243. // then iterate over the array by addTickable() in VexFlowMeasure.graphicalMeasureCreatedCalculations()
  244. const notes: GraphicalNote[] = gve.notes;
  245. // for (const note of gve.notes) { // debug
  246. // const pitch: Pitch = note.sourceNote.Pitch;
  247. // console.log('note: ' + pitch?.ToString() + ', halftone: ' + pitch?.getHalfTone());
  248. // }
  249. const rules: EngravingRules = gve.parentStaffEntry.parentMeasure.parentSourceMeasure.Rules;
  250. const baseNote: GraphicalNote = notes[0];
  251. let keys: string[] = [];
  252. const accidentals: string[] = [];
  253. const baseNoteLength: Fraction = baseNote.graphicalNoteLength;
  254. const isTuplet: boolean = baseNote.sourceNote.NoteTuplet !== undefined;
  255. let duration: string = VexFlowConverter.durations(baseNoteLength, isTuplet)[0];
  256. if (baseNote.sourceNote.TypeLength !== undefined &&
  257. baseNote.sourceNote.TypeLength !== baseNoteLength &&
  258. baseNote.sourceNote.TypeLength.RealValue !== 0) {
  259. duration = VexFlowConverter.durations(baseNote.sourceNote.TypeLength, isTuplet)[0];
  260. baseNote.numberOfDots = baseNote.sourceNote.DotsXml;
  261. }
  262. let vfClefType: string = undefined;
  263. let numDots: number = baseNote.numberOfDots;
  264. let alignCenter: boolean = false;
  265. let xShift: number = 0;
  266. let isRest: boolean = false;
  267. let restYPitch: Pitch;
  268. for (const note of notes) {
  269. if (numDots < note.numberOfDots) {
  270. numDots = note.numberOfDots;
  271. }
  272. // if it is a rest:
  273. if (note.sourceNote.isRest()) {
  274. isRest = true;
  275. if (note.sourceNote.Pitch) {
  276. const restVfPitch: [string, string, ClefInstruction] = (note as VexFlowGraphicalNote).vfpitch;
  277. keys = [restVfPitch[0]];
  278. break;
  279. } else {
  280. keys = ["b/4"]; // default placement
  281. // pause rest encircled by two beamed notes: place rest just below previous note
  282. const pauseVoiceEntry: VoiceEntry = note.parentVoiceEntry?.parentVoiceEntry;
  283. if (pauseVoiceEntry) {
  284. const neighborGSEs: GraphicalStaffEntry[] = note.parentVoiceEntry?.parentStaffEntry.parentMeasure.staffEntries;
  285. let previousVoiceEntry: VoiceEntry, followingVoiceEntry: VoiceEntry;
  286. let pauseVEIndex: number = -1;
  287. for (let i: number = 0; i < neighborGSEs.length; i++) {
  288. if (neighborGSEs[i]?.graphicalVoiceEntries[0].parentVoiceEntry === pauseVoiceEntry) {
  289. pauseVEIndex = i;
  290. break;
  291. }
  292. }
  293. if (pauseVEIndex >= 1 && (neighborGSEs.length - 1) >= (pauseVEIndex + 1)) {
  294. previousVoiceEntry = neighborGSEs[pauseVEIndex - 1]?.graphicalVoiceEntries[0]?.parentVoiceEntry;
  295. followingVoiceEntry = neighborGSEs[pauseVEIndex + 1]?.graphicalVoiceEntries[0]?.parentVoiceEntry;
  296. if (previousVoiceEntry && followingVoiceEntry) {
  297. const previousNote: Note = previousVoiceEntry.Notes[0];
  298. const followingNote: Note = followingVoiceEntry.Notes[0];
  299. if (previousNote.NoteBeam?.Notes.includes(followingNote)) {
  300. const previousNotePitch: Pitch = previousVoiceEntry.Notes.last().Pitch;
  301. const clef: ClefInstruction = (note as VexFlowGraphicalNote).Clef();
  302. const vfpitch: [string, string, ClefInstruction] = VexFlowConverter.pitch(
  303. VexFlowConverter.restToNotePitch(previousNotePitch.getTransposedPitch(-2), clef.ClefType),
  304. false, clef);
  305. keys = [vfpitch[0]];
  306. }
  307. }
  308. }
  309. }
  310. }
  311. // TODO do collision checking, place rest e.g. either below staff (A3, for stem direction below voice) or above (C5)
  312. // if it is a full measure rest:
  313. // (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.
  314. // Note: this should not apply to most pickup measures, e.g. with an 8th pickup measure in a 3/4 time signature)
  315. // const measureDuration: number = note.sourceNote.SourceMeasure.Duration.RealValue;
  316. const isWholeMeasureRest: boolean = note.sourceNote.IsWholeMeasureRest ||
  317. baseNoteLength.RealValue === note.sourceNote.SourceMeasure.ActiveTimeSignature.RealValue;
  318. if (isWholeMeasureRest) {
  319. keys = ["d/5"];
  320. if (gve.parentStaffEntry.parentMeasure.ParentStaff.StafflineCount === 1) {
  321. keys = ["b/4"];
  322. }
  323. duration = "w";
  324. numDots = 0;
  325. // If it's a whole rest we want it smack in the middle. Apparently there is still an issue in vexflow:
  326. // https://github.com/0xfe/vexflow/issues/579 The author reports that he needs to add some negative x shift
  327. // if the measure has no modifiers.
  328. alignCenter = true;
  329. xShift = rules.WholeRestXShiftVexflow * unitInPixels; // TODO find way to make dependent on the modifiers
  330. // affects VexFlowStaffEntry.calculateXPosition()
  331. }
  332. //If we have more than one visible voice entry, shift the rests so no collision occurs
  333. if (note.sourceNote.ParentStaff.Voices.length > 1) {
  334. const staffGves: GraphicalVoiceEntry[] = note.parentVoiceEntry.parentStaffEntry.graphicalVoiceEntries;
  335. //Find all visible voice entries (don't want invisible rests/notes causing visible shift)
  336. const restVoiceId: number = note.parentVoiceEntry.parentVoiceEntry.ParentVoice.VoiceId;
  337. let maxHalftone: number;
  338. let linesShift: number;
  339. for (const staffGve of staffGves) {
  340. for (const gveNote of staffGve.notes) {
  341. if (gveNote === note || gveNote.sourceNote.isRest() || !gveNote.sourceNote.PrintObject) {
  342. continue;
  343. }
  344. // unfortunately, we don't have functional note bounding boxes at this point,
  345. // so we have to infer the note positions and sizes manually.
  346. const wantedStemDirection: StemDirectionType = gveNote.parentVoiceEntry.parentVoiceEntry.WantedStemDirection;
  347. const isUpperVoiceRest: boolean = restVoiceId === 1 || restVoiceId === 5;
  348. const lineShiftDirection: number = isUpperVoiceRest ? 1 : -1; // voice 1: put rest above (-y). other voices: below
  349. const gveNotePitch: Pitch = gveNote.sourceNote.Pitch;
  350. const noteHalftone: number = gveNotePitch.getHalfTone();
  351. const newHigh: boolean = lineShiftDirection === 1 && noteHalftone > maxHalftone;
  352. const newLow: boolean = lineShiftDirection === -1 && noteHalftone < maxHalftone;
  353. if (!maxHalftone || newHigh || newLow) {
  354. maxHalftone = noteHalftone;
  355. linesShift = 0;
  356. // add stem length if necessary
  357. if (isUpperVoiceRest && wantedStemDirection === StemDirectionType.Up) {
  358. linesShift += 7; // rest should be above notes with up stem
  359. } else if (!isUpperVoiceRest && wantedStemDirection === StemDirectionType.Down) {
  360. linesShift += 7; // rest should be below notes with down stem
  361. } else if (isUpperVoiceRest) {
  362. linesShift += 1;
  363. } else {
  364. linesShift += 2;
  365. }
  366. if (!duration.includes("8")) { // except for 8th rests, rests are middle-aligned in vexflow (?)
  367. //linesShift += 3;
  368. if (wantedStemDirection === StemDirectionType.Up && lineShiftDirection === -1) {
  369. linesShift += 1; // quarter rests need a little more below upwards stems. over downwards stems it's fine.
  370. }
  371. }
  372. if (gveNote.sourceNote.NoteBeam) {
  373. linesShift += 1; // TODO this is of course rather a workaround, but the beams aren't completed yet here.
  374. // instead, we could calculate how many lines are between the notes of the beam,
  375. // and which stem of which note is longer, so its rest needs that many lines more.
  376. // this is more of "reverse engineering" or rather "advance engineering" the graphical notes,
  377. // which are unfortunately not built/drawn yet here.
  378. }
  379. if (duration.includes("w")) {
  380. linesShift /= 2; // TODO maybe a different fix, whole notes may need another look
  381. }
  382. linesShift += (Math.ceil(rules.RestCollisionYPadding) * 0.5); // 0.5 is smallest unit
  383. linesShift *= lineShiftDirection;
  384. note.lineShift = linesShift;
  385. }
  386. }
  387. }
  388. if (maxHalftone > 0) {
  389. let octaveOffset: number = 3;
  390. const restClefInstruction: ClefInstruction = (note as VexFlowGraphicalNote).Clef();
  391. switch (restClefInstruction.ClefType) {
  392. case ClefEnum.F:
  393. octaveOffset = 5;
  394. break;
  395. case ClefEnum.C:
  396. octaveOffset = 4;
  397. // if (restClefInstruction.Line == 4) // tenor clef quarter rests can be off
  398. break;
  399. default:
  400. break;
  401. }
  402. restYPitch = Pitch.fromHalftone(maxHalftone);
  403. keys = [VexFlowConverter.pitch(restYPitch, true, restClefInstruction, undefined, octaveOffset)[0]];
  404. }
  405. }
  406. // vfClefType seems to be undefined for rest notes, but setting it seems to break rest positioning.
  407. // if (!vfClefType) {
  408. // const clef = (note as VexFlowGraphicalNote).Clef();
  409. // const vexClef: any = VexFlowConverter.Clef(clef);
  410. // vfClefType = vexClef.type;
  411. // }
  412. break;
  413. }
  414. const pitch: [string, string, ClefInstruction] = (note as VexFlowGraphicalNote).vfpitch;
  415. keys.push(pitch[0]);
  416. accidentals.push(pitch[1]);
  417. if (!vfClefType) {
  418. const vfClef: {type: string, annotation: string} = VexFlowConverter.Clef(pitch[2]);
  419. vfClefType = vfClef.type;
  420. }
  421. }
  422. for (let i: number = 0, len: number = numDots; i < len; ++i) {
  423. duration += "d";
  424. }
  425. if (notes.length === 1 && notes[0].sourceNote.Notehead?.Shape === NoteHeadShape.SLASH) {
  426. //if there are multiple note heads, all of them will be slash note head if done like this
  427. // -> see note_type = "s" below
  428. duration += "s"; // we have to specify a slash note head like this in Vexflow
  429. }
  430. if (isRest) {
  431. // "r" has to be put after the "d"s for rest notes.
  432. duration += "r";
  433. }
  434. let vfnote: VF.StaveNote;
  435. const vfnoteStruct: any = {
  436. align_center: alignCenter,
  437. auto_stem: true,
  438. clef: vfClefType,
  439. duration: duration,
  440. keys: keys,
  441. slash: gve.GraceSlash,
  442. };
  443. const firstNote: Note = gve.notes[0].sourceNote;
  444. if (firstNote.IsCueNote) {
  445. vfnoteStruct.glyph_font_scale = VF.DEFAULT_NOTATION_FONT_SCALE * VF.GraceNote.SCALE;
  446. vfnoteStruct.stroke_px = VF.GraceNote.LEDGER_LINE_OFFSET;
  447. }
  448. if (gve.parentVoiceEntry.IsGrace || gve.notes[0].sourceNote.IsCueNote) {
  449. vfnote = new VF.GraceNote(vfnoteStruct);
  450. } else {
  451. vfnote = new VF.StaveNote(vfnoteStruct);
  452. (vfnote as any).stagger_same_whole_notes = rules.StaggerSameWholeNotes;
  453. // it would be nice to only save this once, not for every note, but has to be accessible in stavenote.js
  454. const lyricsEntries: GraphicalLyricEntry[] = gve.parentStaffEntry.LyricsEntries;
  455. let nextOrCloseNoteHasLyrics: boolean = true;
  456. let extraExistingPadding: number = 0;
  457. if (lyricsEntries.length > 0 &&
  458. rules.RenderLyrics &&
  459. rules.LyricsUseXPaddingForLongLyrics
  460. ) { // if these conditions don't apply, we don't need the following calculation
  461. // don't add padding if next note or close note (within quarter distance) has no lyrics
  462. // usually checking the last note is enough, but
  463. // sometimes you get e.g. a 16th with lyrics, one without lyrics, then one with lyrics again,
  464. // easily causing an overlap as well
  465. // the overlap is fixed by measure elongation, but leads to huge measures (see EngravingRule MaximumLyricsElongationFactor)
  466. const startingGMeasure: GraphicalMeasure = gve.parentStaffEntry.parentMeasure;
  467. const startingSEIndex: number = startingGMeasure.staffEntries.indexOf(gve.parentStaffEntry);
  468. // const staffEntries: VoiceEntry[] = gve.parentVoiceEntry.ParentVoice.VoiceEntries;
  469. // unfortunately the voice entries apparently don't include rests, so they would be ignored
  470. const staffEntriesToCheck: GraphicalStaffEntry [] = [];
  471. for (let seIndex: number = startingSEIndex + 1; seIndex < startingGMeasure.staffEntries.length; seIndex++) {
  472. const se: GraphicalStaffEntry = startingGMeasure.staffEntries[seIndex];
  473. if (se.graphicalVoiceEntries[0]) {
  474. staffEntriesToCheck.push(se);
  475. }
  476. }
  477. // // also check next measure:
  478. // // problem: hard to get the next measure object here. (might need to put .nextMeasure into GraphicalMeasure)
  479. // const stafflineMeasures: GraphicalMeasure[] = startingGMeasure.ParentStaffLine.Measures;
  480. // const measureIndexInStaffline: number = stafflineMeasures.indexOf(startingGMeasure);
  481. // if (measureIndexInStaffline + 1 < stafflineMeasures.length) {
  482. // const nextMeasure: GraphicalMeasure = stafflineMeasures[measureIndexInStaffline + 1];
  483. // for (const se of nextMeasure.staffEntries) {
  484. // staffEntriesToCheck.push(se);
  485. // }
  486. // }
  487. let totalDistanceFromFirstNote: Fraction;
  488. let lastTimestamp: Fraction = gve.parentStaffEntry.relInMeasureTimestamp.clone();
  489. for (const currentSE of staffEntriesToCheck) {
  490. const currentTimestamp: Fraction = currentSE.relInMeasureTimestamp.clone();
  491. totalDistanceFromFirstNote = Fraction.minus(currentTimestamp, gve.parentVoiceEntry.Timestamp);
  492. if (totalDistanceFromFirstNote.RealValue > 0.25) { // more than a quarter note distance: don't add padding
  493. nextOrCloseNoteHasLyrics = false;
  494. break;
  495. }
  496. if (currentSE.LyricsEntries.length > 0) {
  497. // nextOrCloseNoteHasLyrics = true;
  498. break;
  499. }
  500. const lastDistanceCovered: Fraction = Fraction.minus(currentTimestamp, lastTimestamp);
  501. extraExistingPadding += lastDistanceCovered.RealValue * 32; // for every 8th note in between (0.125), we need around 4 padding less (*4*8)
  502. lastTimestamp = currentTimestamp;
  503. }
  504. // if the for loop ends without breaking, we are at measure end and assume we need padding
  505. }
  506. if (rules.RenderLyrics &&
  507. rules.LyricsUseXPaddingForLongLyrics &&
  508. lyricsEntries.length > 0 &&
  509. nextOrCloseNoteHasLyrics) {
  510. // VexFlowPatch: add padding to the right for large lyrics,
  511. // so that measure doesn't need to be enlarged too much for spacing
  512. let hasShortNotes: boolean = false;
  513. let padding: number = 0;
  514. for (const note of notes) {
  515. if (note.sourceNote.Length.RealValue <= 0.125) { // 8th or shorter
  516. hasShortNotes = true;
  517. // if (note.sourceNote.Length.RealValue <= 0.0625) { // 16th or shorter
  518. // padding += 0.0; // unnecessary by now. what rather needs more padding is eighth notes now.
  519. // }
  520. break;
  521. }
  522. }
  523. let addPadding: boolean = false;
  524. for (const lyricsEntry of lyricsEntries) {
  525. const widthThreshold: number = rules.LyricsXPaddingWidthThreshold;
  526. // letters like i and l take less space, so we should use the visual width and not number of characters
  527. let currentLyricsWidth: number = lyricsEntry.GraphicalLabel.PositionAndShape.Size.width;
  528. if (lyricsEntry.hasDashFromLyricWord()) {
  529. currentLyricsWidth += 0.5;
  530. }
  531. if (currentLyricsWidth > widthThreshold) {
  532. padding += currentLyricsWidth - widthThreshold;
  533. // if (currentLyricsWidth > 4) {
  534. // padding *= 1.15; // only maybe needed if LyricsXPaddingFactorForLongLyrics < 1
  535. // }
  536. // check if we need padding because next staff entry also has long lyrics or it's the last note in the measure
  537. const currentStaffEntry: GraphicalStaffEntry = gve.parentStaffEntry;
  538. const measureStaffEntries: GraphicalStaffEntry[] = currentStaffEntry.parentMeasure.staffEntries;
  539. const currentStaffEntryIndex: number = measureStaffEntries.indexOf(currentStaffEntry);
  540. const isLastNoteInMeasure: boolean = currentStaffEntryIndex === measureStaffEntries.length - 1;
  541. if (isLastNoteInMeasure) {
  542. extraExistingPadding += rules.LyricsXPaddingReductionForLastNoteInMeasure; // need less padding
  543. }
  544. if (!hasShortNotes) {
  545. extraExistingPadding += rules.LyricsXPaddingReductionForLongNotes; // quarter or longer notes need less padding
  546. }
  547. if (rules.LyricsXPaddingForLastNoteInMeasure || !isLastNoteInMeasure) {
  548. if (currentLyricsWidth > widthThreshold + extraExistingPadding) {
  549. addPadding = true;
  550. padding -= extraExistingPadding; // we don't need to add the e.g. 1.2 we already get from measure end padding
  551. // for last note in the measure, this is usually not necessary,
  552. // but in rare samples with quite long text on the last note it is.
  553. }
  554. }
  555. break; // TODO take the max padding across verses
  556. }
  557. // for situations unlikely to cause overlap we shouldn't add padding,
  558. // e.g. Brooke West sample (OSMD Function Test Chord Symbols) - width ~3.1 in measure 11 on 'ling', no padding needed.
  559. // though Beethoven - Geliebte has only 8ths in measure 2 and is still problematic,
  560. // so unfortunately we can't just check if the next note is 16th or less.
  561. }
  562. if (addPadding) {
  563. (vfnote as any).paddingRight = 10 * rules.LyricsXPaddingFactorForLongLyrics * padding;
  564. }
  565. }
  566. }
  567. const lineShift: number = gve.notes[0].lineShift;
  568. if (lineShift !== 0) {
  569. vfnote.getKeyProps()[0].line += lineShift;
  570. }
  571. // check for slash noteheads (among other noteheads)
  572. if (notes.length > 1) {
  573. // for a single note, we can use duration += "s" (see above).
  574. // If we use the below solution for a single note as well, the notehead sometimes goes over the stem.
  575. for (let n: number = 0; n < notes.length; n++) {
  576. const note: VexFlowGraphicalNote = notes[n] as VexFlowGraphicalNote;
  577. if (note.sourceNote.Notehead?.Shape === NoteHeadShape.SLASH) {
  578. (vfnote as any).note_heads[n].note_type = "s"; // slash notehead
  579. }
  580. }
  581. }
  582. // Annotate GraphicalNote with which line of the staff it appears on
  583. vfnote.getKeyProps().forEach(({ line }, i) => gve.notes[i].staffLine = line);
  584. if (rules.LedgerLineWidth || rules.LedgerLineStrokeStyle) {
  585. // FIXME should probably use vfnote.setLedgerLineStyle. this doesn't seem to do anything.
  586. // however, this is also set in VexFlowVoiceEntry.color() anyways.
  587. if (!((vfnote as any).ledgerLineStyle)) {
  588. (vfnote as any).ledgerLineStyle = {};
  589. }
  590. if (rules.LedgerLineWidth) {
  591. (vfnote as any).ledgerLineStyle.lineWidth = rules.LedgerLineWidth;
  592. }
  593. if (rules.LedgerLineStrokeStyle) {
  594. (vfnote as any).ledgerLineStyle.strokeStyle = rules.LedgerLineStrokeStyle;
  595. }
  596. }
  597. if (rules.ColoringEnabled) {
  598. const defaultColorStem: string = rules.DefaultColorStem;
  599. let stemColor: string = gve.parentVoiceEntry.StemColor;
  600. if (!stemColor && defaultColorStem) {
  601. stemColor = defaultColorStem;
  602. }
  603. const stemStyle: Object = { fillStyle: stemColor, strokeStyle: stemColor };
  604. if (stemColor) {
  605. //gve.parentVoiceEntry.StemColor = stemColor; // this shouldn't be set by DefaultColorStem
  606. vfnote.setStemStyle(stemStyle);
  607. if (vfnote.flag && rules.ColorFlags) {
  608. vfnote.setFlagStyle(stemStyle);
  609. }
  610. }
  611. }
  612. vfnote.x_shift = xShift;
  613. if (gve.parentVoiceEntry.IsGrace && gve.notes[0].sourceNote.NoteBeam) {
  614. // Vexflow seems to have issues with wanted stem direction for beamed grace notes,
  615. // when the stem is connected to a beamed main note (e.g. Haydn Concertante bar 57)
  616. gve.parentVoiceEntry.WantedStemDirection = gve.notes[0].sourceNote.NoteBeam.Notes[0].ParentVoiceEntry.WantedStemDirection;
  617. }
  618. if (gve.parentVoiceEntry) {
  619. const wantedStemDirection: StemDirectionType = gve.parentVoiceEntry.WantedStemDirection;
  620. switch (wantedStemDirection) {
  621. case(StemDirectionType.Up):
  622. vfnote.setStemDirection(VF.Stem.UP);
  623. gve.parentVoiceEntry.StemDirection = StemDirectionType.Up;
  624. break;
  625. case (StemDirectionType.Down):
  626. vfnote.setStemDirection(VF.Stem.DOWN);
  627. gve.parentVoiceEntry.StemDirection = StemDirectionType.Down;
  628. break;
  629. default:
  630. }
  631. }
  632. // add accidentals
  633. for (let i: number = 0, len: number = notes.length; i < len; i += 1) {
  634. (notes[i] as VexFlowGraphicalNote).setIndex(vfnote, i);
  635. if (accidentals[i]) {
  636. if (accidentals[i] === "###") { // triple sharp
  637. vfnote.addAccidental(i, new VF.Accidental("##"));
  638. vfnote.addAccidental(i, new VF.Accidental("#"));
  639. continue;
  640. } else if (accidentals[i] === "bbs") { // triple flat
  641. vfnote.addAccidental(i, new VF.Accidental("bb"));
  642. vfnote.addAccidental(i, new VF.Accidental("b"));
  643. continue;
  644. }
  645. vfnote.addAccidental(i, new VF.Accidental(accidentals[i])); // normal accidental
  646. }
  647. // add Tremolo strokes (only single note tremolos for now, Vexflow doesn't have beams for two-note tremolos yet)
  648. const tremoloStrokes: number = notes[i].sourceNote.TremoloStrokes;
  649. if (tremoloStrokes > 0) {
  650. const tremolo: VF.Tremolo = new VF.Tremolo(tremoloStrokes);
  651. (tremolo as any).extra_stroke_scale = rules.TremoloStrokeScale;
  652. (tremolo as any).y_spacing_scale = rules.TremoloYSpacingScale;
  653. vfnote.addModifier(i, tremolo);
  654. }
  655. }
  656. // half note tremolo: set notehead to half note (Vexflow otherwise takes the notehead from duration) (Hack)
  657. if (firstNote.Length.RealValue === 0.25 && firstNote.Notehead && firstNote.Notehead.Filled === false) {
  658. const keyProps: Object[] = vfnote.getKeyProps();
  659. for (let i: number = 0; i < keyProps.length; i++) {
  660. (<any>keyProps[i]).code = "v81";
  661. }
  662. }
  663. for (let i: number = 0, len: number = numDots; i < len; ++i) {
  664. vfnote.addDotToAll();
  665. }
  666. return vfnote;
  667. }
  668. public static generateArticulations(vfnote: VF.StemmableNote, gNote: GraphicalNote,
  669. rules: EngravingRules): void {
  670. if (!vfnote || vfnote.getAttribute("type") === "GhostNote") {
  671. return;
  672. }
  673. for (const articulation of gNote.sourceNote.ParentVoiceEntry.Articulations) {
  674. let vfArtPosition: number = VF.Modifier.Position.ABOVE;
  675. if (vfnote.getStemDirection() === VF.Stem.UP) {
  676. vfArtPosition = VF.Modifier.Position.BELOW;
  677. }
  678. let vfArt: VF.Articulation = undefined;
  679. const articulationEnum: ArticulationEnum = articulation.articulationEnum;
  680. if (rules.ArticulationPlacementFromXML) {
  681. if (articulation.placement === PlacementEnum.Above) {
  682. vfArtPosition = VF.Modifier.Position.ABOVE;
  683. } else if (articulation.placement === PlacementEnum.Below) {
  684. vfArtPosition = VF.Modifier.Position.BELOW;
  685. } // else if undefined: don't change
  686. }
  687. switch (articulationEnum) {
  688. case ArticulationEnum.accent: {
  689. vfArt = new VF.Articulation("a>");
  690. const slurs: Slur[] = gNote.sourceNote.NoteSlurs;
  691. for (const slur of slurs) {
  692. if (slur.StartNote === gNote.sourceNote) { // && slur.PlacementXml === articulation.placement
  693. if (slur.PlacementXml === PlacementEnum.Above) {
  694. vfArt.setYShift(-rules.SlurStartArticulationYOffsetOfArticulation * 10);
  695. } else if (slur.PlacementXml === PlacementEnum.Below) {
  696. vfArt.setYShift(rules.SlurStartArticulationYOffsetOfArticulation * 10);
  697. }
  698. }
  699. }
  700. break;
  701. }
  702. case ArticulationEnum.breathmark: {
  703. vfArt = new VF.Articulation("abr");
  704. if (articulation.placement === PlacementEnum.Above) {
  705. vfArtPosition = VF.Modifier.Position.ABOVE;
  706. }
  707. (vfArt as any).breathMarkDistance = rules.BreathMarkDistance; // default 0.8 = 80% towards next note or staff end
  708. break;
  709. }
  710. case ArticulationEnum.downbow: {
  711. vfArt = new VF.Articulation("am");
  712. if (articulation.placement === undefined) { // downbow/upbow should be above by default
  713. vfArtPosition = VF.Modifier.Position.ABOVE;
  714. articulation.placement = PlacementEnum.Above;
  715. }
  716. break;
  717. }
  718. case ArticulationEnum.fermata: {
  719. vfArt = new VF.Articulation("a@a");
  720. vfArtPosition = VF.Modifier.Position.ABOVE;
  721. articulation.placement = PlacementEnum.Above;
  722. break;
  723. }
  724. case ArticulationEnum.marcatodown: {
  725. vfArt = new VF.Articulation("a|"); // Vexflow only knows marcato up, so we use a down stroke here.
  726. break;
  727. }
  728. case ArticulationEnum.marcatoup: {
  729. vfArt = new VF.Articulation("a^");
  730. // according to Gould - Behind Bars, Marcato should always be above the staff, regardless of stem direction.
  731. vfArtPosition = VF.Modifier.Position.ABOVE;
  732. // alternative: place close to note (below staff if below 3rd line). looks strange though, see test_marcato_position
  733. // if (rules.PositionMarcatoCloseToNote) {
  734. // const noteLine: number = vfnote.getLineNumber();
  735. // if (noteLine > 3) {
  736. // vfArtPosition = VF.Modifier.Position.ABOVE;
  737. // } else {
  738. // vfArtPosition = VF.Modifier.Position.BELOW;
  739. // }
  740. // //console.log("measure " + gNote.parentVoiceEntry.parentStaffEntry.parentMeasure.MeasureNumber + ", line " + noteLine);
  741. // }
  742. break;
  743. }
  744. case ArticulationEnum.invertedfermata: {
  745. const pve: VoiceEntry = gNote.sourceNote.ParentVoiceEntry;
  746. const sourceNote: Note = gNote.sourceNote;
  747. // find inverted fermata, push it to last voice entry in staffentry list,
  748. // so that it doesn't overlap notes (gets displayed right below higher note)
  749. // TODO this could maybe be moved elsewhere or done more elegantly,
  750. // but on the other hand here it only gets checked if we have an inverted fermata anyways, seems efficient.
  751. if (pve !== sourceNote.ParentVoiceEntry.ParentSourceStaffEntry.VoiceEntries.last()) {
  752. pve.Articulations = pve.Articulations.slice(pve.Articulations.indexOf(articulation));
  753. pve.ParentSourceStaffEntry.VoiceEntries.last().Articulations.push(articulation);
  754. continue;
  755. }
  756. vfArt = new VF.Articulation("a@u");
  757. vfArtPosition = VF.Modifier.Position.BELOW;
  758. articulation.placement = PlacementEnum.Below;
  759. break;
  760. }
  761. case ArticulationEnum.lefthandpizzicato: {
  762. vfArt = new VF.Articulation("a+");
  763. break;
  764. }
  765. case ArticulationEnum.naturalharmonic: {
  766. vfArt = new VF.Articulation("ah");
  767. break;
  768. }
  769. case ArticulationEnum.snappizzicato: {
  770. vfArt = new VF.Articulation("ao");
  771. break;
  772. }
  773. case ArticulationEnum.staccatissimo: {
  774. vfArt = new VF.Articulation("av");
  775. break;
  776. }
  777. case ArticulationEnum.staccato: {
  778. vfArt = new VF.Articulation("a.");
  779. break;
  780. }
  781. case ArticulationEnum.tenuto: {
  782. vfArt = new VF.Articulation("a-");
  783. break;
  784. }
  785. case ArticulationEnum.upbow: {
  786. vfArt = new VF.Articulation("a|");
  787. if (articulation.placement === undefined) { // downbow/upbow should be above by default
  788. vfArtPosition = VF.Modifier.Position.ABOVE;
  789. articulation.placement = PlacementEnum.Above;
  790. }
  791. break;
  792. }
  793. case ArticulationEnum.strongaccent: {
  794. vfArt = new VF.Articulation("a^");
  795. break;
  796. }
  797. default: {
  798. break;
  799. }
  800. }
  801. if (vfArt) {
  802. vfArt.setPosition(vfArtPosition);
  803. (vfnote as StaveNote).addModifier(0, vfArt);
  804. }
  805. }
  806. }
  807. public static generateOrnaments(vfnote: VF.StemmableNote, oContainer: OrnamentContainer): void {
  808. let vfPosition: number = VF.Modifier.Position.ABOVE;
  809. if (oContainer.placement === PlacementEnum.Below) {
  810. vfPosition = VF.Modifier.Position.BELOW;
  811. }
  812. let vfOrna: VF.Ornament = undefined;
  813. switch (oContainer.GetOrnament) {
  814. case OrnamentEnum.DelayedInvertedTurn: {
  815. vfOrna = new VF.Ornament("turn_inverted");
  816. vfOrna.setDelayed(true);
  817. break;
  818. }
  819. case OrnamentEnum.DelayedTurn: {
  820. vfOrna = new VF.Ornament("turn");
  821. vfOrna.setDelayed(true);
  822. break;
  823. }
  824. case OrnamentEnum.InvertedMordent: {
  825. vfOrna = new VF.Ornament("mordent"); // Vexflow uses baroque, not MusicXML definition
  826. vfOrna.setDelayed(false);
  827. break;
  828. }
  829. case OrnamentEnum.InvertedTurn: {
  830. vfOrna = new VF.Ornament("turn_inverted");
  831. vfOrna.setDelayed(false);
  832. break;
  833. }
  834. case OrnamentEnum.Mordent: {
  835. vfOrna = new VF.Ornament("mordent_inverted");
  836. vfOrna.setDelayed(false);
  837. break;
  838. }
  839. case OrnamentEnum.Trill: {
  840. vfOrna = new VF.Ornament("tr");
  841. vfOrna.setDelayed(false);
  842. break;
  843. }
  844. case OrnamentEnum.Turn: {
  845. vfOrna = new VF.Ornament("turn");
  846. vfOrna.setDelayed(false);
  847. break;
  848. }
  849. default: {
  850. log.warn("unhandled OrnamentEnum type: " + oContainer.GetOrnament);
  851. return;
  852. }
  853. }
  854. if (vfOrna) {
  855. if (oContainer.AccidentalBelow !== AccidentalEnum.NONE) {
  856. vfOrna.setLowerAccidental(Pitch.accidentalVexflow(oContainer.AccidentalBelow));
  857. }
  858. if (oContainer.AccidentalAbove !== AccidentalEnum.NONE) {
  859. vfOrna.setUpperAccidental(Pitch.accidentalVexflow(oContainer.AccidentalAbove));
  860. }
  861. vfOrna.setPosition(vfPosition); // Vexflow draws it above right now in any case, never below
  862. (vfnote as StaveNote).addModifier(0, vfOrna);
  863. }
  864. }
  865. public static StrokeTypeFromArpeggioType(arpeggioType: ArpeggioType): VF.Stroke.Type {
  866. switch (arpeggioType) {
  867. case ArpeggioType.ARPEGGIO_DIRECTIONLESS:
  868. return VF.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
  869. case ArpeggioType.BRUSH_DOWN:
  870. return VF.Stroke.Type.BRUSH_UP; // TODO somehow up and down are mixed up in Vexflow right now
  871. case ArpeggioType.BRUSH_UP:
  872. return VF.Stroke.Type.BRUSH_DOWN; // TODO somehow up and down are mixed up in Vexflow right now
  873. case ArpeggioType.RASQUEDO_DOWN:
  874. return VF.Stroke.Type.RASQUEDO_UP;
  875. case ArpeggioType.RASQUEDO_UP:
  876. return VF.Stroke.Type.RASQUEDO_DOWN;
  877. case ArpeggioType.ROLL_DOWN:
  878. return VF.Stroke.Type.ROLL_UP; // TODO somehow up and down are mixed up in Vexflow right now
  879. case ArpeggioType.ROLL_UP:
  880. return VF.Stroke.Type.ROLL_DOWN; // TODO somehow up and down are mixed up in Vexflow right now
  881. default:
  882. return VF.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
  883. }
  884. }
  885. /**
  886. * Convert a set of GraphicalNotes to a VexFlow StaveNote
  887. * @param notes form a chord on the staff
  888. * @returns {VF.StaveNote}
  889. */
  890. public static CreateTabNote(gve: GraphicalVoiceEntry): VF.TabNote {
  891. const tabPositions: {str: number, fret: number}[] = [];
  892. const notes: GraphicalNote[] = gve.notes.reverse();
  893. const tabPhrases: { type: number, text: string, width: number }[] = [];
  894. const frac: Fraction = gve.notes[0].graphicalNoteLength;
  895. const isTuplet: boolean = gve.notes[0].sourceNote.NoteTuplet !== undefined;
  896. let duration: string = VexFlowConverter.durations(frac, isTuplet)[0];
  897. let numDots: number = 0;
  898. for (const note of gve.notes) {
  899. const tabNote: TabNote = note.sourceNote as TabNote;
  900. let tabPosition: {str: number, fret: number} = {str: tabNote.StringNumberTab, fret: tabNote.FretNumber};
  901. if (!(note.sourceNote instanceof TabNote)) {
  902. log.info(`invalid tab note: ${note.sourceNote.Pitch.ToString()} in measure ${gve.parentStaffEntry.parentMeasure.MeasureNumber}` +
  903. ", likely missing XML string+fret number.");
  904. tabPosition = {str: 1, fret: 0}; // random safe values, otherwise it's both undefined for invalid notes
  905. }
  906. tabPositions.push(tabPosition);
  907. if (tabNote.BendArray) {
  908. tabNote.BendArray.forEach( function( bend: {bendalter: number, direction: string} ): void {
  909. let phraseText: string;
  910. const phraseStep: number = bend.bendalter - tabPosition.fret;
  911. if (phraseStep > 1) {
  912. phraseText = "Full";
  913. } else if (phraseStep === 1) {
  914. phraseText = "1/2";
  915. } else {
  916. phraseText = "1/4";
  917. }
  918. if (bend.direction === "up") {
  919. tabPhrases.push({type: VF.Bend.UP, text: phraseText, width: 10});
  920. } else {
  921. tabPhrases.push({type: VF.Bend.DOWN, text: phraseText, width: 10});
  922. }
  923. });
  924. }
  925. if (numDots < note.numberOfDots) {
  926. numDots = note.numberOfDots;
  927. }
  928. }
  929. for (let i: number = 0, len: number = numDots; i < len; ++i) {
  930. duration += "d";
  931. }
  932. const vfnote: VF.TabNote = new VF.TabNote({
  933. duration: duration,
  934. positions: tabPositions,
  935. });
  936. for (let i: number = 0, len: number = notes.length; i < len; i += 1) {
  937. (notes[i] as VexFlowGraphicalNote).setIndex(vfnote, i);
  938. }
  939. tabPhrases.forEach(function(phrase: { type: number, text: string, width: number }): void {
  940. if (phrase.type === VF.Bend.UP) {
  941. vfnote.addModifier (new VF.Bend(phrase.text, false));
  942. } else {
  943. vfnote.addModifier (new VF.Bend(phrase.text, true));
  944. }
  945. });
  946. return vfnote;
  947. }
  948. /**
  949. * Convert a ClefInstruction to a string represention of a clef type in VexFlow.
  950. *
  951. * @param clef The OSMD object to be converted representing the clef
  952. * @param size The VexFlow size to be used. Can be `default` or `small`.
  953. * As soon as #118 is done, this parameter will be dispensable.
  954. * @returns A string representation of a VexFlow clef
  955. * @see https://github.com/0xfe/vexflow/blob/master/src/clef.js
  956. * @see https://github.com/0xfe/vexflow/blob/master/tests/clef_tests.js
  957. */
  958. public static Clef(clef: ClefInstruction, size: string = "default"): { type: string, size: string, annotation: string } {
  959. let type: string;
  960. let annotation: string;
  961. // Make sure size is either "default" or "small"
  962. if (size !== "default" && size !== "small") {
  963. log.warn(`Invalid VexFlow clef size "${size}" specified. Using "default".`);
  964. size = "default";
  965. }
  966. /*
  967. * For all of the following conversions, OSMD uses line numbers 1-5 starting from
  968. * the bottom, while VexFlow uses 0-4 starting from the top.
  969. */
  970. switch (clef.ClefType) {
  971. // G Clef
  972. case ClefEnum.G:
  973. switch (clef.Line) {
  974. case 1:
  975. type = "french"; // VexFlow line 4
  976. break;
  977. case 2:
  978. type = "treble"; // VexFlow line 3
  979. break;
  980. default:
  981. type = "treble";
  982. log.error(`Clef ${ClefEnum[clef.ClefType]} on line ${clef.Line} not supported by VexFlow. Using default value "${type}".`);
  983. }
  984. break;
  985. // F Clef
  986. case ClefEnum.F:
  987. switch (clef.Line) {
  988. case 4:
  989. type = "bass"; // VexFlow line 1
  990. break;
  991. case 3:
  992. type = "baritone-f"; // VexFlow line 2
  993. break;
  994. case 5:
  995. type = "subbass"; // VexFlow line 0
  996. break;
  997. default:
  998. type = "bass";
  999. log.error(`Clef ${ClefEnum[clef.ClefType]} on line ${clef.Line} not supported by VexFlow. Using default value "${type}".`);
  1000. }
  1001. break;
  1002. // C Clef
  1003. case ClefEnum.C:
  1004. switch (clef.Line) {
  1005. case 3:
  1006. type = "alto"; // VexFlow line 2
  1007. break;
  1008. case 4:
  1009. type = "tenor"; // VexFlow line 1
  1010. break;
  1011. case 1:
  1012. type = "soprano"; // VexFlow line 4
  1013. break;
  1014. case 2:
  1015. type = "mezzo-soprano"; // VexFlow line 3
  1016. break;
  1017. default:
  1018. type = "alto";
  1019. log.error(`Clef ${ClefEnum[clef.ClefType]} on line ${clef.Line} not supported by VexFlow. Using default value "${type}".`);
  1020. }
  1021. break;
  1022. // Percussion Clef
  1023. case ClefEnum.percussion:
  1024. type = "percussion";
  1025. break;
  1026. // TAB Clef
  1027. case ClefEnum.TAB:
  1028. // only used currently for creating the notes in the normal stave: There we need a normal treble clef
  1029. type = "treble";
  1030. break;
  1031. default:
  1032. log.info("bad clef type: " + clef.ClefType);
  1033. type = "treble";
  1034. }
  1035. // annotations in vexflow don't allow bass and 8va. No matter the offset :(
  1036. if (clef.OctaveOffset === 1 && type !== "bass" ) {
  1037. annotation = "8va";
  1038. } else if (clef.OctaveOffset === -1) {
  1039. annotation = "8vb";
  1040. }
  1041. return { type, size, annotation };
  1042. }
  1043. /**
  1044. * Convert a RhythmInstruction to a VexFlow TimeSignature object
  1045. * @param rhythm
  1046. * @returns {VF.TimeSignature}
  1047. * @constructor
  1048. */
  1049. public static TimeSignature(rhythm: RhythmInstruction): VF.TimeSignature {
  1050. let timeSpec: string;
  1051. switch (rhythm.SymbolEnum) {
  1052. case RhythmSymbolEnum.NONE:
  1053. timeSpec = rhythm.Rhythm.Numerator + "/" + rhythm.Rhythm.Denominator;
  1054. break;
  1055. case RhythmSymbolEnum.COMMON:
  1056. timeSpec = "C";
  1057. break;
  1058. case RhythmSymbolEnum.CUT:
  1059. timeSpec = "C|";
  1060. break;
  1061. default:
  1062. }
  1063. return new VF.TimeSignature(timeSpec);
  1064. }
  1065. /**
  1066. * Convert a KeyInstruction to a string representing in VexFlow a key
  1067. * @param key
  1068. * @returns {string}
  1069. */
  1070. public static keySignature(key: KeyInstruction): string {
  1071. if (!key) {
  1072. return undefined;
  1073. }
  1074. let ret: string;
  1075. switch (key.Mode) {
  1076. case KeyEnum.minor:
  1077. ret = VexFlowConverter.minorMap[key.Key] + "m";
  1078. break;
  1079. case KeyEnum.major:
  1080. ret = VexFlowConverter.majorMap[key.Key];
  1081. break;
  1082. // some XMLs don't have the mode set despite having a key signature.
  1083. case KeyEnum.none:
  1084. ret = VexFlowConverter.majorMap[key.Key];
  1085. break;
  1086. default:
  1087. ret = "C";
  1088. }
  1089. return ret;
  1090. }
  1091. /**
  1092. * Converts a lineType to a VexFlow StaveConnector type
  1093. * @param lineType
  1094. * @returns {any}
  1095. */
  1096. public static line(lineType: SystemLinesEnum, linePosition: SystemLinePosition): any {
  1097. switch (lineType) {
  1098. case SystemLinesEnum.SingleThin:
  1099. if (linePosition === SystemLinePosition.MeasureBegin) {
  1100. return VF.StaveConnector.type.SINGLE;
  1101. }
  1102. return VF.StaveConnector.type.SINGLE_RIGHT;
  1103. case SystemLinesEnum.DoubleThin:
  1104. return VF.StaveConnector.type.THIN_DOUBLE;
  1105. case SystemLinesEnum.ThinBold:
  1106. return VF.StaveConnector.type.BOLD_DOUBLE_RIGHT;
  1107. case SystemLinesEnum.BoldThinDots:
  1108. return VF.StaveConnector.type.BOLD_DOUBLE_LEFT;
  1109. case SystemLinesEnum.DotsThinBold:
  1110. return VF.StaveConnector.type.BOLD_DOUBLE_RIGHT;
  1111. case SystemLinesEnum.DotsBoldBoldDots:
  1112. return VF.StaveConnector.type.BOLD_DOUBLE_RIGHT;
  1113. case SystemLinesEnum.None:
  1114. return VF.StaveConnector.type.NONE;
  1115. default:
  1116. }
  1117. }
  1118. /**
  1119. * Construct a string which can be used in a CSS font property
  1120. * @param fontSize
  1121. * @param fontStyle
  1122. * @param font
  1123. * @returns {string}
  1124. */
  1125. public static font(fontSize: number, fontStyle: FontStyles = FontStyles.Regular,
  1126. font: Fonts = Fonts.TimesNewRoman, rules: EngravingRules, fontFamily: string = undefined): string {
  1127. let style: string = "normal";
  1128. let weight: string = "normal";
  1129. let family: string = `'${rules.DefaultFontFamily}'`; // default "'Times New Roman'"
  1130. switch (fontStyle) {
  1131. case FontStyles.Bold:
  1132. weight = "bold";
  1133. break;
  1134. case FontStyles.Italic:
  1135. style = "italic";
  1136. break;
  1137. case FontStyles.BoldItalic:
  1138. style = "italic";
  1139. weight = "bold";
  1140. break;
  1141. case FontStyles.Underlined:
  1142. // TODO
  1143. break;
  1144. default:
  1145. break;
  1146. }
  1147. switch (font) { // currently not used
  1148. case Fonts.Kokila:
  1149. // TODO Not Supported
  1150. break;
  1151. default:
  1152. }
  1153. if (fontFamily && fontFamily !== "default") {
  1154. family = `'${fontFamily}'`;
  1155. }
  1156. return style + " " + weight + " " + Math.floor(fontSize) + "px " + family;
  1157. }
  1158. /**
  1159. * Converts the style into a string that VexFlow RenderContext can understand
  1160. * as the weight of the font
  1161. */
  1162. public static fontStyle(style: FontStyles): string {
  1163. switch (style) {
  1164. case FontStyles.Bold:
  1165. return "bold";
  1166. case FontStyles.Italic:
  1167. return "italic";
  1168. case FontStyles.BoldItalic:
  1169. return "italic bold";
  1170. default:
  1171. return "normal";
  1172. }
  1173. }
  1174. /**
  1175. * Convert OutlineAndFillStyle to CSS properties
  1176. * @param styleId
  1177. * @returns {string}
  1178. */
  1179. public static style(styleId: OutlineAndFillStyleEnum): string {
  1180. const ret: string = OUTLINE_AND_FILL_STYLE_DICT.getValue(styleId);
  1181. return ret;
  1182. }
  1183. }