/
VexflowStafflineNoteCalculator.ts
193 lines (184 loc) · 9.22 KB
/
VexflowStafflineNoteCalculator.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
import { IStafflineNoteCalculator } from "../../Interfaces/IStafflineNoteCalculator";
import { GraphicalNote } from "../GraphicalNote";
import { Pitch, NoteEnum } from "../../../Common/DataObjects/Pitch";
import { VexFlowGraphicalNote } from "./VexFlowGraphicalNote";
import { Dictionary } from "typescript-collections";
import { EngravingRules } from "../EngravingRules";
import { ClefEnum } from "../../VoiceData/Instructions/ClefInstruction";
import { StemDirectionType, VoiceEntry } from "../../VoiceData/VoiceEntry";
export class VexflowStafflineNoteCalculator implements IStafflineNoteCalculator {
private rules: EngravingRules;
private staffPitchListMapping: Dictionary<number, Array<Pitch>> = new Dictionary<number, Array<Pitch>>();
//These render on the single line by default
private baseLineNote: NoteEnum = NoteEnum.B;
private baseLineOctave: number = 1;
constructor(rules: EngravingRules) {
this.rules = rules;
}
/**
* This method is called for each note during the calc phase. We want to track all possible positions to make decisions
* during layout about where notes should be positioned.
* This directly puts notes that share a line to the same position, regardless of voice
* @param graphicalNote The note to be checked/positioned
* @param staffIndex The staffline the note is on
*/
public trackNote(graphicalNote: GraphicalNote): void {
if (!(graphicalNote instanceof VexFlowGraphicalNote) || graphicalNote.Clef().ClefType !== ClefEnum.percussion ||
graphicalNote.sourceNote.isRest() || this.rules.PercussionOneLineCutoff === 0 ||
this.rules.PercussionForceVoicesOneLineCutoff === -1) {
return;
}
const staffIndex: number =
graphicalNote.parentVoiceEntry.parentStaffEntry.sourceStaffEntry.ParentStaff.idInMusicSheet;
let currentPitchList: Array<Pitch> = undefined;
if (!this.staffPitchListMapping.containsKey(staffIndex)) {
this.staffPitchListMapping.setValue(staffIndex, new Array<Pitch>());
}
currentPitchList = this.staffPitchListMapping.getValue(staffIndex);
const pitch: Pitch = graphicalNote.sourceNote.Pitch;
VexflowStafflineNoteCalculator.findOrInsert(currentPitchList, pitch);
}
private static PitchIndexOf(array: Array<Pitch>, pitch: Pitch, start: number = 0): number {
if (start > array.length - 1) {
return -1;
}
for (let i: number = start; i < array.length; i++) {
const p2: Pitch = array[i];
if (pitch.OperatorEquals(p2)) {
return i;
}
}
return -1;
}
private static findOrInsert(array: Array<Pitch>, pitch: Pitch): number {
for (let i: number = 0; i < array.length; i++) {
const p2: Pitch = array[i];
if (pitch.OperatorEquals(p2)) {
return i;
} else {
if (pitch.OperatorFundamentalLessThan(p2)) {
array.splice(i, 0, pitch);
return i;
}
}
}
//If we reach here, we've reached the end of the array.
//Means its the greatest pitch
array.push(pitch);
return array.length - 1;
}
/**
* This method is called for each note, and should make any necessary position changes based on the number of stafflines, clef, etc.
* @param graphicalNote The note to be checked/positioned
* @param staffIndex The staffline that this note exists on
* @returns the newly positioned note
*/
public positionNote(graphicalNote: GraphicalNote): GraphicalNote {
const staffIndex: number =
graphicalNote.parentVoiceEntry.parentStaffEntry.sourceStaffEntry.ParentStaff.idInMusicSheet;
if (!(graphicalNote instanceof VexFlowGraphicalNote) || graphicalNote.sourceNote.isRest()
|| !this.staffPitchListMapping.containsKey(staffIndex)) {
return graphicalNote;
}
const currentPitchList: Array<Pitch> = this.staffPitchListMapping.getValue(staffIndex);
//const xmlSingleStaffline: boolean = graphicalNote.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentStaff.StafflineCount === 1;
const positionByXml: boolean = this.rules.PercussionUseXMLDisplayStep &&
graphicalNote.sourceNote.displayStepUnpitched !== undefined;
if (currentPitchList.length > this.rules.PercussionOneLineCutoff && !positionByXml && !this.rules.PercussionUseCajon2NoteSystem) {
//Don't need to position notes. We aren't under the cutoff
return graphicalNote;
}
const vfGraphicalNote: VexFlowGraphicalNote = graphicalNote as VexFlowGraphicalNote;
const notePitch: Pitch = graphicalNote.sourceNote.Pitch;
let displayNote: NoteEnum = this.baseLineNote;
let displayOctave: number = this.baseLineOctave;
if (this.rules.PercussionUseCajon2NoteSystem) {
if (notePitch.FundamentalNote === NoteEnum.C) {
displayNote = NoteEnum.G;
displayOctave = 1;
}
} else if (this.rules.PercussionUseXMLDisplayStep
&& graphicalNote.sourceNote.displayStepUnpitched !== undefined) {
//&& xmlSingleStaffline) {
displayNote = graphicalNote.sourceNote.displayStepUnpitched;
displayOctave = graphicalNote.sourceNote.displayOctaveUnpitched + this.rules.PercussionOneLineXMLDisplayStepOctaveOffset;
}
//If we only need to render on one line
if (currentPitchList.length <= this.rules.PercussionForceVoicesOneLineCutoff) {
vfGraphicalNote.setAccidental(new Pitch(displayNote, displayOctave, notePitch.Accidental));
} else {
const pitchIndex: number = VexflowStafflineNoteCalculator.PitchIndexOf(currentPitchList, notePitch);
if (pitchIndex > -1) {
const half: number = Math.ceil(currentPitchList.length / 2);
if (!this.rules.PercussionUseXMLDisplayStep) {
if (pitchIndex >= half) {
//position above
displayOctave = 2;
switch ((pitchIndex - half) % 5) {
case 1:
displayNote = NoteEnum.E;
break;
case 2:
displayNote = NoteEnum.G;
break;
case 3:
displayNote = NoteEnum.B;
break;
case 4:
displayNote = NoteEnum.D;
displayOctave = 3;
break;
default:
displayNote = NoteEnum.C;
break;
}
} else { //position below
switch (pitchIndex % 5) {
case 1:
displayNote = NoteEnum.F;
break;
case 2:
displayNote = NoteEnum.D;
break;
case 3:
displayNote = NoteEnum.B;
displayOctave = 0;
break;
case 4:
displayNote = NoteEnum.G;
displayOctave = 0;
break;
default:
displayNote = NoteEnum.A;
break;
}
}
}
const mappedPitch: Pitch = new Pitch(displayNote, displayOctave, notePitch.Accidental);
//Map the pitch, set stems properly
vfGraphicalNote.setAccidental(mappedPitch);
const parentVoiceEntry: VoiceEntry = vfGraphicalNote.parentVoiceEntry.parentVoiceEntry;
// Only switch stems if we aren't sharing stems with another note
if (!this.rules.SetWantedStemDirectionByXml && parentVoiceEntry.Notes.length < 2) {
if (mappedPitch.Octave > this.baseLineOctave ||
(mappedPitch.FundamentalNote === this.baseLineNote && mappedPitch.Octave === this.baseLineOctave)) {
vfGraphicalNote.parentVoiceEntry.parentVoiceEntry.WantedStemDirection = StemDirectionType.Up;
} else {
vfGraphicalNote.parentVoiceEntry.parentVoiceEntry.WantedStemDirection = StemDirectionType.Down;
}
}
}
}
return vfGraphicalNote;
}
/**
* Get the number of unique "voices" or note positions
* @param staffIndex The Staffline to get the count of
*/
public getStafflineUniquePositionCount(staffIndex: number): number {
if (this.staffPitchListMapping.containsKey(staffIndex)) {
return this.staffPitchListMapping.getValue(staffIndex).length;
}
return 0;
}
}