Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions scripts/debug-patterns
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
import { Scale } from "tonal";
import { diatonicPatterns, pentatonicPatterns } from "../src/frets/system/patterns.js";

/**
* Build a mapping from diatonic scale degree (1-7) to array index
* For 7-note scales: degree 1 -> 0, degree 2 -> 1, etc.
* For 5-note scales: uses intervals to map (e.g., major pent 1,2,3,5,6 -> 0,1,2,3,4)
*/
function buildDegreeToIndexMap(intervals) {
const map = {};
intervals.forEach((interval, index) => {
// Extract the degree number from interval like "1P", "2M", "3m", "5P", "6M"
const degree = parseInt(interval);
map[degree] = index;
});
return map;
}

/**
* Debug utility to visualize pattern indexes as actual note names or intervals
*/
Expand Down Expand Up @@ -33,17 +48,28 @@ function debugPattern(scaleName, shape, useIntervals = false) {
const displayMode = useIntervals ? "Intervals" : "Notes";
const displayValues = useIntervals ? intervals : notes;

// Build degree-to-index mapping for pentatonic scales
const degreeToIndex = isPentatonic ? buildDegreeToIndexMap(intervals) : null;

console.log(`\n${"=".repeat(60)}`);
console.log(`Scale: ${scaleName}`);
console.log(`Shape: ${shape}`);
console.log(`${displayMode}: ${displayValues.join(", ")}`);
if (isPentatonic) {
console.log(`Degree mapping: ${Object.entries(degreeToIndex).map(([d, i]) => `${d}→${i}`).join(", ")}`);
}
console.log(`${"=".repeat(60)}\n`);

pattern.forEach((stringPattern, stringIndex) => {
const stringName = stringNames[stringIndex].padEnd(6);
const indexes = stringPattern.map(i => String(i).padStart(2)).join(", ");
// Scale degrees are 1-indexed (1-7), so subtract 1 to get array index (0-6)
const actualValues = stringPattern.map(i => displayValues[i - 1].padEnd(3)).join(", ");

// For diatonic: degree - 1 gives array index
// For pentatonic: use the degree-to-index mapping
const actualValues = stringPattern.map(degree => {
const index = isPentatonic ? degreeToIndex[degree] : degree - 1;
return displayValues[index].padEnd(3);
}).join(", ");

console.log(`${stringName}: [${indexes}] → [${actualValues}]`);
});
Expand Down
41 changes: 28 additions & 13 deletions src/frets/system/caged.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,47 @@ export default function caged(strings, scale) {
"CAGED system only works with 5-note pentatonic or 7-note scales"
);

// Determine if this is a minor scale (has minor 3rd)
const isMinor = scale.intervals.includes("3m");

// Build a map from chroma to scale degree and interval
// Both pentatonic and diatonic patterns use 7-degree format
const chromaToScaleDegree = new Map();
const chromaToInterval = new Map();

scale.notes.forEach((noteName, index) => {
const noteObj = Note.get(noteName);
// Scale degrees are 1-indexed (1-5 for pentatonic, 1-7 for diatonic)
const degree = index + 1;
chromaToScaleDegree.set(noteObj.chroma, degree);
chromaToInterval.set(noteObj.chroma, scale.intervals[index]);
});

// Determine if this is a minor scale (has minor 3rd)
const isMinor = scale.intervals.includes("3m");
if (noteCount === 5) {
// Pentatonic: map notes to their 7-degree equivalents
// Major pentatonic uses degrees: 1, 2, 3, 5, 6
// Minor pentatonic uses degrees: 1, 3, 4, 5, 7
const majorDegrees = [1, 2, 3, 5, 6];
const minorDegrees = [1, 3, 4, 5, 7];
const degreeMapping = isMinor ? minorDegrees : majorDegrees;

scale.notes.forEach((noteName, index) => {
const noteObj = Note.get(noteName);
chromaToScaleDegree.set(noteObj.chroma, degreeMapping[index]);
chromaToInterval.set(noteObj.chroma, scale.intervals[index]);
});
} else {
// Diatonic: degrees are 1-7
scale.notes.forEach((noteName, index) => {
const noteObj = Note.get(noteName);
chromaToScaleDegree.set(noteObj.chroma, index + 1);
chromaToInterval.set(noteObj.chroma, scale.intervals[index]);
});
}

// Build CAGED shapes with their patterns
// For minor scales, rotate the diatonic pattern degrees by +2
// For minor scales, rotate the pattern degrees by +2
// This shifts the shapes so that G shape root aligns at fret 8 instead of fret 5
const basePatterns = noteCount === 5 ? pentatonicPatterns : diatonicPatterns;

const cagedShapes = Object.entries(cagedPositionMapping).map(
([pos, shape]) => {
let pattern = basePatterns[shape];

// For 7-note minor scales, rotate degrees by +2
if (noteCount === 7 && isMinor) {
// For minor scales (both pentatonic and diatonic), rotate degrees by +2
if (isMinor) {
pattern = pattern.map((stringDegrees) =>
stringDegrees.map((degree) => rotateDegree(degree, 2))
);
Expand Down
165 changes: 165 additions & 0 deletions src/frets/system/caged.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,169 @@ describe("CAGED system", () => {
expect(lowE[0].positions.CAGED).toContain(5);
});
});

describe("C major pentatonic", () => {
it("assigns positions using 7-degree mapping (1,2,3,5,6)", () => {
const scale = Scale.get("C major pentatonic");
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
const lowE = fb.strings[5];

// C major pentatonic: C(1), D(2), E(3), G(5), A(6)
// On low E: E at fret 0, G at fret 3, A at fret 5, C at fret 8, D at fret 10
expect(lowE[0].note.pc).toBe("E");
expect(lowE[0].positions.CAGED).toBeDefined();
expect(lowE[0].positions.CAGED.length).toBeGreaterThan(0);

expect(lowE[3].note.pc).toBe("G");
expect(lowE[3].positions.CAGED).toBeDefined();

expect(lowE[5].note.pc).toBe("A");
expect(lowE[5].positions.CAGED).toBeDefined();

expect(lowE[8].note.pc).toBe("C");
expect(lowE[8].positions.CAGED).toBeDefined();

expect(lowE[10].note.pc).toBe("D");
expect(lowE[10].positions.CAGED).toBeDefined();
});

it("assigns all 5 positions across the fretboard", () => {
const scale = Scale.get("C major pentatonic");
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);

const allPositions = new Set();
for (const string of fb.strings) {
for (const note of string) {
if (note.positions.CAGED) {
note.positions.CAGED.forEach((p) => allPositions.add(p));
}
}
}

expect(allPositions.size).toBe(5);
expect([...allPositions].sort()).toEqual([1, 2, 3, 4, 5]);
});

it("assigns correct intervals to scale notes", () => {
const scale = Scale.get("C major pentatonic");
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);

// C major pentatonic intervals: 1P, 2M, 3M, 5P, 6M
const expectedIntervals = ["1P", "2M", "3M", "5P", "6M"];

for (const string of fb.strings) {
for (const note of string) {
if (note.interval) {
expect(expectedIntervals).toContain(note.interval);
}
}
}
});
});

describe("C minor pentatonic", () => {
it("assigns positions with rotation for minor scale", () => {
const scale = Scale.get("C minor pentatonic");
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
const lowE = fb.strings[5];

// C minor pentatonic: C(1), Eb(3), F(4), G(5), Bb(7)
// On low E: F at fret 1, G at fret 3, Bb at fret 6, C at fret 8
expect(lowE[1].note.pc).toBe("F");
expect(lowE[1].positions.CAGED).toBeDefined();
expect(lowE[1].positions.CAGED.length).toBeGreaterThan(0);

expect(lowE[3].note.pc).toBe("G");
expect(lowE[3].positions.CAGED).toBeDefined();

expect(lowE[6].note.pc).toBe("Bb");
expect(lowE[6].positions.CAGED).toBeDefined();

expect(lowE[8].note.pc).toBe("C");
expect(lowE[8].positions.CAGED).toBeDefined();
});

it("assigns all 5 positions across the fretboard", () => {
const scale = Scale.get("C minor pentatonic");
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);

const allPositions = new Set();
for (const string of fb.strings) {
for (const note of string) {
if (note.positions.CAGED) {
note.positions.CAGED.forEach((p) => allPositions.add(p));
}
}
}

expect(allPositions.size).toBe(5);
expect([...allPositions].sort()).toEqual([1, 2, 3, 4, 5]);
});

it("assigns correct intervals to scale notes", () => {
const scale = Scale.get("C minor pentatonic");
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);

// C minor pentatonic intervals: 1P, 3m, 4P, 5P, 7m
const expectedIntervals = ["1P", "3m", "4P", "5P", "7m"];

for (const string of fb.strings) {
for (const note of string) {
if (note.interval) {
expect(expectedIntervals).toContain(note.interval);
}
}
}
});
});

