/
VexFlowConverter.ts
1255 lines (1192 loc) · 61.2 KB
/
VexFlowConverter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import Vex from "vexflow";
import VF = Vex.Flow;
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 = VF.StaveNote;
import { ArpeggioType } from "../../VoiceData/Arpeggio";
import { TabNote } from "../../VoiceData/TabNote";
import { PlacementEnum } from "../../VoiceData/Expressions/AbstractExpression";
import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
import { Slur } from "../../VoiceData/Expressions/ContinuousExpressions/Slur";
import { GraphicalLyricEntry } from "../GraphicalLyricEntry";
import { GraphicalMeasure } from "../GraphicalMeasure";
/**
* 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.0001) { // essentially > 0, but using a small delta to prevent infinite loop
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.TRIANGLE_INVERTED:
return codeStart + "TI";
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): VF.GhostNote[] {
const ghostNotes: VF.GhostNote[] = [];
const durations: string[] = VexFlowConverter.durations(frac, false);
for (const duration of durations) {
ghostNotes.push(new VF.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 {VF.StaveNote}
*/
public static StaveNote(gve: GraphicalVoiceEntry): VF.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 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 = note.sourceNote.IsWholeMeasureRest ||
baseNoteLength.RealValue === note.sourceNote.SourceMeasure.ActiveTimeSignature.RealValue;
if (isWholeMeasureRest) {
keys = ["d/5"];
if (gve.parentStaffEntry.parentMeasure.ParentStaff.StafflineCount === 1) {
keys = ["b/4"];
}
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;
}
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 (notes.length === 1 && notes[0].sourceNote.Notehead?.Shape === NoteHeadShape.SLASH) {
//if there are multiple note heads, all of them will be slash note head if done like this
// -> see note_type = "s" below
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: VF.StaveNote;
const vfnoteStruct: any = {
align_center: alignCenter,
auto_stem: true,
clef: vfClefType,
duration: duration,
keys: keys,
slash: gve.GraceSlash,
};
const firstNote: Note = gve.notes[0].sourceNote;
if (firstNote.IsCueNote) {
vfnoteStruct.glyph_font_scale = VF.DEFAULT_NOTATION_FONT_SCALE * VF.GraceNote.SCALE;
vfnoteStruct.stroke_px = VF.GraceNote.LEDGER_LINE_OFFSET;
}
if (gve.parentVoiceEntry.IsGrace || gve.notes[0].sourceNote.IsCueNote) {
vfnote = new VF.GraceNote(vfnoteStruct);
} else {
vfnote = new VF.StaveNote(vfnoteStruct);
(vfnote as any).stagger_same_whole_notes = rules.StaggerSameWholeNotes;
// it would be nice to only save this once, not for every note, but has to be accessible in stavenote.js
const lyricsEntries: GraphicalLyricEntry[] = gve.parentStaffEntry.LyricsEntries;
let nextOrCloseNoteHasLyrics: boolean = true;
let extraExistingPadding: number = 0;
if (lyricsEntries.length > 0 &&
rules.RenderLyrics &&
rules.LyricsUseXPaddingForLongLyrics
) { // if these conditions don't apply, we don't need the following calculation
// don't add padding if next note or close note (within quarter distance) has no lyrics
// usually checking the last note is enough, but
// sometimes you get e.g. a 16th with lyrics, one without lyrics, then one with lyrics again,
// easily causing an overlap as well
// the overlap is fixed by measure elongation, but leads to huge measures (see EngravingRule MaximumLyricsElongationFactor)
const startingGMeasure: GraphicalMeasure = gve.parentStaffEntry.parentMeasure;
const startingSEIndex: number = startingGMeasure.staffEntries.indexOf(gve.parentStaffEntry);
// const staffEntries: VoiceEntry[] = gve.parentVoiceEntry.ParentVoice.VoiceEntries;
// unfortunately the voice entries apparently don't include rests, so they would be ignored
const staffEntriesToCheck: GraphicalStaffEntry [] = [];
for (let seIndex: number = startingSEIndex + 1; seIndex < startingGMeasure.staffEntries.length; seIndex++) {
const se: GraphicalStaffEntry = startingGMeasure.staffEntries[seIndex];
if (se.graphicalVoiceEntries[0]) {
staffEntriesToCheck.push(se);
}
}
// // also check next measure:
// // problem: hard to get the next measure object here. (might need to put .nextMeasure into GraphicalMeasure)
// const stafflineMeasures: GraphicalMeasure[] = startingGMeasure.ParentStaffLine.Measures;
// const measureIndexInStaffline: number = stafflineMeasures.indexOf(startingGMeasure);
// if (measureIndexInStaffline + 1 < stafflineMeasures.length) {
// const nextMeasure: GraphicalMeasure = stafflineMeasures[measureIndexInStaffline + 1];
// for (const se of nextMeasure.staffEntries) {
// staffEntriesToCheck.push(se);
// }
// }
let totalDistanceFromFirstNote: Fraction;
let lastTimestamp: Fraction = gve.parentStaffEntry.relInMeasureTimestamp.clone();
for (const currentSE of staffEntriesToCheck) {
const currentTimestamp: Fraction = currentSE.relInMeasureTimestamp.clone();
totalDistanceFromFirstNote = Fraction.minus(currentTimestamp, gve.parentVoiceEntry.Timestamp);
if (totalDistanceFromFirstNote.RealValue > 0.25) { // more than a quarter note distance: don't add padding
nextOrCloseNoteHasLyrics = false;
break;
}
if (currentSE.LyricsEntries.length > 0) {
// nextOrCloseNoteHasLyrics = true;
break;
}
const lastDistanceCovered: Fraction = Fraction.minus(currentTimestamp, lastTimestamp);
extraExistingPadding += lastDistanceCovered.RealValue * 32; // for every 8th note in between (0.125), we need around 4 padding less (*4*8)
lastTimestamp = currentTimestamp;
}
// if the for loop ends without breaking, we are at measure end and assume we need padding
}
if (rules.RenderLyrics &&
rules.LyricsUseXPaddingForLongLyrics &&
lyricsEntries.length > 0 &&
nextOrCloseNoteHasLyrics) {
// VexFlowPatch: add padding to the right for large lyrics,
// so that measure doesn't need to be enlarged too much for spacing
let hasShortNotes: boolean = false;
let padding: number = 0;
for (const note of notes) {
if (note.sourceNote.Length.RealValue <= 0.125) { // 8th or shorter
hasShortNotes = true;
// if (note.sourceNote.Length.RealValue <= 0.0625) { // 16th or shorter
// padding += 0.0; // unnecessary by now. what rather needs more padding is eighth notes now.
// }
break;
}
}
let addPadding: boolean = false;
for (const lyricsEntry of lyricsEntries) {
const widthThreshold: number = rules.LyricsXPaddingWidthThreshold;
// letters like i and l take less space, so we should use the visual width and not number of characters
let currentLyricsWidth: number = lyricsEntry.GraphicalLabel.PositionAndShape.Size.width;
if (lyricsEntry.hasDashFromLyricWord()) {
currentLyricsWidth += 0.5;
}
if (currentLyricsWidth > widthThreshold) {
padding += currentLyricsWidth - widthThreshold;
// if (currentLyricsWidth > 4) {
// padding *= 1.15; // only maybe needed if LyricsXPaddingFactorForLongLyrics < 1
// }
// check if we need padding because next staff entry also has long lyrics or it's the last note in the measure
const currentStaffEntry: GraphicalStaffEntry = gve.parentStaffEntry;
const measureStaffEntries: GraphicalStaffEntry[] = currentStaffEntry.parentMeasure.staffEntries;
const currentStaffEntryIndex: number = measureStaffEntries.indexOf(currentStaffEntry);
const isLastNoteInMeasure: boolean = currentStaffEntryIndex === measureStaffEntries.length - 1;
if (isLastNoteInMeasure) {
extraExistingPadding += rules.LyricsXPaddingReductionForLastNoteInMeasure; // need less padding
}
if (!hasShortNotes) {
extraExistingPadding += rules.LyricsXPaddingReductionForLongNotes; // quarter or longer notes need less padding
}
if (rules.LyricsXPaddingForLastNoteInMeasure || !isLastNoteInMeasure) {
if (currentLyricsWidth > widthThreshold + extraExistingPadding) {
addPadding = true;
padding -= extraExistingPadding; // we don't need to add the e.g. 1.2 we already get from measure end padding
// for last note in the measure, this is usually not necessary,
// but in rare samples with quite long text on the last note it is.
}
}
break; // TODO take the max padding across verses
}
// for situations unlikely to cause overlap we shouldn't add padding,
// e.g. Brooke West sample (OSMD Function Test Chord Symbols) - width ~3.1 in measure 11 on 'ling', no padding needed.
// though Beethoven - Geliebte has only 8ths in measure 2 and is still problematic,
// so unfortunately we can't just check if the next note is 16th or less.
}
if (addPadding) {
(vfnote as any).paddingRight = 10 * rules.LyricsXPaddingFactorForLongLyrics * padding;
}
}
}
const lineShift: number = gve.notes[0].lineShift;
if (lineShift !== 0) {
vfnote.getKeyProps()[0].line += lineShift;
}
// check for slash noteheads (among other noteheads)
if (notes.length > 1) {
// for a single note, we can use duration += "s" (see above).
// If we use the below solution for a single note as well, the notehead sometimes goes over the stem.
for (let n: number = 0; n < notes.length; n++) {
const note: VexFlowGraphicalNote = notes[n] as VexFlowGraphicalNote;
if (note.sourceNote.Notehead?.Shape === NoteHeadShape.SLASH) {
(vfnote as any).note_heads[n].note_type = "s"; // slash notehead
}
}
}
// 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; // this shouldn't be set by DefaultColorStem
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(VF.Stem.UP);
gve.parentVoiceEntry.StemDirection = StemDirectionType.Up;
break;
case (StemDirectionType.Down):
vfnote.setStemDirection(VF.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 VF.Accidental("##"));
vfnote.addAccidental(i, new VF.Accidental("#"));
continue;
} else if (accidentals[i] === "bbs") { // triple flat
vfnote.addAccidental(i, new VF.Accidental("bb"));
vfnote.addAccidental(i, new VF.Accidental("b"));
continue;
}
vfnote.addAccidental(i, new VF.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: VF.Tremolo = new VF.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: VF.StemmableNote, gNote: GraphicalNote,
rules: EngravingRules): void {
if (!vfnote || vfnote.getAttribute("type") === "GhostNote") {
return;
}
for (const articulation of gNote.sourceNote.ParentVoiceEntry.Articulations) {
let vfArtPosition: number = VF.Modifier.Position.ABOVE;
if (vfnote.getStemDirection() === VF.Stem.UP) {
vfArtPosition = VF.Modifier.Position.BELOW;
}
let vfArt: VF.Articulation = undefined;
const articulationEnum: ArticulationEnum = articulation.articulationEnum;
if (rules.ArticulationPlacementFromXML) {
if (articulation.placement === PlacementEnum.Above) {
vfArtPosition = VF.Modifier.Position.ABOVE;
} else if (articulation.placement === PlacementEnum.Below) {
vfArtPosition = VF.Modifier.Position.BELOW;
} // else if undefined: don't change
}
switch (articulationEnum) {
case ArticulationEnum.accent: {
vfArt = new VF.Articulation("a>");
const slurs: Slur[] = gNote.sourceNote.NoteSlurs;
for (const slur of slurs) {
if (slur.StartNote === gNote.sourceNote) { // && slur.PlacementXml === articulation.placement
if (slur.PlacementXml === PlacementEnum.Above) {
vfArt.setYShift(-rules.SlurStartArticulationYOffsetOfArticulation * 10);
} else if (slur.PlacementXml === PlacementEnum.Below) {
vfArt.setYShift(rules.SlurStartArticulationYOffsetOfArticulation * 10);
}
}
}
break;
}
case ArticulationEnum.breathmark: {
vfArt = new VF.Articulation("abr");
if (articulation.placement === PlacementEnum.Above) {
vfArtPosition = VF.Modifier.Position.ABOVE;
}
(vfArt as any).breathMarkDistance = rules.BreathMarkDistance; // default 0.8 = 80% towards next note or staff end
break;
}
case ArticulationEnum.downbow: {
vfArt = new VF.Articulation("am");
if (articulation.placement === undefined) { // downbow/upbow should be above by default
vfArtPosition = VF.Modifier.Position.ABOVE;
articulation.placement = PlacementEnum.Above;
}
break;
}
case ArticulationEnum.fermata: {
vfArt = new VF.Articulation("a@a");
vfArtPosition = VF.Modifier.Position.ABOVE;
articulation.placement = PlacementEnum.Above;
break;
}
case ArticulationEnum.marcatodown: {
vfArt = new VF.Articulation("a|"); // Vexflow only knows marcato up, so we use a down stroke here.
break;
}
case ArticulationEnum.marcatoup: {
vfArt = new VF.Articulation("a^");
// according to Gould - Behind Bars, Marcato should always be above the staff, regardless of stem direction.
vfArtPosition = VF.Modifier.Position.ABOVE;
// alternative: place close to note (below staff if below 3rd line). looks strange though, see test_marcato_position
// if (rules.PositionMarcatoCloseToNote) {
// const noteLine: number = vfnote.getLineNumber();
// if (noteLine > 3) {
// vfArtPosition = VF.Modifier.Position.ABOVE;
// } else {
// vfArtPosition = VF.Modifier.Position.BELOW;
// }
// //console.log("measure " + gNote.parentVoiceEntry.parentStaffEntry.parentMeasure.MeasureNumber + ", line " + noteLine);
// }
break;
}
case ArticulationEnum.invertedfermata: {
const pve: VoiceEntry = gNote.sourceNote.ParentVoiceEntry;
const sourceNote: Note = gNote.sourceNote;
// find inverted fermata, push it to last voice entry in staffentry list,
// so that it doesn't overlap notes (gets displayed right below higher note)
// TODO this could maybe be moved elsewhere or done more elegantly,
// but on the other hand here it only gets checked if we have an inverted fermata anyways, seems efficient.
if (pve !== sourceNote.ParentVoiceEntry.ParentSourceStaffEntry.VoiceEntries.last()) {
pve.Articulations = pve.Articulations.slice(pve.Articulations.indexOf(articulation));
pve.ParentSourceStaffEntry.VoiceEntries.last().Articulations.push(articulation);
continue;
}
vfArt = new VF.Articulation("a@u");
vfArtPosition = VF.Modifier.Position.BELOW;
articulation.placement = PlacementEnum.Below;
break;
}
case ArticulationEnum.lefthandpizzicato: {
vfArt = new VF.Articulation("a+");
break;
}
case ArticulationEnum.naturalharmonic: {
vfArt = new VF.Articulation("ah");
break;
}
case ArticulationEnum.snappizzicato: {
vfArt = new VF.Articulation("ao");
break;
}
case ArticulationEnum.staccatissimo: {
vfArt = new VF.Articulation("av");
break;
}
case ArticulationEnum.staccato: {
vfArt = new VF.Articulation("a.");
break;
}
case ArticulationEnum.tenuto: {
vfArt = new VF.Articulation("a-");
break;
}
case ArticulationEnum.upbow: {
vfArt = new VF.Articulation("a|");
if (articulation.placement === undefined) { // downbow/upbow should be above by default
vfArtPosition = VF.Modifier.Position.ABOVE;
articulation.placement = PlacementEnum.Above;
}
break;
}
case ArticulationEnum.strongaccent: {
vfArt = new VF.Articulation("a^");
break;
}
default: {
break;
}
}
if (vfArt) {
vfArt.setPosition(vfArtPosition);
(vfnote as StaveNote).addModifier(0, vfArt);
}
}
}
public static generateOrnaments(vfnote: VF.StemmableNote, oContainer: OrnamentContainer): void {
let vfPosition: number = VF.Modifier.Position.ABOVE;
if (oContainer.placement === PlacementEnum.Below) {
vfPosition = VF.Modifier.Position.BELOW;
}
let vfOrna: VF.Ornament = undefined;
switch (oContainer.GetOrnament) {
case OrnamentEnum.DelayedInvertedTurn: {
vfOrna = new VF.Ornament("turn_inverted");
vfOrna.setDelayed(true);
break;
}
case OrnamentEnum.DelayedTurn: {
vfOrna = new VF.Ornament("turn");
vfOrna.setDelayed(true);
break;
}
case OrnamentEnum.InvertedMordent: {
vfOrna = new VF.Ornament("mordent"); // Vexflow uses baroque, not MusicXML definition
vfOrna.setDelayed(false);
break;
}
case OrnamentEnum.InvertedTurn: {
vfOrna = new VF.Ornament("turn_inverted");
vfOrna.setDelayed(false);
break;
}
case OrnamentEnum.Mordent: {
vfOrna = new VF.Ornament("mordent_inverted");
vfOrna.setDelayed(false);
break;
}
case OrnamentEnum.Trill: {
vfOrna = new VF.Ornament("tr");
vfOrna.setDelayed(false);
break;
}
case OrnamentEnum.Turn: {
vfOrna = new VF.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): VF.Stroke.Type {
switch (arpeggioType) {
case ArpeggioType.ARPEGGIO_DIRECTIONLESS:
return VF.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
case ArpeggioType.BRUSH_DOWN:
return VF.Stroke.Type.BRUSH_UP; // TODO somehow up and down are mixed up in Vexflow right now
case ArpeggioType.BRUSH_UP:
return VF.Stroke.Type.BRUSH_DOWN; // TODO somehow up and down are mixed up in Vexflow right now
case ArpeggioType.RASQUEDO_DOWN:
return VF.Stroke.Type.RASQUEDO_UP;
case ArpeggioType.RASQUEDO_UP:
return VF.Stroke.Type.RASQUEDO_DOWN;
case ArpeggioType.ROLL_DOWN:
return VF.Stroke.Type.ROLL_UP; // TODO somehow up and down are mixed up in Vexflow right now
case ArpeggioType.ROLL_UP:
return VF.Stroke.Type.ROLL_DOWN; // TODO somehow up and down are mixed up in Vexflow right now
default:
return VF.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
}
}
/**
* Convert a set of GraphicalNotes to a VexFlow StaveNote
* @param notes form a chord on the staff
* @returns {VF.StaveNote}
*/
public static CreateTabNote(gve: GraphicalVoiceEntry): VF.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;
let tabVibrato: boolean = false;
for (const note of gve.notes) {
const tabNote: TabNote = note.sourceNote as TabNote;
let tabPosition: {str: number, fret: number} = {str: tabNote.StringNumberTab, fret: tabNote.FretNumber};
if (!(note.sourceNote instanceof TabNote)) {
log.info(`invalid tab note: ${note.sourceNote.Pitch.ToString()} in measure ${gve.parentStaffEntry.parentMeasure.MeasureNumber}` +
", likely missing XML string+fret number.");
tabPosition = {str: 1, fret: 0}; // random safe values, otherwise it's both undefined for invalid notes
}
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: VF.Bend.UP, text: phraseText, width: 10});
} else {
tabPhrases.push({type: VF.Bend.DOWN, text: phraseText, width: 10});
}
});
}
if (tabNote.VibratoStroke) {
tabVibrato = true;
}
if (numDots < note.numberOfDots) {
numDots = note.numberOfDots;
}
}
for (let i: number = 0, len: number = numDots; i < len; ++i) {
duration += "d";
}
const vfnote: VF.TabNote = new VF.TabNote({
duration: duration,
positions: tabPositions,
});
(vfnote as any).BackgroundColor = gve.parentStaffEntry.parentMeasure.parentSourceMeasure.Rules.PageBackgroundColor; // may be undefined
// this fixes background color for rects around tab numbers if PageBackgroundColor set or transparent color unsupported.
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 === VF.Bend.UP) {
vfnote.addModifier (new VF.Bend(phrase.text, false));
} else {
vfnote.addModifier (new VF.Bend(phrase.text, true));
}
});
if (tabVibrato) {
vfnote.addModifier(new VF.Vibrato());
}
return vfnote;
}
/**