-
Notifications
You must be signed in to change notification settings - Fork 3
/
get-tab-position.js
292 lines (267 loc) · 10.4 KB
/
get-tab-position.js
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
import { getKeys } from './get-keys';
import { VEX_ACCIDENTAL_FROM_ABCJS, SEMITONES_FROM_ACCIDENTAL, TUNING_TYPES } from '../constants';
import { getDiatonicFromLetter, getChromaticFromLetter } from './utils';
/**
* Converts an array of VexFlow keys into array of guitar note positions.
* TODO: handle tab for multiple stacked notes instead of just keys[0]... this isn't really that hard,
* just step through keys[] using the same code, only have to ensure the tab positions aren't on the same string.
*
* @param {Array} keys the notes in VexFlow format such as ['e/3'];
* @param {Object} abcKeySignature key signature in abcjs format
* @param {Array} barContents the previous abcjs note objects in this bar
* @param {number} index the position of the current note in barContents
* @param {Array} tuning object with: array of tuning from lowest string to highest in VexFlow format
* such as ['e/3', 'a/3', 'd/4', 'g/4', 'b/4', 'e/5'], and optionally a showFingerings flag which instructs
* the function to output violin fingerings like finger "1" instead of tab position 2, low finger "v2"
* instead of tab position 3, etc.
* @param {bool} isGraceNote did the keys represent the regular note or grace notes attached
* to note at this barContents index?
*
* @returns {Array} the tab positions such as [{str: 6, fret: 0}]
*/
export function getTabPosition(keys, abcKeySignature, barContents, index, tuning, isGraceNote) {
if (isGraceNote === undefined) {
throw new Error('getTabPosition: isGraceNote argument must be supplied');
}
const diatonic = getDiatonicFromLetter(keys[0]);
const absoluteChromatic = getAbsoluteChromatic(keys[0]);
// this will hold the chromatic pitch after taking into account previous accidentals in the bar
let adjustedAbsoluteChromatic = absoluteChromatic;
// apply accidentals in key signature to the note
abcKeySignature.accidentals.forEach((accidental) => {
if (diatonic === getDiatonicFromLetter(accidental.note.toLowerCase())) {
switch (accidental.acc) {
case 'sharp':
adjustedAbsoluteChromatic = absoluteChromatic + 1;
break;
case 'flat':
adjustedAbsoluteChromatic = absoluteChromatic - 1;
break;
default:
break;
}
}
});
/* apply accidentals from previous notes in the bar to the current note if appropriate (will
overwrite, not add to the changes made above with key signature). when accidental occurs
at the SAME barContents index as the note whose position is being calculated, it needs
to be like this:
accidental on a regular note at index n:
- position being calculated for a regular note at n: apply accidental
- position being calculated for a grace note at n: do not apply
accidental on a grace note at index n:
- position being calculated for a regular note at n: apply
- position being calculated for a grace note at n: apply
or in other words, the precedence should be:
- this is a regular note n:
- apply acc from regular n
- apply acc from grace n
- apply acc from regular n-1
- apply acc from grace n-1
- this is a grace note n:
- apply acc from grace note n
- apply acc from regular n-1
- apply acc from grace n-1
This is why the previousAppliesToCurrent checks for isGraceNote on the second if block
This is also why the if block that checks for accidentals on grace notes must come
before the if block that checks for accidentals on regular notes, so regular n-1
can have the opportunity to overwrite grace n-1, or in the case of note n and it's not
a grace note, so regular n can overwrite grace n
Just modified the code to actually step through the arrays of pitches and grace notes
and consider the accidentals of each note in array, instead of just [0]. Haven't considered
what tests are necessary for this. Understand it's not doing the same thing in each case.
With pitches[] it's an array of pitches arranged vertically at one single index in
barContents. with gracenotes[] it's an array of gracenotes arranged horizontally
preceding the regular notes at that index
*/
barContents.forEach((previousObj, j) => {
if (previousObj.gracenotes && index >= j) {
const previousKeys = getKeys(previousObj.gracenotes);
previousObj.gracenotes.forEach((graceNote, k) => {
const accidental = VEX_ACCIDENTAL_FROM_ABCJS[graceNote.accidental];
if (accidental && diatonic === getDiatonicFromLetter(previousKeys[k])) {
adjustedAbsoluteChromatic = absoluteChromatic + SEMITONES_FROM_ACCIDENTAL[accidental];
}
});
}
const previousAppliesToCurrent = (index >= j && !isGraceNote) || (index > j);
if (previousAppliesToCurrent && previousObj.el_type === 'note' && !previousObj.rest) {
const previousKeys = getKeys(previousObj.pitches);
previousObj.pitches.forEach((pitch, k) => {
const accidental = VEX_ACCIDENTAL_FROM_ABCJS[pitch.accidental];
if (accidental && diatonic === getDiatonicFromLetter(previousKeys[k])) {
adjustedAbsoluteChromatic = absoluteChromatic + SEMITONES_FROM_ACCIDENTAL[accidental];
}
});
}
});
if (tuning.type === TUNING_TYPES.WHISTLE) {
return getTinWhistlePosition(adjustedAbsoluteChromatic, tuning.pitchOffset);
} if (tuning.type === TUNING_TYPES.HARMONICA) {
return getHarmonicaPosition(adjustedAbsoluteChromatic, tuning.pitchOffset);
}
const stringChromaticPitches = tuning.strings.map(string => getAbsoluteChromatic(string));
let left;
let top;
if (adjustedAbsoluteChromatic < stringChromaticPitches[0]) {
return [{ str: 1, fret: '?' }];
}
for (let i = 0; i < stringChromaticPitches.length; i += 1) {
if (!(stringChromaticPitches[i + 1] && adjustedAbsoluteChromatic >= stringChromaticPitches[i + 1])) {
top = stringChromaticPitches.length - i;
left = adjustedAbsoluteChromatic - stringChromaticPitches[i];
break;
}
}
if (tuning.type === TUNING_TYPES.STRINGS_FIDDLE_FINGERINGS) {
left = convertToFingering(left);
}
return [{ str: top, fret: left }];
}
/**
* Converts a VexFlow key into a chromatic note number starting from 0 at (C0 or C1 I think)
* @param {string} key the note in VexFlow format such as 'e/3';
*
* @returns {Array} the note number such as 28 ??? or is that E2??
*/
function getAbsoluteChromatic(key) {
const chromaticNote = getChromaticFromLetter(key);
const octave = key.charAt(2);
let noteNumber = octave * 12 + chromaticNote;
if (key.charAt(3) === '#') {
noteNumber += 1;
} else if (key.charAt(3) === 'b') {
noteNumber -= 1;
}
return noteNumber;
}
/**
* Converts a fret position (for a given string) to a violin fingering. Works for first position,
* any notes beyond first position finger 4 will be displayed as '?'
* @param {number} fret the fret such as 2
*
* @returns {string} the violin fingering such as '1'. VexFlow accepts a string or a number
* parameter, so it's not a problem that I'm taking a number and returning string
*/
function convertToFingering(fret) {
switch (fret) {
case 0:
return '0';
case 1:
return 'v1';
case 2:
return '1';
case 3:
return 'v2';
case 4:
return '2';
case 5:
return '3';
case 6:
return 'v4';
case 7:
return '4';
default:
return '?';
}
}
/**
* Converts a chromatic pitch value (should be same as MIDI note number, c3 == 48) to a diatonic harmonica position.
* Supports harmonica in c, a, and g though would be trivial to support every pitch of harmonica.
* @param {number} adjustedAbsoluteChromatic the pitch such as 48 for c3
* @param {string} pitchOffset The pitch offset from 'c' harmonica positions
*
* @returns {Array} the tab positions for VexFlow. VexFlow thinks this is guitar tab, so we still specify string and
* fret, but we can enter any string for fret, so we just write the positions in string form on the first line of tablature
*/
function getHarmonicaPosition(adjustedAbsoluteChromatic, pitchOffset) {
// 'd' = draw bend, 'b' = blow bend, 'ob' = overblow, 'od' = overdraw
const harmonicaPositions = [
'^1', // c
'v1d',
'v1', // d
'^1ob',
'^2', // e
'v2dd', // f
'v2d',
'v2', // g
'v3ddd',
'v3dd', // a
'v3d',
'v3', // b
'^4', // c
'v4d',
'v4', // d
'^4ob',
'^5', // e
'v5', // f
'^5ob',
'^6', // g
'v6d',
'v6', // a
'^6ob',
'v7', // b
'^7', // c
'v7od',
'v8', // d
'^8b',
'^8', // e
'v9', // f
'^9b',
'^9', // g
'v9od',
'v10', // a
'^10bb',
'^10bb', // b
'^10', // c
'v10od'
];
const startPitch = 48 + pitchOffset;
if (adjustedAbsoluteChromatic - startPitch >= 0 && adjustedAbsoluteChromatic - startPitch < harmonicaPositions.length) {
return [{ str: 1, fret: harmonicaPositions[adjustedAbsoluteChromatic - startPitch] }];
}
return [{ str: 1, fret: '?' }];
}
/**
* Converts a chromatic pitch value (should be same as MIDI note number, c3 == 48) to a tin whistle position.
* Supports whistle in d, c, and b flat though would be trivial to support every pitch of whistle.
* @param {number} adjustedAbsoluteChromatic the pitch such as 48 for c3
* @param {string} key The pitch offset from 'd' whistle
*
* @returns {Array} the tab positions for VexFlow. VexFlow thinks this is guitar tab, so we still specify string and
* fret, but we can enter any string for fret, so we just write the positions in string form on the first line of tablature
*/
function getTinWhistlePosition(adjustedAbsoluteChromatic, pitchOffset) {
const whistlePositions = [
'6', // D, 1
'5½',
'5', // E, M2
'4½', // F
'4', // M3
'3', // G, P4
'2½',
'2', // A, P5
'1½',
'1', // B, M6
'½', // C
'0', // M7
'6+', // D, P8
'5½+',
'5+', // E, M2
'4½+', // F
'4+', // M3
'3+', // G, P4
'2½+',
'2+', // A, P5
'1½+',
'1+', // B, M6
'½+', // C
'0+' // M7
];
// pitch offset for C is -2...
const startPitch = 50 + pitchOffset;
if (adjustedAbsoluteChromatic - startPitch >= 0 && adjustedAbsoluteChromatic - startPitch < whistlePositions.length) {
return [{ str: 1, fret: whistlePositions[adjustedAbsoluteChromatic - startPitch] }];
}
return [{ str: 1, fret: '?' }];
}