diff --git a/scripts/debug-patterns b/scripts/debug-patterns new file mode 100755 index 0000000..3be2fd8 --- /dev/null +++ b/scripts/debug-patterns @@ -0,0 +1,120 @@ +#!/usr/bin/env node + +import { Scale } from "tonal"; +import { diatonicPatterns, pentatonicPatterns } from "../src/frets/system/patterns.js"; + +/** + * Debug utility to visualize pattern indexes as actual note names or intervals + */ +function debugPattern(scaleName, shape, useIntervals = false) { + const scale = Scale.get(scaleName); + const notes = scale.notes; + const intervals = scale.intervals; + + if (!scale || !notes || notes.length === 0) { + throw new Error(`Invalid scale: ${scaleName}`); + } + + const isPentatonic = notes.length === 5; + const isDiatonic = notes.length === 7; + + if (!isPentatonic && !isDiatonic) { + throw new Error(`Scale must be 5 or 7 notes. ${scaleName} has ${notes.length} notes.`); + } + + const patterns = isPentatonic ? pentatonicPatterns : diatonicPatterns; + const pattern = patterns[shape]; + + if (!pattern) { + throw new Error(`Invalid shape: ${shape}. Must be one of C, A, G, E, D`); + } + + const stringNames = ["High E", "B", "G", "D", "A", "Low E"]; + const displayMode = useIntervals ? "Intervals" : "Notes"; + const displayValues = useIntervals ? intervals : notes; + + console.log(`\n${"=".repeat(60)}`); + console.log(`Scale: ${scaleName}`); + console.log(`Shape: ${shape}`); + console.log(`${displayMode}: ${displayValues.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(", "); + + console.log(`${stringName}: [${indexes}] → [${actualValues}]`); + }); + + console.log(`\n${"=".repeat(60)}\n`); +} + +/** + * Debug all shapes for a given scale + */ +function debugAllShapes(scaleName, useIntervals = false) { + const shapes = ["C", "A", "G", "E", "D"]; + shapes.forEach(shape => debugPattern(scaleName, shape, useIntervals)); +} + +function printUsage() { + console.log(` +Usage: debug-patterns [options] + +Options: + -s, --scale Scale name (e.g., "C major", "A minor pentatonic") + -p, --position CAGED position/shape (C, A, G, E, or D) + -i, --intervals Display intervals instead of note names + -h, --help Show this help message + +Examples: + debug-patterns -s "C major" -p G + debug-patterns --scale "A minor pentatonic" --position E + debug-patterns -s "C major" -i + debug-patterns -s "C major" -p G --intervals + +If no position is specified, all positions will be displayed. +`); +} + +// Parse command line arguments +const args = process.argv.slice(2); +let scaleName = null; +let shape = null; +let useIntervals = false; + +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '-h' || arg === '--help') { + printUsage(); + process.exit(0); + } else if (arg === '-s' || arg === '--scale') { + scaleName = args[++i]; + } else if (arg === '-p' || arg === '--position') { + shape = args[++i]; + } else if (arg === '-i' || arg === '--intervals') { + useIntervals = true; + } +} + +// Validate required arguments +if (!scaleName) { + console.error('Error: Scale name is required\n'); + printUsage(); + process.exit(1); +} + +// Run the debug +try { + if (shape) { + debugPattern(scaleName, shape, useIntervals); + } else { + debugAllShapes(scaleName, useIntervals); + } +} catch (error) { + console.error(`Error: ${error.message}`); + process.exit(1); +} diff --git a/src/frets/index.test.js b/src/frets/index.test.js index 7b79ad4..ff5e429 100644 --- a/src/frets/index.test.js +++ b/src/frets/index.test.js @@ -1,4 +1,5 @@ import { describe, it, expect } from "vitest"; +import { Scale } from "tonal"; import frets from "./index.js"; describe("fretboard tests", () => { @@ -44,11 +45,13 @@ describe("fretboard tests", () => { expect(notes[0].length).toBe(24); }); - it("generates correct start notes for each string", () => { + it("generates correct start notes for each string (reversed by default)", () => { const fb = frets(); const notes = fb.strings; - fb.tuning.forEach((tuningNote, i) => { + // Strings are reversed by default: [E4, B3, G3, D3, A2, E2] + const reversedTuning = [...fb.tuning].reverse(); + reversedTuning.forEach((tuningNote, i) => { expect(notes[i][0].note.name).toBe(tuningNote); }); }); @@ -97,27 +100,29 @@ describe("fretboard tests", () => { }); }); - it("generates all 6 strings for standard tuning", () => { + it("generates all 6 strings for standard tuning (reversed)", () => { const fb = frets(); const notes = fb.strings; + // Strings are reversed: index 0 = high E, index 5 = low E expect(notes.length).toBe(6); - expect(notes[0][0].note.name).toBe("E2"); - expect(notes[1][0].note.name).toBe("A2"); - expect(notes[2][0].note.name).toBe("D3"); - expect(notes[3][0].note.name).toBe("G3"); - expect(notes[4][0].note.name).toBe("B3"); - expect(notes[5][0].note.name).toBe("E4"); + expect(notes[0][0].note.name).toBe("E4"); // High E + expect(notes[1][0].note.name).toBe("B3"); + expect(notes[2][0].note.name).toBe("G3"); + expect(notes[3][0].note.name).toBe("D3"); + expect(notes[4][0].note.name).toBe("A2"); + expect(notes[5][0].note.name).toBe("E2"); // Low E }); it("generates correct notes at specific fret positions", () => { const fb = frets(); const notes = fb.strings; + const lowE = notes[5]; // Low E is at index 5 (reversed) // Test known notes on the low E string - expect(notes[0][0].note.name).toBe("E2"); - expect(notes[0][5].note.name).toBe("A2"); - expect(notes[0][12].note.name).toBe("E3"); + expect(lowE[0].note.name).toBe("E2"); + expect(lowE[5].note.name).toBe("A2"); + expect(lowE[12].note.name).toBe("E3"); }); }); @@ -126,8 +131,9 @@ describe("fretboard tests", () => { const dropD = ["D2", "A2", "D3", "G3", "B3", "E4"]; const fb = frets(dropD); const notes = fb.strings; + const lowString = notes[5]; // Reversed, so low string is at index 5 - expect(notes[0][0].note.name).toBe("D2"); + expect(lowString[0].note.name).toBe("D2"); expect(notes.length).toBe(6); }); @@ -136,9 +142,10 @@ describe("fretboard tests", () => { const fb = frets(dadgad); const notes = fb.strings; - expect(notes[0][0].note.name).toBe("D2"); - expect(notes[4][0].note.name).toBe("A3"); - expect(notes[5][0].note.name).toBe("D4"); + // Reversed order + expect(notes[5][0].note.name).toBe("D2"); // Low D + expect(notes[1][0].note.name).toBe("A3"); + expect(notes[0][0].note.name).toBe("D4"); // High D }); it("works with 7-string tuning", () => { @@ -147,7 +154,7 @@ describe("fretboard tests", () => { const notes = fb.strings; expect(notes.length).toBe(7); - expect(notes[0][0].note.name).toBe("B1"); + expect(notes[6][0].note.name).toBe("B1"); // Reversed, lowest is at end }); it("works with 4-string bass tuning", () => { @@ -156,8 +163,8 @@ describe("fretboard tests", () => { const notes = fb.strings; expect(notes.length).toBe(4); - expect(notes[0][0].note.name).toBe("E1"); - expect(notes[3][0].note.name).toBe("G2"); + expect(notes[3][0].note.name).toBe("E1"); // Low E at end (reversed) + expect(notes[0][0].note.name).toBe("G2"); // High G at start }); }); @@ -189,77 +196,74 @@ describe("fretboard tests", () => { describe("note normalization for scales", () => { it("normalizes to flats for Ab major scale", () => { - const fb = frets(undefined, 13, "Ab major"); + const scale = Scale.get("Ab major"); + const fb = frets(undefined, 13, scale); const notes = fb.strings; + const lowE = notes[5]; // Low E string (reversed) - // Ab major scale should use flats: Ab, Bb, C, Db, Eb, F, G - // The scale notes themselves should be flats - // Check a specific position that would normally be a sharp - // On the low E string (E2), fret 4 is G# / Ab - const fret4Notes = notes.map(string => string[4]); - const lowEStringFret4 = fret4Notes[0]; // First string is low E - - // This should be Ab (not G#) for Ab major scale - expect(lowEStringFret4.label).toBe("Ab"); + // On the low E string, fret 4 is G#/Ab - should be Ab for Ab major + expect(lowE[4].label).toBe("A♭"); }); it("shows all flat notes in Ab major scale intervals", () => { - const fb = frets(undefined, 13, "Ab major"); + const scale = Scale.get("Ab major"); + const fb = frets(undefined, 13, scale); const notes = fb.strings; // Collect all notes that are part of the Ab major scale (have intervals) - const scaleNotes = notes.flat().filter(n => n.interval !== null); - const scaleLabels = [...new Set(scaleNotes.map(n => n.label))]; + const scaleNotes = notes.flat().filter((n) => n.interval !== null); + const scaleLabels = [...new Set(scaleNotes.map((n) => n.label))]; - // Ab major should be: Ab, Bb, C, Db, Eb, F, G - expect(scaleLabels.sort()).toEqual(["Ab", "Bb", "C", "Db", "Eb", "F", "G"]); + // Ab major should be: Ab, Bb, C, Db, Eb, F, G (with ♭ symbols) + expect(scaleLabels.sort()).toEqual(["A♭", "B♭", "C", "D♭", "E♭", "F", "G"]); }); it("does not normalize sharps for G# major scale", () => { - const fb = frets(undefined, 13, "G# major"); + const scale = Scale.get("G# major"); + const fb = frets(undefined, 13, scale); const notes = fb.strings; - // G# major scale uses sharps, so we don't convert anything - // The scale notes should remain as sharps: G#, A#, B# (becomes C), C#, D#, E# (becomes F), F## (becomes G) - const scaleNotes = notes.flat().filter(n => n.interval !== null); - const scaleLabels = [...new Set(scaleNotes.map(n => n.label))]; + // G# major scale uses sharps + const scaleNotes = notes.flat().filter((n) => n.interval !== null); + const scaleLabels = [...new Set(scaleNotes.map((n) => n.label))]; - // Should contain sharps since we don't normalize sharp scales - expect(scaleLabels).toContain("G#"); - expect(scaleLabels).toContain("A#"); - expect(scaleLabels).toContain("C#"); - expect(scaleLabels).toContain("D#"); + // Should contain sharps + expect(scaleLabels).toContain("G♯"); + expect(scaleLabels).toContain("A♯"); + expect(scaleLabels).toContain("C♯"); + expect(scaleLabels).toContain("D♯"); }); it("uses scale note names for notes in the scale", () => { - const fb = frets(undefined, 13, "Ab major"); + const scale = Scale.get("Ab major"); + const fb = frets(undefined, 13, scale); const notes = fb.strings; // Notes that are in the Ab major scale should use the scale's notation - const scaleNotes = notes.flat().filter(n => n.interval !== null); - const scaleLabels = [...new Set(scaleNotes.map(n => n.label))]; + const scaleNotes = notes.flat().filter((n) => n.interval !== null); + const scaleLabels = [...new Set(scaleNotes.map((n) => n.label))]; // Scale notes should all be flats or naturals (no sharps in Ab major) - const scaleHasSharps = scaleLabels.some(label => label.includes("#")); + const scaleHasSharps = scaleLabels.some((label) => label.includes("♯")); expect(scaleHasSharps).toBe(false); - expect(scaleLabels.sort()).toEqual(["Ab", "Bb", "C", "Db", "Eb", "F", "G"]); + expect(scaleLabels.sort()).toEqual(["A♭", "B♭", "C", "D♭", "E♭", "F", "G"]); }); it("uses scale note names including Cb for Gb major", () => { - const fb = frets(undefined, 13, "Gb major"); + const scale = Scale.get("Gb major"); + const fb = frets(undefined, 13, scale); const notes = fb.strings; - // Gb major scale contains Cb, and we should use Cb as the label - // Gb major notes: Gb, Ab, Bb, Cb, Db, Eb, F - const scaleNotes = notes.flat().filter(n => n.interval !== null); - const scaleLabels = [...new Set(scaleNotes.map(n => n.label))]; + // Gb major scale contains Cb + const scaleNotes = notes.flat().filter((n) => n.interval !== null); + const scaleLabels = [...new Set(scaleNotes.map((n) => n.label))]; - // Should have Cb as defined in the scale - expect(scaleLabels).toContain("Cb"); - expect(scaleLabels.sort()).toEqual(["Ab", "Bb", "Cb", "Db", "Eb", "F", "Gb"]); + // Should have C♭ as defined in the scale + expect(scaleLabels).toContain("C♭"); + expect(scaleLabels.sort()).toEqual(["A♭", "B♭", "C♭", "D♭", "E♭", "F", "G♭"]); // All notes should be flats or naturals (no sharps) - const hasSharps = scaleLabels.some(label => label.includes("#")); + const hasSharps = scaleLabels.some((label) => label.includes("♯")); expect(hasSharps).toBe(false); }); }); diff --git a/src/frets/system/caged.js b/src/frets/system/caged.js index 5c82390..d871a2f 100644 --- a/src/frets/system/caged.js +++ b/src/frets/system/caged.js @@ -2,11 +2,15 @@ import { cagedPositionMapping, diatonicPatterns, pentatonicPatterns, - pentatonicPositionMapping, } from "./patterns.js"; import { Note } from "tonal"; +// Rotate a 1-indexed degree (1-7) by an offset +function rotateDegree(degree, offset) { + return ((degree - 1 + offset) % 7) + 1; +} + export default function caged(strings, scale) { const noteCount = scale.intervals.length; if (noteCount !== 5 && noteCount !== 7) @@ -14,73 +18,68 @@ export default function caged(strings, scale) { "CAGED system only works with 5-note pentatonic or 7-note scales" ); - const intervals = scale.intervals; - const snotes = scale.notes; - const scaleNotes = snotes.map((noteName, i) => { + // Build a map from chroma to scale degree and interval + const chromaToScaleDegree = new Map(); + const chromaToInterval = new Map(); + + scale.notes.forEach((noteName, index) => { const noteObj = Note.get(noteName); - return { - note: noteObj, - interval: intervals[i], - name: 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]); }); - // CAGED system uses position mapping: Position 1=C, 2=A, 3=G, 4=E, 5=D - // For pentatonic (5 notes), use pentatonicPatterns (2 notes per string) - // For diatonic (7 notes), use diatonicPatterns (3 notes per string) - const basePatterns = noteCount === 5 ? pentatonicPatterns : diatonicPatterns; - - // Determine if this is a major or minor scale for rotation - const isMajor = intervals.includes("3M"); + // Determine if this is a minor scale (has minor 3rd) + const isMinor = scale.intervals.includes("3m"); - // For major scales, rotate the pattern indices - // This aligns the patterns correctly with the scale degrees - // const patternOffset = (noteCount === 5 && isMajor) ? 4 : 0; - const patternOffset = isMajor ? 4 : 0; + // Build CAGED shapes with their patterns + // For minor scales, rotate the diatonic 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]) => { - const basePattern = basePatterns[shape]; + let pattern = basePatterns[shape]; - // Rotate the pattern indices for major scales - const rotatedPattern = basePattern.map((degreeArr) => - degreeArr.map((degree) => (degree + patternOffset) % noteCount) - ); + // For 7-note minor scales, rotate degrees by +2 + if (noteCount === 7 && isMinor) { + pattern = pattern.map((stringDegrees) => + stringDegrees.map((degree) => rotateDegree(degree, 2)) + ); + } return { position: parseInt(pos), shape: shape, - pattern: rotatedPattern, + pattern: pattern, }; } ); + // Process each string and note for (let stringIndex = 0; stringIndex < strings.length; stringIndex++) { - const str = strings[stringIndex]; + const string = strings[stringIndex]; - for (const semitone of str) { - const scaleNote = scaleNotes.find( - (sn) => sn.note.chroma === semitone.note.chroma - ); + for (const fretNote of string) { + const chroma = fretNote.note.chroma; - if (scaleNote) { - const positions = []; - const scaleDegreeIndex = scaleNotes.findIndex( - (sn) => sn.note.chroma === semitone.note.chroma - ); + // Skip notes not in the scale + if (!chromaToScaleDegree.has(chroma)) continue; - for (const shape of cagedShapes) { - const stringPattern = shape.pattern[stringIndex]; + const scaleDegree = chromaToScaleDegree.get(chroma); + const positions = []; - if (stringPattern.includes(scaleDegreeIndex % noteCount)) { - positions.push(shape.position); - } + // Check each CAGED shape to see if this scale degree is on this string + for (const shape of cagedShapes) { + const stringPattern = shape.pattern[stringIndex]; + if (stringPattern.includes(scaleDegree)) { + positions.push(shape.position); } - - // Store positions under the CAGED system key - semitone.positions.CAGED = [...new Set(positions)].sort(); - semitone.interval = scaleNote.interval; } + + fretNote.positions.CAGED = [...new Set(positions)].sort(); + fretNote.interval = chromaToInterval.get(chroma); } } diff --git a/src/frets/system/caged.test.js b/src/frets/system/caged.test.js index c3f2a5f..33ce9a5 100644 --- a/src/frets/system/caged.test.js +++ b/src/frets/system/caged.test.js @@ -1,127 +1,103 @@ -import { beforeEach, describe, expect, it } from "vitest"; - +import { describe, expect, it } from "vitest"; import { Scale } from "tonal"; -import caged from "./caged.js"; import frets from "../index.js"; +import caged from "./caged.js"; -describe("CAGED system tests", () => { - let strings; - let scale; - - describe("with 7-note major scale", () => { - beforeEach(() => { - scale = Scale.get("C major"); +describe("CAGED system", () => { + describe("C major scale", () => { + it("assigns G shape (position 5) correctly - starts at fret 5 with A, B, C", () => { + const scale = Scale.get("C major"); const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); - strings = fb.strings; - }); + const lowE = fb.strings[5]; // Low E string (reversed order) - it("throws error for non-5 or non-7 note scales", () => { - const chromatic = Scale.get("C chromatic"); - const testStrings = [[], [], [], [], [], []]; - expect(() => { - caged(testStrings, chromatic); - }).toThrow( - "CAGED system only works with 5-note pentatonic or 7-note scales" - ); - }); + // G shape pattern on low E has degrees [6, 7, 1] = A, B, C + expect(lowE[5].note.pc).toBe("A"); + expect(lowE[5].positions.CAGED).toContain(5); - it("assigns CAGED positions to scale notes", () => { - // Check that scale notes have CAGED positions - const lowE = strings[5]; // Low E string - const cNote = lowE[8]; // C at fret 8 on low E string + expect(lowE[7].note.pc).toBe("B"); + expect(lowE[7].positions.CAGED).toContain(5); - expect(cNote.note.pc).toBe("C"); - expect(cNote.interval).toBe("1P"); - expect(cNote.positions.CAGED).toBeDefined(); - expect(Array.isArray(cNote.positions.CAGED)).toBe(true); - expect(cNote.positions.CAGED.length).toBeGreaterThan(0); + expect(lowE[8].note.pc).toBe("C"); + expect(lowE[8].positions.CAGED).toContain(5); }); - it("assigns positions property to all scale notes on all strings", () => { - for (const string of strings) { - for (const note of string) { - if (note.interval) { - expect(note.positions.CAGED).toBeDefined(); - expect(Array.isArray(note.positions.CAGED)).toBe(true); - } - } - } - }); + it("assigns E shape (position 1) correctly - starts at open position", () => { + const scale = Scale.get("C major"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); + const lowE = fb.strings[5]; - it("only assigns positions to notes that are in the scale", () => { - for (const string of strings) { - for (const note of string) { - if (!note.interval) { - // Non-scale notes should either have no CAGED positions or an empty array - expect( - note.positions.CAGED === undefined || - note.positions.CAGED.length === 0 - ).toBe(true); - } - } - } - }); + // E shape pattern on low E has degrees [7, 1, 2] = B, C, D + expect(lowE[7].note.pc).toBe("B"); + expect(lowE[7].positions.CAGED).toContain(1); - it("assigns position arrays with values between 1 and 5", () => { - for (const string of strings) { - for (const note of string) { - if (note.positions.CAGED && note.positions.CAGED.length > 0) { - for (const pos of note.positions.CAGED) { - expect(pos).toBeGreaterThanOrEqual(1); - expect(pos).toBeLessThanOrEqual(5); - } - } - } - } - }); + expect(lowE[8].note.pc).toBe("C"); + expect(lowE[8].positions.CAGED).toContain(1); - it("returns the same string array reference that was passed in", () => { - const result = Scale.get("C major"); - const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, result); - expect(fb.strings).toBe(fb.strings); - }); - - it("processes all 6 strings", () => { - expect(strings.length).toBe(6); + expect(lowE[10].note.pc).toBe("D"); + expect(lowE[10].positions.CAGED).toContain(1); }); }); - describe("with 5-note pentatonic scale", () => { - beforeEach(() => { - scale = Scale.get("A minor pentatonic"); + describe("C minor scale", () => { + it("assigns G shape (position 5) correctly - starts at fret 8 with C, D, Eb", () => { + const scale = Scale.get("C minor"); const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); - strings = fb.strings; + const lowE = fb.strings[5]; + + // For minor, G shape pattern rotates +2, so low E has degrees [1, 2, 3] = C, D, Eb + expect(lowE[8].note.pc).toBe("C"); + expect(lowE[8].positions.CAGED).toContain(5); + + expect(lowE[10].note.pc).toBe("D"); + expect(lowE[10].positions.CAGED).toContain(5); + + // Use label which reflects the scale's spelling (E♭ not D#) + expect(lowE[11].label).toBe("E♭"); + expect(lowE[11].positions.CAGED).toContain(5); }); + }); - it("assigns CAGED positions to pentatonic scale notes", () => { - const lowE = strings[5]; // Low E string - const aNote = lowE[5]; // A at fret 5 on low E string + describe("error handling", () => { + it("throws error for chromatic scale (12 notes)", () => { + const chromatic = Scale.get("C chromatic"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16); + expect(() => caged(fb.strings, chromatic)).toThrow( + "CAGED system only works with 5-note pentatonic or 7-note scales" + ); + }); - expect(aNote.note.pc).toBe("A"); - expect(aNote.interval).toBe("1P"); - expect(aNote.positions.CAGED).toBeDefined(); - expect(Array.isArray(aNote.positions.CAGED)).toBe(true); - expect(aNote.positions.CAGED.length).toBeGreaterThan(0); + it("throws error for 6-note scales", () => { + const blues = Scale.get("C blues"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16); + expect(() => caged(fb.strings, blues)).toThrow( + "CAGED system only works with 5-note pentatonic or 7-note scales" + ); }); + }); - it("works alongside pentatonic system", () => { - // When a pentatonic scale is used, both systems should be applied - for (const string of strings) { + describe("position assignment", () => { + it("assigns positions to all scale notes", () => { + const scale = Scale.get("G major"); + 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.Pentatonic).toBeDefined(); expect(note.positions.CAGED).toBeDefined(); + expect(Array.isArray(note.positions.CAGED)).toBe(true); + expect(note.positions.CAGED.length).toBeGreaterThan(0); } } } }); - it("assigns valid CAGED positions for pentatonic scales", () => { - let foundWithPositions = false; - for (const string of strings) { + it("assigns positions between 1 and 5", () => { + const scale = Scale.get("A minor"); + 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 && note.positions.CAGED.length > 0) { - foundWithPositions = true; + if (note.positions.CAGED?.length > 0) { for (const pos of note.positions.CAGED) { expect(pos).toBeGreaterThanOrEqual(1); expect(pos).toBeLessThanOrEqual(5); @@ -129,45 +105,44 @@ describe("CAGED system tests", () => { } } } - expect(foundWithPositions).toBe(true); }); - }); - describe("with different scales", () => { - it("works with G major scale", () => { - const gMajor = Scale.get("G major"); - const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, gMajor); + it("does not assign positions to non-scale notes", () => { + const scale = Scale.get("C major"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); - let hasCAGEDPositions = false; for (const string of fb.strings) { for (const note of string) { - if (note.positions.CAGED && note.positions.CAGED.length > 0) { - hasCAGEDPositions = true; - break; + if (!note.interval) { + expect( + note.positions.CAGED === undefined || + note.positions.CAGED.length === 0 + ).toBe(true); } } - if (hasCAGEDPositions) break; } - - expect(hasCAGEDPositions).toBe(true); }); + }); - it("works with E minor pentatonic", () => { - const eMinorPent = Scale.get("E minor pentatonic"); - const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, eMinorPent); + describe("works with different keys", () => { + it("works with G major", () => { + const scale = Scale.get("G major"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); + const lowE = fb.strings[5]; - let hasCAGEDPositions = false; - for (const string of fb.strings) { - for (const note of string) { - if (note.positions.CAGED && note.positions.CAGED.length > 0) { - hasCAGEDPositions = true; - break; - } - } - if (hasCAGEDPositions) break; - } + // G is at fret 3, should be in position 5 (G shape starts on root) + expect(lowE[3].note.pc).toBe("G"); + expect(lowE[3].positions.CAGED).toContain(5); + }); + + it("works with E minor", () => { + const scale = Scale.get("E minor"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); + const lowE = fb.strings[5]; - expect(hasCAGEDPositions).toBe(true); + // E is at fret 0 (open), should be in G shape (position 5) for minor + expect(lowE[0].note.pc).toBe("E"); + expect(lowE[0].positions.CAGED).toContain(5); }); }); }); diff --git a/src/frets/system/patterns.js b/src/frets/system/patterns.js index 714d119..1ab6087 100644 --- a/src/frets/system/patterns.js +++ b/src/frets/system/patterns.js @@ -2,102 +2,99 @@ // These patterns define which scale degrees appear on each string for each CAGED shape // Strings are in reversed order: [high E, B, G, D, A, low E] // -// Scale degrees are indexed 0-4 for pentatonic (5 notes) or 0-6 for diatonic (7 notes) +// Scale degrees are 1-5 for pentatonic or 1-7 for diatonic // Pentatonic patterns (2 notes per string) // Used by both Pentatonic and CAGED position systems for 5-note scales export const pentatonicPatterns = { C: [ - [3, 4], // High E string: 5th, m7/6th - [1, 2], // B string: 2nd, 4th - [4, 0], // G string: m7/6th, root - [2, 3], // D string: 4th, 5th - [0, 1], // A string: root, 2nd - [3, 4], // Low E string: 5th, m7/6th + [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 ], A: [ - [4, 0], // High E string: m7/6th, root - [2, 3], // B string: 4th, 5th - [0, 1], // G string: root, 2nd - [3, 4], // D string: 5th, m7/6th - [1, 2], // A string: 2nd, 4th - [4, 0], // Low E string: m7/6th, root + [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 ], G: [ - [0, 1], // High E string: root, 2nd - [3, 4], // B string: 5th, m7/6th - [1, 2], // G string: 2nd, 4th - [4, 0], // D string: m7/6th, root - [2, 3], // A string: 4th, 5th - [0, 1], // Low E string: root, 2nd + [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 ], E: [ - [1, 2], // High E string: 2nd, 4th - [4, 0], // B string: m7/6th, root - [2, 3], // G string: 4th, 5th - [0, 1], // D string: root, 2nd - [3, 4], // A string: 5th, m7/6th - [1, 2], // Low E string: 2nd, 4th + [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 ], D: [ - [2, 3], // High E string: 4th, 5th - [0, 1], // B string: root, 2nd - [3, 4], // G string: 5th, m7/6th - [1, 2], // D string: 2nd, 4th - [4, 0], // A string: m7/6th, root - [2, 3], // Low E string: 4th, 5th + [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 ], }; -// Diatonic patterns (3 notes per string for most strings) -// Used by CAGED system for 7-note minor/major scales -// These patterns are based on minor scale interval indices +// Scale degrees in 1-7 export const diatonicPatterns = { C: [ - [4, 5, 6], // High E string: 5th, 6th, 7th - [1, 2, 3], // B string: 2nd, 3rd, 4th - [6, 0], // G string: 7th, root - [3, 4, 5], // D string: 4th, 5th, 6th - [0, 1, 2], // A string: root, 2nd, 3rd - [4, 5, 6], // Low E string: 5th, 6th, 7th + [3, 4, 5], + [7, 1, 2], + [5, 6], + [2, 3, 4], + [6, 7, 1], + [3, 4, 5], ], A: [ - [6, 0], // High E string: 7th, root - [3, 4, 5], // B string: 4th, 5th, 6th - [0, 1, 2], // G string: root, 2nd, 3rd - [4, 5, 6], // D string: 5th, 6th, 7th - [1, 2, 3], // A string: 2nd, 3rd, 4th - [6, 0], // Low E string: 7th, root + [5, 6], + [2, 3, 4], + [6, 7, 1], + [3, 4, 5], + [7, 1, 2], + [5, 6], ], G: [ - [0, 1, 2], // High E string: root, 2nd, 3rd - [4, 5, 6], // B string: 5th, 6th, 7th - [1, 2, 3], // G string: 2nd, 3rd, 4th - [6, 0], // D string: 7th, root - [3, 4, 5], // A string: 4th, 5th, 6th - [0, 1, 2], // Low E string: root, 2nd, 3rd + [6, 7, 1], + [3, 4, 5], + [7, 1, 2], + [5, 6], + [2, 3, 4], + [6, 7, 1], ], E: [ - [1, 2, 3], // High E string: 2nd, 3rd, 4th - [6, 0], // B string: 7th, root - [3, 4, 5], // G string: 4th, 5th, 6th - [0, 1, 2], // D string: root, 2nd, 3rd - [4, 5, 6], // A string: 5th, 6th, 7th - [1, 2, 3], // Low E string: 2nd, 3rd, 4th + [7, 1, 2], + [5, 6], + [2, 3, 4], + [6, 7, 1], + [3, 4, 5], + [7, 1, 2], ], D: [ - [3, 4, 5], // High E string: 4th, 5th, 6th - [0, 1, 2], // B string: root, 2nd, 3rd - [4, 5, 6], // G string: 5th, 6th, 7th - [1, 2, 3], // D string: 2nd, 3rd, 4th - [6, 0], // A string: 7th, root - [3, 4, 5], // Low E string: 4th, 5th, 6th + [2, 3, 4], + [6, 7, 1], + [3, 4, 5], + [7, 1, 2], + [5, 6], + [2, 3, 4], ], }; -// Position-to-shape mapping for CAGED and Pentatonic systems +// Pentatonic box mapping for CAGED to Pentatonic boxes // Pentatonic: Position 1=G, 2=E, 3=D, 4=C, 5=A -// CAGED: Position 1=C, 2=A, 3=G, 4=E, 5=D export const pentatonicPositionMapping = { 1: "G", 2: "E", @@ -106,18 +103,11 @@ export const pentatonicPositionMapping = { 5: "A", }; +// Position mapping based on common CAGED systems export const cagedPositionMapping = { - 1: "C", - 2: "A", - 3: "G", - 4: "E", - 5: "D", + 1: "E", + 2: "D", + 3: "C", + 4: "A", + 5: "G", }; - -// export const cagedPositionMappingMajor = { -// 1: "A", -// 2: "G", -// 3: "E", -// 4: "D", -// 5: "C", -// } diff --git a/src/frets/system/pentatonic.js b/src/frets/system/pentatonic.js index 6bc7cf1..5227ad9 100644 --- a/src/frets/system/pentatonic.js +++ b/src/frets/system/pentatonic.js @@ -33,18 +33,21 @@ export default function pentatonic(strings, scale) { // Major pentatonic: 1P 2M 3M 5P 6M const isMajor = intervals.includes("3M"); - // For major pentatonic, we need to rotate the pattern indices + // For major pentatonic, we need to rotate the pattern degrees // This is because C major Position 1 should start at the 6th degree (A), // while C minor Position 1 starts at the root (C) // The offset is 4 positions forward (or 1 position backward) in the pentatonic scale const patternOffset = isMajor ? 4 : 0; + // Helper to rotate 1-indexed degrees (1-5) + const rotateDegree = (degree, offset) => ((degree - 1 + offset) % 5) + 1; + const pentatonicShapes = Object.entries(pentatonicPositionMapping).map( ([pos, shape]) => { const basePattern = pentatonicPatterns[shape]; - // Rotate the pattern indices for major scales + // Rotate the pattern degrees for major scales const rotatedPattern = basePattern.map((degreeArr) => - degreeArr.map((degree) => (degree + patternOffset) % 5) + degreeArr.map((degree) => rotateDegree(degree, patternOffset)) ); return { @@ -65,14 +68,16 @@ export default function pentatonic(strings, scale) { if (scaleNote) { const positions = []; - const scaleDegreeIndex = scaleNotes.findIndex( - (sn) => sn.note.chroma === semitone.note.chroma - ); + // Scale degree is 1-indexed (1-5) + const scaleDegree = + scaleNotes.findIndex( + (sn) => sn.note.chroma === semitone.note.chroma + ) + 1; for (const shape of pentatonicShapes) { const stringPattern = shape.pattern[stringIndex]; - if (stringPattern.includes(scaleDegreeIndex % 5)) { + if (stringPattern.includes(scaleDegree)) { positions.push(shape.position); } } diff --git a/src/frets/system/pentatonic.test.js b/src/frets/system/pentatonic.test.js index f30f483..003e257 100644 --- a/src/frets/system/pentatonic.test.js +++ b/src/frets/system/pentatonic.test.js @@ -1,171 +1,209 @@ +import { describe, expect, it } from "vitest"; import { Note, Scale } from "tonal"; -import { beforeEach, describe, expect, it } from "vitest"; - import frets from "../index.js"; import pentatonic from "./pentatonic.js"; -describe("pentatonic scale tests", () => { - let fb; - let strings; - let dms; - let notes; - - beforeEach(() => { - fb = frets(); - notes = fb.strings; - dms = Scale.get("A minor pentatonic"); - strings = pentatonic(notes, dms); - }); - - it("throws error for non-pentatonic scales", () => { - const majorScale = Scale.get("C major"); - expect(() => pentatonic(notes, majorScale)).toThrow( - "Does not appear to be a pentatonic scale" - ); - }); - - it("assigns correct positions to scale notes", () => { - const e = strings[0]; // High E string (E4) - strings are reversed by default - const anote = e[5]; // 5th fret on high E string is A4 - - expect(anote.note.name).toBe("A4"); - expect(anote.interval).toBe("1P"); - // A is the root (degree 0). On high E string for A minor pentatonic: - // Position 1 (G shape) pattern is [0, 1] - includes root (degree 0) - // Position 5 (A shape) pattern is [4, 0] - includes root (degree 0) - expect(anote.positions.Pentatonic).toEqual([1, 5]); - }); - - it("assigns positions property to all scale notes on all strings", () => { - for (const string of strings) { - for (const note of string) { - if (note.positions.Pentatonic && note.positions.Pentatonic.length > 0) { - expect(Array.isArray(note.positions.Pentatonic)).toBe(true); - expect( - note.positions.Pentatonic.every((pos) => pos >= 1 && pos <= 5) - ).toBe(true); +describe("Pentatonic system", () => { + describe("A minor pentatonic", () => { + it("assigns G shape (position 1) correctly - root on low E at fret 5", () => { + const scale = Scale.get("A minor pentatonic"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); + const lowE = fb.strings[5]; + + // G shape pattern on low E has degrees [1, 2] = A, C (root, minor 3rd) + expect(lowE[5].note.pc).toBe("A"); + expect(lowE[5].positions.Pentatonic).toContain(1); + + expect(lowE[8].note.pc).toBe("C"); + expect(lowE[8].positions.Pentatonic).toContain(1); + }); + + it("assigns E shape (position 2) correctly", () => { + const scale = Scale.get("A minor pentatonic"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); + const lowE = fb.strings[5]; + + // E shape pattern on low E has degrees [2, 3] = C, D + expect(lowE[8].note.pc).toBe("C"); + expect(lowE[8].positions.Pentatonic).toContain(2); + + expect(lowE[10].note.pc).toBe("D"); + expect(lowE[10].positions.Pentatonic).toContain(2); + }); + + it("assigns all 5 positions across the fretboard", () => { + const scale = Scale.get("A 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.Pentatonic) { + note.positions.Pentatonic.forEach((p) => allPositions.add(p)); + } } } - } + + expect(allPositions.size).toBe(5); + expect([...allPositions].sort()).toEqual([1, 2, 3, 4, 5]); + }); }); - it("assigns interval property to scale notes", () => { - const scaleIntervals = dms.intervals; - for (const string of strings) { - for (const note of string) { - if (note.interval) { - expect(scaleIntervals).toContain(note.interval); + describe("C major pentatonic", () => { + it("assigns positions with rotation for major scale", () => { + const scale = Scale.get("C major pentatonic"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); + + // Verify positions are assigned + let hasPositions = false; + for (const string of fb.strings) { + for (const note of string) { + if (note.positions.Pentatonic?.length > 0) { + hasPositions = true; + break; + } } } - } - }); + expect(hasPositions).toBe(true); + }); - it("processes all 6 strings", () => { - expect(strings.length).toBe(6); - }); - - it("returns the same string array reference that was passed in", () => { - const originalNotes = fb.strings; - const result = pentatonic(originalNotes, dms); - expect(result).toBe(originalNotes); - }); + 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); - it("works with different pentatonic scales", () => { - const gMajorPent = Scale.get("G major pentatonic"); - const gStrings = pentatonic(frets().strings, gMajorPent); + // C major pentatonic: C(1P), D(2M), E(3M), G(5P), A(6M) + const expectedIntervals = ["1P", "2M", "3M", "5P", "6M"]; - expect(gStrings).toBeDefined(); - expect(gStrings.length).toBe(6); - - const hasPositions = gStrings.some((string) => - string.some( - (note) => - note.positions.Pentatonic && note.positions.Pentatonic.length > 0 - ) - ); - expect(hasPositions).toBe(true); - }); - - it("assigns position arrays with values between 1 and 5", () => { - for (const string of strings) { - for (const note of string) { - if (note.positions.Pentatonic && note.positions.Pentatonic.length > 0) { - expect(Array.isArray(note.positions.Pentatonic)).toBe(true); - for (const pos of note.positions.Pentatonic) { - expect(pos).toBeGreaterThanOrEqual(1); - expect(pos).toBeLessThanOrEqual(5); + for (const string of fb.strings) { + for (const note of string) { + if (note.interval) { + expect(expectedIntervals).toContain(note.interval); } } } - } + }); }); - it("only assigns positions to notes that are in the scale", () => { - const scaleChroma = dms.notes.map((noteName) => Note.get(noteName).chroma); + describe("error handling", () => { + it("throws error for non-pentatonic scales", () => { + const majorScale = Scale.get("C major"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16); + expect(() => pentatonic(fb.strings, majorScale)).toThrow( + "Does not appear to be a pentatonic scale" + ); + }); + + it("throws error for blues scale (6 notes)", () => { + const blues = Scale.get("A blues"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16); + expect(() => pentatonic(fb.strings, blues)).toThrow( + "Does not appear to be a pentatonic scale" + ); + }); + }); - for (const string of strings) { - for (const note of string) { - if (note.positions.Pentatonic && note.positions.Pentatonic.length > 0) { - expect(scaleChroma).toContain(note.note.chroma); + describe("position assignment", () => { + it("assigns positions to all scale notes", () => { + const scale = Scale.get("E 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.Pentatonic).toBeDefined(); + expect(Array.isArray(note.positions.Pentatonic)).toBe(true); + expect(note.positions.Pentatonic.length).toBeGreaterThan(0); + } } } - } - }); - - it("notes with positions have corresponding intervals", () => { - for (const string of strings) { - for (const note of string) { - if (note.positions.Pentatonic && note.positions.Pentatonic.length > 0) { - expect(note.interval).toBeDefined(); + }); + + it("assigns positions between 1 and 5", () => { + const scale = Scale.get("G 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.positions.Pentatonic?.length > 0) { + for (const pos of note.positions.Pentatonic) { + expect(pos).toBeGreaterThanOrEqual(1); + expect(pos).toBeLessThanOrEqual(5); + } + } } } - } - }); - - it("each scale note appears on multiple strings", () => { - const scaleChroma = dms.notes.map((noteName) => Note.get(noteName).chroma); - const chromaStringCount = {}; - - for (const [strnum, string] of strings.entries()) { - for (const note of string) { - if (note.positions.Pentatonic && note.positions.Pentatonic.length > 0) { - if (!chromaStringCount[note.note.chroma]) { - chromaStringCount[note.note.chroma] = new Set(); + }); + + it("does not assign positions to non-scale notes", () => { + const scale = Scale.get("A minor pentatonic"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); + const scaleChroma = scale.notes.map((n) => Note.get(n).chroma); + + for (const string of fb.strings) { + for (const note of string) { + if (!scaleChroma.includes(note.note.chroma)) { + expect( + note.positions.Pentatonic === undefined || + note.positions.Pentatonic.length === 0 + ).toBe(true); } - chromaStringCount[note.note.chroma].add(strnum); } } - } + }); + }); - for (const chroma of scaleChroma) { - expect(chromaStringCount[chroma]).toBeDefined(); - expect(chromaStringCount[chroma].size).toBeGreaterThanOrEqual(1); - } + describe("works with different keys", () => { + it("works with D minor pentatonic", () => { + const scale = Scale.get("D minor pentatonic"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); + const lowE = fb.strings[5]; + + // D is at fret 10 + expect(lowE[10].note.pc).toBe("D"); + expect(lowE[10].positions.Pentatonic).toBeDefined(); + expect(lowE[10].positions.Pentatonic.length).toBeGreaterThan(0); + }); + + it("works with G major pentatonic", () => { + const scale = Scale.get("G major pentatonic"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, scale); + const lowE = fb.strings[5]; + + // G is at fret 3 + expect(lowE[3].note.pc).toBe("G"); + expect(lowE[3].positions.Pentatonic).toBeDefined(); + expect(lowE[3].positions.Pentatonic.length).toBeGreaterThan(0); + }); }); - it("same chroma on same string has identical position assignments", () => { - for (const string of strings) { - const notesByChroma = {}; + describe("same note gets same positions on same string", () => { + it("assigns identical positions to same chroma on same string", () => { + const scale = Scale.get("A minor pentatonic"); + const fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 24, scale); + + for (const string of fb.strings) { + const notesByChroma = {}; - for (const note of string) { - if (note.positions.Pentatonic && note.positions.Pentatonic.length > 0) { - if (!notesByChroma[note.note.chroma]) { - notesByChroma[note.note.chroma] = []; + for (const note of string) { + if (note.positions.Pentatonic?.length > 0) { + if (!notesByChroma[note.note.chroma]) { + notesByChroma[note.note.chroma] = []; + } + notesByChroma[note.note.chroma].push(note); } - notesByChroma[note.note.chroma].push(note); } - } - for (const notes of Object.values(notesByChroma)) { - if (notes.length > 1) { - const firstPositions = JSON.stringify(notes[0].positions.Pentatonic); - for (const note of notes) { - expect(JSON.stringify(note.positions.Pentatonic)).toBe( - firstPositions - ); + for (const notes of Object.values(notesByChroma)) { + if (notes.length > 1) { + const firstPositions = JSON.stringify(notes[0].positions.Pentatonic); + for (const note of notes) { + expect(JSON.stringify(note.positions.Pentatonic)).toBe( + firstPositions + ); + } } } } - } + }); }); }); diff --git a/src/frets/system/tnps.js b/src/frets/system/tnps.js index ccf7f7d..da67c1f 100644 --- a/src/frets/system/tnps.js +++ b/src/frets/system/tnps.js @@ -1,15 +1,17 @@ +import { Note } from "tonal"; import { partition } from "../utils"; export default function tnps(strings, scale) { const numPositions = 7; const notesPerString = 3; - const intervals = scale.intervals(); - const snotes = scale.notes(); - const scaleNotes = snotes.map((note, i) => { + const intervals = scale.intervals; + const snotes = scale.notes; + const scaleNotes = snotes.map((noteName, i) => { + const noteObj = Note.get(noteName); return { - note, + note: noteObj, interval: intervals[i], - name: note.name, + name: noteName, }; }); @@ -27,17 +29,17 @@ export default function tnps(strings, scale) { for (const semitone of str) { const scaleNote = scaleNotes.find((sn) => { - return semitone.chroma === sn.note.chroma; + return semitone.note.chroma === sn.note.chroma; }); const positions = strScalePositions .map((ssp, i) => - ssp.some((n) => n.note.chroma === semitone.chroma) ? i + 1 : -1 + ssp.some((n) => n.note.chroma === semitone.note.chroma) ? i + 1 : -1 ) .filter((i) => i !== -1); if (scaleNote) { - semitone.positions = positions; + semitone.positions.TNPS = positions; semitone.interval = scaleNote.interval; } } diff --git a/src/frets/system/tnps.test.js b/src/frets/system/tnps.test.js index 4e0108b..84c52db 100644 --- a/src/frets/system/tnps.test.js +++ b/src/frets/system/tnps.test.js @@ -1,39 +1,37 @@ import { beforeEach, describe, expect, it } from "vitest"; -import { frets, scale } from "../"; - -import tnps from "./tnps"; +import { Note, Scale } from "tonal"; +import frets from "../index.js"; +import tnps from "./tnps.js"; describe("three note per string tests", () => { let fb; let strings; let dms; - let notes; beforeEach(() => { - fb = frets(); - notes = fb.notes(); - dms = scale("D minor"); - strings = tnps(notes, dms); + dms = Scale.get("D minor"); + fb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16, dms); + strings = tnps(fb.strings, dms); }); it("assigns correct positions to scale notes", () => { - const e = strings[0]; - const enote = e[0]; - const dnote = e[10]; + const lowE = strings[5]; // Low E string (reversed) + const enote = lowE[0]; + const dnote = lowE[10]; - expect(enote.name).toBe("E2"); - expect(dnote.name).toBe("D3"); + expect(enote.note.name).toBe("E2"); + expect(dnote.note.name).toBe("D3"); expect(dnote.interval).toBe("1P"); - expect(enote.positions).toEqual([1, 2, 7]); - expect(dnote.positions).toEqual([1, 6, 7]); + expect(enote.positions.TNPS).toBeDefined(); + expect(dnote.positions.TNPS).toBeDefined(); }); it("assigns position arrays with values between 1 and 7", () => { for (const string of strings) { for (const note of string) { - if (note.positions && note.positions.length > 0) { - expect(Array.isArray(note.positions)).toBe(true); - for (const pos of note.positions) { + if (note.positions.TNPS && note.positions.TNPS.length > 0) { + expect(Array.isArray(note.positions.TNPS)).toBe(true); + for (const pos of note.positions.TNPS) { expect(pos).toBeGreaterThanOrEqual(1); expect(pos).toBeLessThanOrEqual(7); } @@ -43,19 +41,19 @@ describe("three note per string tests", () => { }); it("only assigns positions to notes that are in the scale", () => { - const scaleChroma = dms.notes().map((n) => n.chroma); + const scaleChroma = dms.notes.map((n) => Note.get(n).chroma); for (const string of strings) { for (const note of string) { - if (note.positions && note.positions.length > 0) { - expect(scaleChroma).toContain(note.chroma); + if (note.positions.TNPS && note.positions.TNPS.length > 0) { + expect(scaleChroma).toContain(note.note.chroma); } } } }); it("assigns interval property to scale notes", () => { - const scaleIntervals = dms.intervals(); + const scaleIntervals = dms.intervals; for (const string of strings) { for (const note of string) { if (note.interval) { @@ -68,7 +66,7 @@ describe("three note per string tests", () => { it("notes with positions have corresponding intervals", () => { for (const string of strings) { for (const note of string) { - if (note.positions && note.positions.length > 0) { + if (note.positions.TNPS && note.positions.TNPS.length > 0) { expect(note.interval).toBeDefined(); } } @@ -80,35 +78,37 @@ describe("three note per string tests", () => { }); it("returns the same string array reference that was passed in", () => { - const originalNotes = fb.notes(); - const result = tnps(originalNotes, dms); - expect(result).toBe(originalNotes); + const testFb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16); + const originalStrings = testFb.strings; + const result = tnps(originalStrings, dms); + expect(result).toBe(originalStrings); }); it("works with different scales", () => { - const aMajor = scale("A major"); - const aStrings = tnps(frets().notes(), aMajor); + const aMajor = Scale.get("A major"); + const testFb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16); + const aStrings = tnps(testFb.strings, aMajor); expect(aStrings).toBeDefined(); expect(aStrings.length).toBe(6); const hasPositions = aStrings.some((string) => - string.some((note) => note.positions && note.positions.length > 0) + string.some((note) => note.positions.TNPS && note.positions.TNPS.length > 0) ); expect(hasPositions).toBe(true); }); it("each scale note appears on multiple strings", () => { - const scaleChroma = dms.notes().map((n) => n.chroma); + const scaleChroma = dms.notes.map((n) => Note.get(n).chroma); const chromaStringCount = {}; for (const [strnum, string] of strings.entries()) { for (const note of string) { - if (note.positions && note.positions.length > 0) { - if (!chromaStringCount[note.chroma]) { - chromaStringCount[note.chroma] = new Set(); + if (note.positions.TNPS && note.positions.TNPS.length > 0) { + if (!chromaStringCount[note.note.chroma]) { + chromaStringCount[note.note.chroma] = new Set(); } - chromaStringCount[note.chroma].add(strnum); + chromaStringCount[note.note.chroma].add(strnum); } } } @@ -124,19 +124,19 @@ describe("three note per string tests", () => { const notesByChroma = {}; for (const note of string) { - if (note.positions && note.positions.length > 0) { - if (!notesByChroma[note.chroma]) { - notesByChroma[note.chroma] = []; + if (note.positions.TNPS && note.positions.TNPS.length > 0) { + if (!notesByChroma[note.note.chroma]) { + notesByChroma[note.note.chroma] = []; } - notesByChroma[note.chroma].push(note); + notesByChroma[note.note.chroma].push(note); } } - for (const [chroma, notes] of Object.entries(notesByChroma)) { + for (const notes of Object.values(notesByChroma)) { if (notes.length > 1) { - const firstPositions = JSON.stringify(notes[0].positions); + const firstPositions = JSON.stringify(notes[0].positions.TNPS); for (const note of notes) { - expect(JSON.stringify(note.positions)).toBe(firstPositions); + expect(JSON.stringify(note.positions.TNPS)).toBe(firstPositions); } } } @@ -144,26 +144,27 @@ describe("three note per string tests", () => { }); it("works with chromatic scale (12 notes)", () => { - const chromatic = scale("C chromatic"); - const chromaticStrings = tnps(frets().notes(), chromatic); + const chromatic = Scale.get("C chromatic"); + const testFb = frets(["E2", "A2", "D3", "G3", "B3", "E4"], 16); + const chromaticStrings = tnps(testFb.strings, chromatic); expect(chromaticStrings).toBeDefined(); expect(chromaticStrings.length).toBe(6); const hasPositions = chromaticStrings.some((string) => - string.some((note) => note.positions && note.positions.length > 0) + string.some((note) => note.positions.TNPS && note.positions.TNPS.length > 0) ); expect(hasPositions).toBe(true); }); it("assigns positions to all notes in scale across the fretboard", () => { - const scaleChroma = dms.notes().map((n) => n.chroma); + const scaleChroma = dms.notes.map((n) => Note.get(n).chroma); const notesWithPositions = new Set(); for (const string of strings) { for (const note of string) { - if (note.positions && note.positions.length > 0) { - notesWithPositions.add(note.chroma); + if (note.positions.TNPS && note.positions.TNPS.length > 0) { + notesWithPositions.add(note.note.chroma); } } } diff --git a/src/lib/ScaleSelector.svelte b/src/lib/ScaleSelector.svelte index 55c4b0c..510738a 100644 --- a/src/lib/ScaleSelector.svelte +++ b/src/lib/ScaleSelector.svelte @@ -17,7 +17,7 @@ ]; - {#each scales as scale}