Skip to content

Commit f857a61

Browse files
committed
all systems functional
1 parent faf69e9 commit f857a61

6 files changed

Lines changed: 364 additions & 229 deletions

File tree

src/frets/system/caged.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@ export default function caged(strings, scale) {
2424

2525
scale.notes.forEach((noteName, index) => {
2626
const noteObj = Note.get(noteName);
27-
// For diatonic (7 notes): degrees are 1-7
28-
// For pentatonic (5 notes): degrees are 0-4 (0-indexed as in the patterns)
29-
const degree = noteCount === 7 ? index + 1 : index;
27+
// Scale degrees are 1-indexed (1-5 for pentatonic, 1-7 for diatonic)
28+
const degree = index + 1;
3029
chromaToScaleDegree.set(noteObj.chroma, degree);
3130
chromaToInterval.set(noteObj.chroma, scale.intervals[index]);
3231
});

src/frets/system/caged.test.js

Lines changed: 137 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,148 @@
11
import { describe, expect, it } from "vitest";
22
import { Scale } from "tonal";
33
import frets from "../index.js";
4+
import caged from "./caged.js";
45

56
describe("CAGED system", () => {
6-
it("assigns G shape (position 5) correctly for C major", () => {
7-
const scale = Scale.get("C major");
8-
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
9-
const lowE = fb.strings[5]; // Low E string (reversed order)
7+
describe("C major scale", () => {
8+
it("assigns G shape (position 5) correctly - starts at fret 5 with A, B, C", () => {
9+
const scale = Scale.get("C major");
10+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
11+
const lowE = fb.strings[5]; // Low E string (reversed order)
1012

11-
// G shape on low E string should have A (fret 5), B (fret 7), C (fret 8)
12-
const aNote = lowE[5];
13-
const bNote = lowE[7];
14-
const cNote = lowE[8];
13+
// G shape pattern on low E has degrees [6, 7, 1] = A, B, C
14+
expect(lowE[5].note.pc).toBe("A");
15+
expect(lowE[5].positions.CAGED).toContain(5);
1516

16-
expect(aNote.note.pc).toBe("A");
17-
expect(aNote.positions.CAGED).toContain(5);
17+
expect(lowE[7].note.pc).toBe("B");
18+
expect(lowE[7].positions.CAGED).toContain(5);
1819

19-
expect(bNote.note.pc).toBe("B");
20-
expect(bNote.positions.CAGED).toContain(5);
20+
expect(lowE[8].note.pc).toBe("C");
21+
expect(lowE[8].positions.CAGED).toContain(5);
22+
});
2123

22-
expect(cNote.note.pc).toBe("C");
23-
expect(cNote.positions.CAGED).toContain(5);
24+
it("assigns E shape (position 1) correctly - starts at open position", () => {
25+
const scale = Scale.get("C major");
26+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
27+
const lowE = fb.strings[5];
28+
29+
// E shape pattern on low E has degrees [7, 1, 2] = B, C, D
30+
expect(lowE[7].note.pc).toBe("B");
31+
expect(lowE[7].positions.CAGED).toContain(1);
32+
33+
expect(lowE[8].note.pc).toBe("C");
34+
expect(lowE[8].positions.CAGED).toContain(1);
35+
36+
expect(lowE[10].note.pc).toBe("D");
37+
expect(lowE[10].positions.CAGED).toContain(1);
38+
});
39+
});
40+
41+
describe("C minor scale", () => {
42+
it("assigns G shape (position 5) correctly - starts at fret 8 with C, D, Eb", () => {
43+
const scale = Scale.get("C minor");
44+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
45+
const lowE = fb.strings[5];
46+
47+
// For minor, G shape pattern rotates +2, so low E has degrees [1, 2, 3] = C, D, Eb
48+
expect(lowE[8].note.pc).toBe("C");
49+
expect(lowE[8].positions.CAGED).toContain(5);
50+
51+
expect(lowE[10].note.pc).toBe("D");
52+
expect(lowE[10].positions.CAGED).toContain(5);
53+
54+
// Use label which reflects the scale's spelling (E♭ not D#)
55+
expect(lowE[11].label).toBe("E♭");
56+
expect(lowE[11].positions.CAGED).toContain(5);
57+
});
58+
});
59+
60+
describe("error handling", () => {
61+
it("throws error for chromatic scale (12 notes)", () => {
62+
const chromatic = Scale.get("C chromatic");
63+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16);
64+
expect(() => caged(fb.strings, chromatic)).toThrow(
65+
"CAGED system only works with 5-note pentatonic or 7-note scales"
66+
);
67+
});
68+
69+
it("throws error for 6-note scales", () => {
70+
const blues = Scale.get("C blues");
71+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16);
72+
expect(() => caged(fb.strings, blues)).toThrow(
73+
"CAGED system only works with 5-note pentatonic or 7-note scales"
74+
);
75+
});
76+
});
77+
78+
describe("position assignment", () => {
79+
it("assigns positions to all scale notes", () => {
80+
const scale = Scale.get("G major");
81+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
82+
83+
for (const string of fb.strings) {
84+
for (const note of string) {
85+
if (note.interval) {
86+
expect(note.positions.CAGED).toBeDefined();
87+
expect(Array.isArray(note.positions.CAGED)).toBe(true);
88+
expect(note.positions.CAGED.length).toBeGreaterThan(0);
89+
}
90+
}
91+
}
92+
});
93+
94+
it("assigns positions between 1 and 5", () => {
95+
const scale = Scale.get("A minor");
96+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
97+
98+
for (const string of fb.strings) {
99+
for (const note of string) {
100+
if (note.positions.CAGED?.length > 0) {
101+
for (const pos of note.positions.CAGED) {
102+
expect(pos).toBeGreaterThanOrEqual(1);
103+
expect(pos).toBeLessThanOrEqual(5);
104+
}
105+
}
106+
}
107+
}
108+
});
109+
110+
it("does not assign positions to non-scale notes", () => {
111+
const scale = Scale.get("C major");
112+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
113+
114+
for (const string of fb.strings) {
115+
for (const note of string) {
116+
if (!note.interval) {
117+
expect(
118+
note.positions.CAGED === undefined ||
119+
note.positions.CAGED.length === 0
120+
).toBe(true);
121+
}
122+
}
123+
}
124+
});
125+
});
126+
127+
describe("works with different keys", () => {
128+
it("works with G major", () => {
129+
const scale = Scale.get("G major");
130+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
131+
const lowE = fb.strings[5];
132+
133+
// G is at fret 3, should be in position 5 (G shape starts on root)
134+
expect(lowE[3].note.pc).toBe("G");
135+
expect(lowE[3].positions.CAGED).toContain(5);
136+
});
137+
138+
it("works with E minor", () => {
139+
const scale = Scale.get("E minor");
140+
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
141+
const lowE = fb.strings[5];
142+
143+
// E is at fret 0 (open), should be in G shape (position 5) for minor
144+
expect(lowE[0].note.pc).toBe("E");
145+
expect(lowE[0].positions.CAGED).toContain(5);
146+
});
24147
});
25148
});

src/frets/system/patterns.js

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,50 @@
22
// These patterns define which scale degrees appear on each string for each CAGED shape
33
// Strings are in reversed order: [high E, B, G, D, A, low E]
44
//
5-
// Scale degrees are indexed 0-4 for pentatonic (5 notes) or 0-6 for diatonic (7 notes)
5+
// Scale degrees are 1-5 for pentatonic or 1-7 for diatonic
66

77
// Pentatonic patterns (2 notes per string)
88
// Used by both Pentatonic and CAGED position systems for 5-note scales
99
export const pentatonicPatterns = {
1010
C: [
11-
[3, 4], // High E string: 5th, m7/6th
12-
[1, 2], // B string: 2nd, 4th
13-
[4, 0], // G string: m7/6th, root
14-
[2, 3], // D string: 4th, 5th
15-
[0, 1], // A string: root, 2nd
16-
[3, 4], // Low E string: 5th, m7/6th
11+
[4, 5], // High E string: 5th, m7/6th
12+
[2, 3], // B string: 2nd, 4th
13+
[5, 1], // G string: m7/6th, root
14+
[3, 4], // D string: 4th, 5th
15+
[1, 2], // A string: root, 2nd
16+
[4, 5], // Low E string: 5th, m7/6th
1717
],
1818
A: [
19-
[4, 0], // High E string: m7/6th, root
20-
[2, 3], // B string: 4th, 5th
21-
[0, 1], // G string: root, 2nd
22-
[3, 4], // D string: 5th, m7/6th
23-
[1, 2], // A string: 2nd, 4th
24-
[4, 0], // Low E string: m7/6th, root
19+
[5, 1], // High E string: m7/6th, root
20+
[3, 4], // B string: 4th, 5th
21+
[1, 2], // G string: root, 2nd
22+
[4, 5], // D string: 5th, m7/6th
23+
[2, 3], // A string: 2nd, 4th
24+
[5, 1], // Low E string: m7/6th, root
2525
],
2626
G: [
27-
[0, 1], // High E string: root, 2nd
28-
[3, 4], // B string: 5th, m7/6th
29-
[1, 2], // G string: 2nd, 4th
30-
[4, 0], // D string: m7/6th, root
31-
[2, 3], // A string: 4th, 5th
32-
[0, 1], // Low E string: root, 2nd
27+
[1, 2], // High E string: root, 2nd
28+
[4, 5], // B string: 5th, m7/6th
29+
[2, 3], // G string: 2nd, 4th
30+
[5, 1], // D string: m7/6th, root
31+
[3, 4], // A string: 4th, 5th
32+
[1, 2], // Low E string: root, 2nd
3333
],
3434
E: [
35-
[1, 2], // High E string: 2nd, 4th
36-
[4, 0], // B string: m7/6th, root
37-
[2, 3], // G string: 4th, 5th
38-
[0, 1], // D string: root, 2nd
39-
[3, 4], // A string: 5th, m7/6th
40-
[1, 2], // Low E string: 2nd, 4th
35+
[2, 3], // High E string: 2nd, 4th
36+
[5, 1], // B string: m7/6th, root
37+
[3, 4], // G string: 4th, 5th
38+
[1, 2], // D string: root, 2nd
39+
[4, 5], // A string: 5th, m7/6th
40+
[2, 3], // Low E string: 2nd, 4th
4141
],
4242
D: [
43-
[2, 3], // High E string: 4th, 5th
44-
[0, 1], // B string: root, 2nd
45-
[3, 4], // G string: 5th, m7/6th
46-
[1, 2], // D string: 2nd, 4th
47-
[4, 0], // A string: m7/6th, root
48-
[2, 3], // Low E string: 4th, 5th
43+
[3, 4], // High E string: 4th, 5th
44+
[1, 2], // B string: root, 2nd
45+
[4, 5], // G string: 5th, m7/6th
46+
[2, 3], // D string: 2nd, 4th
47+
[5, 1], // A string: m7/6th, root
48+
[3, 4], // Low E string: 4th, 5th
4949
],
5050
};
5151

@@ -93,9 +93,8 @@ export const diatonicPatterns = {
9393
],
9494
};
9595