describe("pentatonic position assignment", () => {
it("assigns positions to all pentatonic scale notes", () => {
const scale = Scale.get("A minor pentatonic");
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);

for (const string of fb.strings) {
for (const note of string) {
if (note.interval) {
expect(note.positions.CAGED).toBeDefined();
expect(Array.isArray(note.positions.CAGED)).toBe(true);
expect(note.positions.CAGED.length).toBeGreaterThan(0);
}
}
}
});

it("assigns positions between 1 and 5 for pentatonic", () => {
const scale = Scale.get("G major pentatonic");
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);

for (const string of fb.strings) {
for (const note of string) {
if (note.positions.CAGED?.length > 0) {
for (const pos of note.positions.CAGED) {
expect(pos).toBeGreaterThanOrEqual(1);
expect(pos).toBeLessThanOrEqual(5);
}
}
}
}
});

it("does not assign positions to non-scale notes for pentatonic", () => {
const scale = Scale.get("E minor pentatonic");
const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale);
const scaleNotes = scale.notes.map((n) => n.replace(/[0-9]/g, ""));

for (const string of fb.strings) {
for (const note of string) {
if (!scaleNotes.includes(note.note.pc)) {
expect(
note.positions.CAGED === undefined ||
note.positions.CAGED.length === 0
).toBe(true);
}
}
}
});
});
});
61 changes: 31 additions & 30 deletions src/frets/system/patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,47 @@

