-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathchordFingerings.enharmonics.test.js
More file actions
250 lines (225 loc) · 7.97 KB
/
Copy pathchordFingerings.enharmonics.test.js
File metadata and controls
250 lines (225 loc) · 7.97 KB
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
import { describe, it, expect } from "vitest";
import { Mode, Note, Scale } from "tonal";
import {
CHORDS_DB_TUNING,
FRET_WINDOW,
getChordVariations,
} from "./chordFingerings.js";
// Every scale the UI offers in ScaleSelector, and every key in KeySelector.
// Mode.triads over this matrix is exactly what ScaleChords renders, so any
// combination that fails to resolve is a blank page in production.
const KEYS = [
"C",
"C#",
"D",
"Eb",
"E",
"F",
"F#",
"G",
"Ab",
"A",
"Bb",
"B",
];
const SCALES = [
"major",
"minor",
"dorian",
"phrygian",
"lydian",
"mixolydian",
"locrian",
"harmonic minor",
"melodic minor",
];
const TUNINGS = {
Standard: ["E2", "A2", "D3", "G3", "B3", "E4"],
"Half Step Down": ["Eb2", "Ab2", "Db3", "Gb3", "Bb3", "Eb4"],
"Drop D": ["D2", "A2", "D3", "G3", "B3", "E4"],
"Drop C": ["C2", "G2", "C3", "F3", "A3", "D4"],
};
/** Every triad the app can ask for, as [key, scale, chordName] tuples. */
function allTriads() {
const out = [];
for (const key of KEYS) {
for (const scaleName of SCALES) {
const scale = Scale.get(`${key} ${scaleName}`);
let triads = [];
try {
triads = Mode.triads(scale.type, scale.tonic);
} catch {
continue;
}
for (const chord of triads) out.push([key, scaleName, chord]);
}
}
return out;
}
/** Absolute fret of each string in an svguitar position, or null if muted. */
function absoluteFrets(position) {
// svguitar numbers strings 1..6 high-to-low; return them low-to-high.
const byString = new Map(position.fingers.map(([str, fret]) => [str, fret]));
return [6, 5, 4, 3, 2, 1].map((str) => {
const fret = byString.get(str);
if (fret === "x" || fret === undefined) return null;
if (fret === 0) return 0;
return fret + position.position - 1;
});
}
describe("chord lookup across every key and scale the UI offers", () => {
const triads = allTriads();
it("covers the full 12 keys x 9 scales matrix", () => {
expect(triads.length).toBe(588);
});
// Regression for the crash on keys like Eb minor and Ab minor: Mode.triads
// spells these with double accidentals (Cb, Fb, Bbb, Ebb, F##), which a
// note-name-keyed lookup misses. getChordVariations returned null and the
// render sites dereferenced it, blanking the whole page.
it.each(triads)("resolves %s %s -> %s", (_key, _scale, chordName) => {
const result = getChordVariations(chordName);
expect(result).not.toBeNull();
expect(result.positions.length).toBeGreaterThan(0);
});
});
describe("enharmonic spellings resolve to the same fingerings", () => {
// Left side is what Mode.triads produces; right side is the spelling
// chords-db actually stores.
it.each([
["Cb", "B"],
["Fb", "E"],
["Bbb", "A"],
["Ebb", "D"],
["F##", "G"],
["E#", "F"],
["B#", "C"],
["Cbm", "Bm"],
["F##dim", "Gdim"],
])("%s resolves identically to %s", (spelled, canonical) => {
const a = getChordVariations(spelled);
const b = getChordVariations(canonical);
expect(a).not.toBeNull();
expect(b).not.toBeNull();
expect(a.positions).toEqual(b.positions);
});
it("still returns null for a chord that is not a chord", () => {
expect(getChordVariations("InvalidChord")).toBeNull();
});
});
describe("tuning-aware fingerings", () => {
it("returns the database fingerings unchanged for standard tuning", () => {
expect(getChordVariations("C", CHORDS_DB_TUNING)).toEqual(
getChordVariations("C")
);
});
it("frets the low string higher in Drop D", () => {
const standard = getChordVariations("G", TUNINGS.Standard).positions[0];
const dropD = getChordVariations("G", TUNINGS["Drop D"]).positions[0];
// Open low E in standard G; two frets up in Drop D to sound the same G.
expect(absoluteFrets(standard)[0]).toBe(3);
expect(absoluteFrets(dropD)[0]).toBe(5);
});
it("shifts every string up one fret in Half Step Down", () => {
const standard = getChordVariations("C", TUNINGS.Standard).positions[0];
const halfStep = getChordVariations(
"C",
TUNINGS["Half Step Down"]
).positions[0];
const before = absoluteFrets(standard);
const after = absoluteFrets(halfStep);
before.forEach((fret, i) => {
if (fret === null) expect(after[i]).toBeNull();
else expect(after[i]).toBe(fret + 1);
});
});
it("preserves the sounding pitches in every supported tuning", () => {
for (const chordName of ["C", "G", "Am", "F", "Bdim", "Dm7", "Emaj7"]) {
const reference = getChordVariations(
chordName,
TUNINGS.Standard
).positions.map((p) => JSON.stringify(p.midi));
for (const [label, tuning] of Object.entries(TUNINGS)) {
const positions = getChordVariations(chordName, tuning).positions;
expect(positions.length, `${chordName} in ${label}`).toBeGreaterThan(0);
for (const position of positions) {
expect(
reference,
`${chordName} in ${label} changed the voicing`
).toContain(JSON.stringify(position.midi));
}
}
}
});
it("midi matches what the diagram actually shows", () => {
for (const [, tuning] of Object.entries(TUNINGS)) {
for (const chordName of ["C", "G", "F", "Am"]) {
for (const position of getChordVariations(chordName, tuning)
.positions) {
const sounded = absoluteFrets(position)
.map((fret, i) =>
fret === null ? null : Note.midi(tuning[i]) + fret
)
.filter((note) => note !== null);
expect(sounded).toEqual(position.midi);
}
}
}
});
it("never emits a fret behind the nut", () => {
for (const [, tuning] of Object.entries(TUNINGS)) {
for (const [, , chordName] of allTriads()) {
const result = getChordVariations(chordName, tuning);
for (const position of result.positions) {
expect(position.position).toBeGreaterThan(0);
for (const [, fret] of position.fingers) {
if (typeof fret === "number") expect(fret).toBeGreaterThanOrEqual(0);
}
}
}
}
});
// Retuning can stretch a shape past the window Chord.svelte draws, which
// renders fingers off the end of the neck. Worst observed before the fix was
// a C sus voicing spanning frets 1-9 in Half Step Down, because the open G
// string it leaned on had to be fretted once the tuning dropped.
it("never emits a fret past the diagram window", () => {
for (const [, tuning] of Object.entries(TUNINGS)) {
for (const [, , chordName] of allTriads()) {
for (const position of getChordVariations(chordName, tuning)
.positions) {
for (const [, fret] of position.fingers) {
if (typeof fret === "number") {
expect(fret).toBeLessThanOrEqual(FRET_WINDOW);
}
}
}
}
}
});
it("keeps every retuned shape within a hand's span", () => {
// Open strings ride above the nut and cost no reach, so only fretted
// notes count toward the span.
for (const [, tuning] of Object.entries(TUNINGS)) {
for (const [, , chordName] of allTriads()) {
for (const position of getChordVariations(chordName, tuning)
.positions) {
const fretted = position.fingers
.map(([, fret]) => fret)
.filter((fret) => typeof fret === "number" && fret > 0);
if (fretted.length < 2) continue;
const span = Math.max(...fretted) - Math.min(...fretted) + 1;
expect(span).toBeLessThanOrEqual(FRET_WINDOW);
}
}
}
});
// Seven-string support is a known gap: convertToSVGuitarFormat hardcodes a
// six-string reversal (6 - i). Rather than emit wrong shapes we fall back to
// the standard-tuning fingerings.
it("falls back to standard fingerings for a seven-string tuning", () => {
const sevenString = ["B2", "E2", "A2", "D3", "G3", "B3", "E4"];
expect(getChordVariations("C", sevenString)).toEqual(
getChordVariations("C")
);
});
});