96-
// Position-to-shape mapping for CAGED and Pentatonic systems
96+
// Pentatonic box mapping for CAGED to Pentatonic boxes
9797
// Pentatonic: Position 1=G, 2=E, 3=D, 4=C, 5=A
98-
// CAGED: Position 1=C, 2=A, 3=G, 4=E, 5=D
9998
export const pentatonicPositionMapping = {
10099
1: "G",
101100
2: "E",

src/frets/system/pentatonic.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,21 @@ export default function pentatonic(strings, scale) {
3333
// Major pentatonic: 1P 2M 3M 5P 6M
3434
const isMajor = intervals.includes("3M");
3535

36-
// For major pentatonic, we need to rotate the pattern indices
36+
// For major pentatonic, we need to rotate the pattern degrees
3737
// This is because C major Position 1 should start at the 6th degree (A),
3838
// while C minor Position 1 starts at the root (C)
3939
// The offset is 4 positions forward (or 1 position backward) in the pentatonic scale
4040
const patternOffset = isMajor ? 4 : 0;
4141

42+
// Helper to rotate 1-indexed degrees (1-5)
43+
const rotateDegree = (degree, offset) => ((degree - 1 + offset) % 5) + 1;
44+
4245
const pentatonicShapes = Object.entries(pentatonicPositionMapping).map(
4346
([pos, shape]) => {
4447
const basePattern = pentatonicPatterns[shape];
45-
// Rotate the pattern indices for major scales
48+
// Rotate the pattern degrees for major scales
4649
const rotatedPattern = basePattern.map((degreeArr) =>
47-
degreeArr.map((degree) => (degree + patternOffset) % 5)
50+
degreeArr.map((degree) => rotateDegree(degree, patternOffset))
4851
);
4952

5053
return {
@@ -65,14 +68,16 @@ export default function pentatonic(strings, scale) {
6568

6669
if (scaleNote) {
6770
const positions = [];
68-
const scaleDegreeIndex = scaleNotes.findIndex(
69-
(sn) => sn.note.chroma === semitone.note.chroma
70-
);
71+
// Scale degree is 1-indexed (1-5)
72+
const scaleDegree =
73+
scaleNotes.findIndex(
74+
(sn) => sn.note.chroma === semitone.note.chroma
75+
) + 1;
7176

7277
for (const shape of pentatonicShapes) {
7378
const stringPattern = shape.pattern[stringIndex];
7479

75-
if (stringPattern.includes(scaleDegreeIndex % 5)) {
80+
if (stringPattern.includes(scaleDegree)) {
7681
positions.push(shape.position);
7782
}
7883
}

0 commit comments

Comments
 (0)