// Pentatonic patterns (2 notes per string)
// Used by both Pentatonic and CAGED position systems for 5-note scales
// Scale degrees are 1-6
export const pentatonicPatterns = {
C: [
[4, 5], // High E string: 5th, m7/6th
[2, 3], // B string: 2nd, 4th
[5, 1], // G string: m7/6th, root
[3, 4], // D string: 4th, 5th
[1, 2], // A string: root, 2nd
[4, 5], // Low E string: 5th, m7/6th
[3, 5],
[1, 2],
[5, 6],
[2, 3],
[6, 1],
[3, 5],
],
A: [
[5, 1], // High E string: m7/6th, root
[3, 4], // B string: 4th, 5th
[1, 2], // G string: root, 2nd
[4, 5], // D string: 5th, m7/6th
[2, 3], // A string: 2nd, 4th
[5, 1], // Low E string: m7/6th, root
[5, 6],
[2, 3],
[6, 1],
[3, 5],
[1, 2],
[5, 6],
],
G: [
[1, 2], // High E string: root, 2nd
[4, 5], // B string: 5th, m7/6th
[2, 3], // G string: 2nd, 4th
[5, 1], // D string: m7/6th, root
[3, 4], // A string: 4th, 5th
[1, 2], // Low E string: root, 2nd
[6, 1],
[3, 5],
[1, 2],
[5, 6],
[2, 3],
[6, 1],
],
E: [
[2, 3], // High E string: 2nd, 4th
[5, 1], // B string: m7/6th, root
[3, 4], // G string: 4th, 5th
[1, 2], // D string: root, 2nd
[4, 5], // A string: 5th, m7/6th
[2, 3], // Low E string: 2nd, 4th
[1, 2],
[5, 6],
[2, 3],
[6, 1],
[3, 5],
[1, 2],
],
D: [
[3, 4], // High E string: 4th, 5th
[1, 2], // B string: root, 2nd
[4, 5], // G string: 5th, m7/6th
[2, 3], // D string: 2nd, 4th
[5, 1], // A string: m7/6th, root
[3, 4], // Low E string: 4th, 5th
[2, 3],
[6, 1],
[3, 5],
[1, 2],
[5, 6],
[2, 3],
],
};

Expand Down
Loading