Add chord diagram generation for all keys and chord types
Features: - 12 keys (A through G#/Ab with enharmonic notation) - 8 chord types: Major, Minor, Minor7, 6th, 7th, Dim, Sus4, Aug - SVG chord diagrams with finger positions and degree labels - Toggle between numeric (1-2-3-4) and PIMA finger notation - Root notes highlighted with filled circles for print clarity - Barre chord detection and display - Works with all 24 tunings including 4-string banjo - Print-friendly layout (4 chords per row) https://claude.ai/code/session_017ri8N4m6j39ajt5xUaz65n
+1173 −2 · 2 files changed
-
CHORD_GENERATION_SPEC.md+355 −0 -
index.html+818 −2
@@ -0,0 +1,355 @@ +# Banjo Chord Generation - Project Specification + +## Project Overview + +This specification describes the addition of a **Chord Generation Module** to the existing Interactive Banjo Tunings Reference application. The feature will generate chord diagrams for all 12 keys and 8 chord types, displayed in standard chord grid format, integrated with the existing tuning system. + +--- + +## Current Architecture Summary + +**File Structure:** Single HTML file (`index.html`) with embedded CSS (~800 lines) and JavaScript (~970 lines) + +**Key Existing Components:** +- `CHROMATIC_SCALE`: ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] +- `getNoteAtFret(openNote, fret)`: Core function for note calculation +- `normalizeNote(note)`: Converts any notation to canonical form +- `displayNote(note)`: Applies display preferences (e.g., G# → Ab) +- 24 tuning definitions with full metadata +- SVG-based fretboard visualization +- Capo transposition support +- Solfège notation toggle + +--- + +## Requirements + +### 1. Keys (12 Total) + +| Key | Display Format | +|-----|---------------| +| A | A | +| A#/Bb | A#/Bb | +| B | B | +| C | C | +| C#/Db | C#/Db | +| D | D | +| D#/Eb | D#/Eb | +| E | E | +| F | F | +| F#/Gb | F#/Gb | +| G | G | +| G#/Ab | G#/Ab | + +**Display:** Use slash notation for enharmonic equivalents (A#/Bb) + +### 2. Chord Types (8 Per Key) + +| Type | Symbol | Intervals | Example (C) | +|------|--------|-----------|-------------| +| Major | (none) | 1, 3, 5 | C, E, G | +| Minor | m | 1, ♭3, 5 | C, E♭, G | +| Minor 7 | m7 | 1, ♭3, 5, ♭7 | C, E♭, G, B♭ | +| Sixth | 6 | 1, 3, 5, 6 | C, E, G, A | +| Seventh | 7 | 1, 3, 5, ♭7 | C, E, G, B♭ | +| Diminished | dim | 1, ♭3, ♭5 | C, E♭, G♭ | +| Suspended 4 | sus4 | 1, 4, 5 | C, F, G | +| Augmented | aug | 1, 3, #5 | C, E, G# | + +**Total:** 12 keys × 8 types = **96 chord diagrams per tuning** + +### 3. Chord Intervals (Semitones from Root) + +```javascript +CHORD_INTERVALS = { + 'major': [0, 4, 7], // 1, 3, 5 + 'minor': [0, 3, 7], // 1, ♭3, 5 + 'm7': [0, 3, 7, 10], // 1, ♭3, 5, ♭7 + '6': [0, 4, 7, 9], // 1, 3, 5, 6 + '7': [0, 4, 7, 10], // 1, 3, 5, ♭7 + 'dim': [0, 3, 6], // 1, ♭3, ♭5 + 'sus4': [0, 5, 7], // 1, 4, 5 + 'aug': [0, 4, 8] // 1, 3, #5 +} +``` + +### 4. Chord Diagram Display (Grid Format) + +#### Layout Structure +``` + ○ ← Open string indicator + ┌────┬────┬────┬────┬────┐ + │ │ │ │ │ │ ← Thick top line (nut) + ├────┼────┼────┼────┼────┤ + │ │ ① │ │ ② │ │ ← Fret 1 (circled = finger position) + ├────┼────┼────┼────┼────┤ + │ │ │ ③ │ │ │ ← Fret 2 + ├────┼────┼────┼────┼────┤ + │ │ │ │ │ │ ← Fret 3 + ├────┼────┼────┼────┼────┤ + │ │ │ │ │ │ ← Fret 4 + └────┴────┴────┴────┴────┘ + 5 4 3 2 1 ← String numbers (5th string on left) + G D G B D ← Open string notes + 5 1 5 3 1 ← Chord degree indicators +``` + +#### Visual Elements + +| Element | Symbol | Description | +|---------|--------|-------------| +| Open string | ○ | Circle above nut - string played open | +| Muted string | ✕ | X above nut - string not played | +| Finger position | ①②③④ | Circled number showing which finger | +| Barre | ⌒ | Arc connecting multiple positions on same fret | +| Nut | ━━━━ | Thick horizontal line at top | +| Fret lines | ──── | Thinner horizontal lines | +| Strings | │ | Vertical lines, left to right = low to high | + +#### Finger Notation (Toggleable) + +**Numbered (Default):** +- 1 = Index finger +- 2 = Middle finger +- 3 = Ring finger +- 4 = Pinky +- T = Thumb (rare) + +**PIMA (Classical Guitar):** +- P = Pulgar (Thumb) +- I = Índice (Index) +- M = Medio (Middle) +- A = Anular (Ring) + +### 5. Fret Range Display + +- **Default:** Show frets 1-5 (with nut) +- **Higher positions:** If chord requires frets 5+, show appropriate range with fret number indicator +- **Example:** If chord starts at fret 5, show "5fr" next to first fret line + +### 6. Note Highlighting + +For print-friendly display (black & white), use: +- **Root notes:** Solid filled circle (black fill) +- **Third/Fifth/etc:** Hollow circle with black outline +- **Optional:** Gray fill for secondary chord tones + +### 7. Chord Degree Labels + +Show scale degrees below each string: +- 1 = Root +- 3 = Third (or ♭3 for minor) +- 5 = Fifth (or ♭5 for dim, #5 for aug) +- 6 = Sixth +- 7 = Seventh (or ♭7) +- 4 = Fourth (for sus4) + +--- + +## Data Structure Design + +### Chord Definition + +```javascript +const CHORD_DATA = { + // Key: chord fingering data for a specific tuning + // This will be generated algorithmically based on tuning +}; + +// Chord type definitions +const CHORD_TYPES = { + 'major': { symbol: '', name: 'Major', intervals: [0, 4, 7] }, + 'minor': { symbol: 'm', name: 'Minor', intervals: [0, 3, 7] }, + 'm7': { symbol: 'm7', name: 'Minor 7', intervals: [0, 3, 7, 10] }, + '6': { symbol: '6', name: 'Sixth', intervals: [0, 4, 7, 9] }, + '7': { symbol: '7', name: 'Seventh', intervals: [0, 4, 7, 10] }, + 'dim': { symbol: 'dim', name: 'Diminished', intervals: [0, 3, 6] }, + 'sus4': { symbol: 'sus4', name: 'Suspended 4', intervals: [0, 5, 7] }, + 'aug': { symbol: 'aug', name: 'Augmented', intervals: [0, 4, 8] } +}; + +// Key definitions with enharmonic display +const KEYS = [ + { root: 'A', display: 'A' }, + { root: 'A#', display: 'A#/B♭' }, + { root: 'B', display: 'B' }, + { root: 'C', display: 'C' }, + { root: 'C#', display: 'C#/D♭' }, + { root: 'D', display: 'D' }, + { root: 'D#', display: 'D#/E♭' }, + { root: 'E', display: 'E' }, + { root: 'F', display: 'F' }, + { root: 'F#', display: 'F#/G♭' }, + { root: 'G', display: 'G' }, + { root: 'G#', display: 'G#/A♭' } +]; +``` + +### Chord Shape Structure + +```javascript +// A chord shape definition +{ + key: 'G', + type: 'major', + tuning: 'Open G', + positions: [ + { string: 5, fret: 0, finger: null, degree: 5 }, // Open G + { string: 4, fret: 0, finger: null, degree: 1 }, // Open D + { string: 3, fret: 0, finger: null, degree: 5 }, // Open G + { string: 2, fret: 0, finger: null, degree: 3 }, // Open B + { string: 1, fret: 0, finger: null, degree: 1 } // Open D + ], + barre: null, // or { fret: 2, fromString: 1, toString: 4 } + baseFret: 1, // First fret shown in diagram + muted: [] // Array of muted string numbers +} +``` + +--- + +## UI Design + +### New Tab: "Chords" + +Add as 4th tab alongside existing Tunings, Fretboards, Reference tabs. + +#### Controls +1. **Tuning Selector** - Dropdown to select tuning (inherit from existing) +2. **Key Selector** - Buttons or dropdown for 12 keys +3. **Chord Type Filter** - Buttons to filter by type (or show all) +4. **Finger Notation Toggle** - Switch between 1234 and PIMA +5. **View Mode** - Grid view (all chords) or Single chord view + +#### Layout Options +- **Grid View:** 4×2 grid showing all 8 chord types for selected key +- **Full View:** All 96 chords organized by key (for printing) + +### Print Layout + +Optimize for A4/Letter paper: +- 4 chord diagrams per row +- Key headers with separator lines +- Page breaks between keys (optional) + +--- + +## Implementation Phases + +### Phase 1: Core Music Theory +- [ ] Add CHORD_TYPES constant with intervals +- [ ] Add KEYS constant +- [ ] Create `getChordNotes(root, type)` function +- [ ] Create `findChordPositions(tuning, chordNotes)` algorithm + +### Phase 2: Chord Diagram SVG Generator +- [ ] Create `generateChordDiagramSVG(chordShape)` function +- [ ] Implement finger position circles +- [ ] Implement open/muted string indicators +- [ ] Implement barre chord arcs +- [ ] Implement degree labels +- [ ] Add root note highlighting + +### Phase 3: UI Integration +- [ ] Add Chords tab to navigation +- [ ] Create chord grid container +- [ ] Add key selector +- [ ] Add chord type filter +- [ ] Add finger notation toggle (1234/PIMA) +- [ ] Integrate with existing tuning selector + +### Phase 4: Chord Calculation Algorithm +- [ ] Implement voicing algorithm for each tuning +- [ ] Handle 5-string banjo specifics (short 5th string) +- [ ] Generate optimal fingerings +- [ ] Support alternative voicings + +### Phase 5: Polish & Print +- [ ] Add print-specific styles +- [ ] Optimize diagram sizing +- [ ] Add batch export option +- [ ] Test across all tunings + +--- + +## Technical Considerations + +### 5th String Handling +The 5th string on a 5-string banjo starts at fret 5, not the nut. Chord diagrams must account for this: +- If chord uses frets 1-4, 5th string cannot be fretted (only played open at its pitch) +- Show 5th string position correctly based on `shortStringStartFret` from tuning data + +### Chord Voicing Algorithm +Finding optimal chord voicings is complex. Considerations: +1. Playability (finger stretch limits) +2. Voice leading (smooth transitions) +3. Open strings where beneficial +4. Avoiding string muting conflicts + +**Approach:** Start with a database of common chord shapes, then algorithmically transpose based on tuning. + +### Capo Support +Extend existing capo logic: +- With capo at fret 2, G tuning becomes A tuning +- Chord shapes remain same, key shifts up +- Display effective key with capo position + +--- + +## File Organization + +**Option A: Single File (Recommended)** +Keep everything in `index.html` for maximum portability: +- Add chord CSS to `<style>` section (~150 lines) +- Add chord JS to `<script>` section (~400 lines) + +**Option B: Split Files** +If size becomes unwieldy: +``` +/index.html - Main app +/chords.js - Chord data and logic +/chords.css - Chord diagram styles (optional) +``` +Note: Split requires serving from HTTP server for local development. + +--- + +## Appendix: Common Open G Chord Voicings + +Example chord data for Open G tuning (gDGBD): + +```javascript +// These are reference voicings to be expanded algorithmically +const OPEN_G_CHORDS = { + 'G_major': { + positions: [0, 0, 0, 0, 0], // All open + fingers: [null, null, null, null, null], + degrees: ['5', '1', '5', '3', '1'] + }, + 'C_major': { + positions: [null, 2, 1, 0, 2], // x-2-1-0-2 + fingers: [null, 2, 1, null, 3], + degrees: ['x', '5', '1', '3', '5'] + }, + 'D_major': { + positions: [0, 0, 2, 3, 2], // 0-0-2-3-2 + fingers: [null, null, 1, 3, 2], + degrees: ['3', '1', '5', '1', '5'] + } + // ... etc +}; +``` + +--- + +## Next Steps + +1. Review this specification +2. Confirm UI/UX preferences +3. Begin Phase 1 implementation +4. Iterate based on feedback + +--- + +*Document created: 2026-02-01* +*For: Interactive Banjo Tunings Reference v1.0* @@ -793,6 +793,167 @@ font-size: 22px; } } + + /* ============================================ + CHORD DIAGRAM STYLES + ============================================ */ + .chord-controls { + display: flex; + flex-wrap: wrap; + gap: 15px; + padding: 15px; + background: white; + border: 2px solid var(--border-color); + margin-bottom: 20px; + align-items: flex-end; + } + + .chord-control-group { + display: flex; + flex-direction: column; + gap: 5px; + } + + .chord-control-group label { + font-size: 12px; + font-weight: bold; + color: var(--text-secondary); + } + + .key-buttons { + display: flex; + flex-wrap: wrap; + gap: 5px; + } + + .key-btn { + padding: 8px 12px; + border: 2px solid var(--border-color); + background: white; + cursor: pointer; + font-size: 13px; + font-weight: bold; + border-radius: 4px; + transition: all 0.2s; + min-width: 45px; + text-align: center; + } + + .key-btn:hover { + background: #f0f0f0; + } + + .key-btn.active { + background: var(--bg-dark); + color: white; + border-color: var(--bg-dark); + } + + .chord-type-buttons { + display: flex; + flex-wrap: wrap; + gap: 5px; + } + + .chord-type-btn { + padding: 6px 10px; + border: 2px solid var(--border-color); + background: white; + cursor: pointer; + font-size: 12px; + border-radius: 4px; + transition: all 0.2s; + } + + .chord-type-btn:hover { + background: #f0f0f0; + } + + .chord-type-btn.active { + background: var(--g-family); + color: white; + border-color: var(--g-family); + } + + .chord-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 20px; + padding: 10px; + } + + .chord-diagram-container { + background: white; + border: 2px solid var(--border-color); + border-radius: 8px; + padding: 15px; + text-align: center; + } + + .chord-diagram-title { + font-size: 18px; + font-weight: bold; + margin-bottom: 10px; + color: var(--text-primary); + } + + .chord-diagram-subtitle { + font-size: 12px; + color: var(--text-muted); + margin-bottom: 10px; + } + + .chord-notes-display { + font-size: 11px; + color: var(--text-secondary); + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-color); + } + + .finger-toggle { + display: flex; + gap: 10px; + align-items: center; + } + + .finger-toggle label { + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; + font-size: 13px; + } + + .chord-info-box { + background: #f9f9f9; + border: 1px solid var(--border-color); + padding: 10px 15px; + margin-bottom: 20px; + font-size: 13px; + } + + .no-chord-message { + padding: 40px; + text-align: center; + color: var(--text-muted); + font-style: italic; + } + + @media print { + .chord-controls { + display: none; + } + + .chord-grid { + grid-template-columns: repeat(4, 1fr); + } + + .chord-diagram-container { + break-inside: avoid; + page-break-inside: avoid; + } + } </style> </head> <body> @@ -856,6 +1017,7 @@ <div class="tabs no-print"> <button class="tab active" data-tab="tunings">Tunings</button> <button class="tab" data-tab="fretboards">Fretboards</button> + <button class="tab" data-tab="chords">Chords</button> <button class="tab" data-tab="reference">Reference</button> </div> @@ -906,6 +1068,57 @@ </div> </div> + <!-- Chords Tab --> + <div id="chords-tab" class="tab-content"> + <div class="chord-info-box no-print"> + <strong>CHORD DIAGRAMS:</strong> + Vertical lines = strings (5th on left to 1st on right) | Horizontal lines = frets<br> + ○ = Open string | ✕ = Muted string | Circled numbers = finger positions<br> + Thick top line = nut | Numbers below strings show chord degrees (1=root, 3=third, 5=fifth)<br> + Root notes shown with filled circles for easy identification + </div> + + <div class="chord-controls no-print"> + <div class="chord-control-group"> + <label>Tuning</label> + <select id="chordTuningSelect" style="padding: 8px; font-size: 14px; min-width: 150px;"> + </select> + </div> + + <div class="chord-control-group"> + <label>Key</label> + <div class="key-buttons" id="keyButtons"> + <!-- Keys populated by JS --> + </div> + </div> + + <div class="chord-control-group"> + <label>Chord Type</label> + <div class="chord-type-buttons" id="chordTypeButtons"> + <!-- Chord types populated by JS --> + </div> + </div> + + <div class="chord-control-group"> + <label>Finger Notation</label> + <div class="finger-toggle"> + <label> + <input type="radio" name="fingerNotation" value="numbers" checked> + 1-2-3-4 + </label> + <label> + <input type="radio" name="fingerNotation" value="pima"> + PIMA + </label> + </div> + </div> + </div> + + <div id="chordContainer" class="chord-grid"> + <!-- Chord diagrams rendered here --> + </div> + </div> + <!-- Reference Tab --> <div id="reference-tab" class="tab-content"> <div class="reference-section"> @@ -1098,6 +1311,423 @@ return CHROMATIC_SCALE[newIndex]; } + // ============================================ + // CHORD DATA & FUNCTIONS + // ============================================ + + // 12 keys with display names (using slash for enharmonics) + const CHORD_KEYS = [ + { root: 'C', display: 'C' }, + { root: 'C#', display: 'C#/Db' }, + { root: 'D', display: 'D' }, + { root: 'D#', display: 'D#/Eb' }, + { root: 'E', display: 'E' }, + { root: 'F', display: 'F' }, + { root: 'F#', display: 'F#/Gb' }, + { root: 'G', display: 'G' }, + { root: 'G#', display: 'G#/Ab' }, + { root: 'A', display: 'A' }, + { root: 'A#', display: 'A#/Bb' }, + { root: 'B', display: 'B' } + ]; + + // 8 chord types with intervals (semitones from root) + const CHORD_TYPES = { + 'major': { symbol: '', name: 'Major', intervals: [0, 4, 7], degrees: ['1', '3', '5'] }, + 'minor': { symbol: 'm', name: 'Minor', intervals: [0, 3, 7], degrees: ['1', '♭3', '5'] }, + 'm7': { symbol: 'm7', name: 'Minor 7', intervals: [0, 3, 7, 10], degrees: ['1', '♭3', '5', '♭7'] }, + '6': { symbol: '6', name: 'Sixth', intervals: [0, 4, 7, 9], degrees: ['1', '3', '5', '6'] }, + '7': { symbol: '7', name: 'Seventh', intervals: [0, 4, 7, 10], degrees: ['1', '3', '5', '♭7'] }, + 'dim': { symbol: 'dim', name: 'Diminished', intervals: [0, 3, 6], degrees: ['1', '♭3', '♭5'] }, + 'sus4': { symbol: 'sus4', name: 'Sus4', intervals: [0, 5, 7], degrees: ['1', '4', '5'] }, + 'aug': { symbol: 'aug', name: 'Augmented', intervals: [0, 4, 8], degrees: ['1', '3', '#5'] } + }; + + // Finger notation mapping + const FINGER_NOTATION = { + numbers: { 1: '1', 2: '2', 3: '3', 4: '4', T: 'T' }, + pima: { 1: 'I', 2: 'M', 3: 'A', 4: 'P', T: 'P' } + }; + + // Get all notes in a chord + function getChordNotes(rootNote, chordType) { + const type = CHORD_TYPES[chordType]; + if (!type) return []; + + const rootIndex = CHROMATIC_SCALE.indexOf(normalizeNote(rootNote)); + if (rootIndex === -1) return []; + + return type.intervals.map(interval => { + return CHROMATIC_SCALE[(rootIndex + interval) % 12]; + }); + } + + // Check if a note is in a chord + function isNoteInChord(note, chordNotes) { + const normalizedNote = normalizeNote(note); + return chordNotes.includes(normalizedNote); + } + + // Get the degree of a note in a chord + function getNoteDegree(note, rootNote, chordType) { + const type = CHORD_TYPES[chordType]; + if (!type) return null; + + const normalizedNote = normalizeNote(note); + const rootIndex = CHROMATIC_SCALE.indexOf(normalizeNote(rootNote)); + const noteIndex = CHROMATIC_SCALE.indexOf(normalizedNote); + + if (rootIndex === -1 || noteIndex === -1) return null; + + const interval = (noteIndex - rootIndex + 12) % 12; + const degreeIndex = type.intervals.indexOf(interval); + + if (degreeIndex === -1) return null; + return type.degrees[degreeIndex]; + } + + // Find chord voicing for a given tuning + function findChordVoicing(tuningData, rootNote, chordType, maxFret = 5) { + const chordNotes = getChordNotes(rootNote, chordType); + if (chordNotes.length === 0) return null; + + const strings = tuningData.strings; + const numStrings = strings.length; + const shortStringIdx = tuningData.shortStringIndex; + const shortStringStart = tuningData.shortStringStartFret || 6; + + // Find playable positions for each string + const stringOptions = strings.map((openNote, stringIdx) => { + const options = []; + const normalizedOpen = normalizeNote(openNote); + + // Check open string + if (isNoteInChord(normalizedOpen, chordNotes)) { + // For short string (5th string), it can only be played open if chord position allows + if (stringIdx !== shortStringIdx || shortStringIdx === null) { + options.push({ fret: 0, note: normalizedOpen }); + } + } + + // Check fretted positions + for (let fret = 1; fret <= maxFret; fret++) { + // Skip frets before short string starts + if (stringIdx === shortStringIdx && fret < shortStringStart) { + continue; + } + + const frettedNote = getNoteAtFret(openNote, fret); + if (isNoteInChord(frettedNote, chordNotes)) { + options.push({ fret, note: frettedNote }); + } + } + + return options; + }); + + // Find the best voicing using a simple algorithm + // Prioritize: 1) Lower frets, 2) More open strings, 3) Smaller stretch + const voicing = findBestVoicing(stringOptions, chordNotes, rootNote, numStrings, shortStringIdx); + + if (!voicing) return null; + + // Assign fingers based on fret positions + const positions = voicing.map((pos, idx) => { + if (pos === null) { + return { string: numStrings - idx, fret: null, muted: true, note: null, degree: null }; + } + + const degree = getNoteDegree(pos.note, rootNote, chordType); + const isRoot = normalizeNote(pos.note) === normalizeNote(rootNote); + + return { + string: numStrings - idx, + fret: pos.fret, + muted: false, + note: pos.note, + degree: degree, + isRoot: isRoot + }; + }); + + // Assign finger numbers + assignFingers(positions); + + // Determine base fret for display + const frettedPositions = positions.filter(p => p.fret > 0); + const minFret = frettedPositions.length > 0 ? Math.min(...frettedPositions.map(p => p.fret)) : 1; + const maxFretUsed = frettedPositions.length > 0 ? Math.max(...frettedPositions.map(p => p.fret)) : 1; + const baseFret = minFret > 4 ? minFret : 1; + + // Check for barre chord + const barre = detectBarre(positions); + + return { + positions, + baseFret, + barre, + chordNotes: chordNotes.map(n => displayNote(n)) + }; + } + + function findBestVoicing(stringOptions, chordNotes, rootNote, numStrings, shortStringIdx) { + // Simple greedy algorithm to find a playable voicing + // Try to include all chord notes while minimizing stretch + + const voicing = new Array(numStrings).fill(null); + const usedNotes = new Set(); + + // First pass: find positions for each string preferring open strings + for (let i = 0; i < numStrings; i++) { + const options = stringOptions[i]; + if (options.length === 0) { + voicing[i] = null; // Will be muted + continue; + } + + // Prefer open strings, then lower frets + const sorted = [...options].sort((a, b) => a.fret - b.fret); + + // Prefer root note if available + const rootOption = sorted.find(opt => normalizeNote(opt.note) === normalizeNote(rootNote)); + if (rootOption && !usedNotes.has(rootOption.note + '-' + i)) { + voicing[i] = rootOption; + usedNotes.add(rootOption.note); + } else { + voicing[i] = sorted[0]; + usedNotes.add(sorted[0].note); + } + } + + // Validate: check if all chord notes are present + const presentNotes = new Set(voicing.filter(v => v !== null).map(v => v.note)); + const allNotesPresent = chordNotes.every(note => presentNotes.has(note)); + + // Check stretch (max fret distance between fingers) + const frettedFrets = voicing.filter(v => v !== null && v.fret > 0).map(v => v.fret); + const stretch = frettedFrets.length > 1 ? Math.max(...frettedFrets) - Math.min(...frettedFrets) : 0; + + // If stretch is too large (> 4 frets) or not all notes present, try alternative + if (stretch > 4 || !allNotesPresent) { + // Try to find a better voicing with barres or different positions + return findAlternativeVoicing(stringOptions, chordNotes, rootNote, numStrings); + } + + return voicing; + } + + function findAlternativeVoicing(stringOptions, chordNotes, rootNote, numStrings) { + // Try different starting frets for barre chords + for (let startFret = 1; startFret <= 7; startFret++) { + const voicing = new Array(numStrings).fill(null); + + for (let i = 0; i < numStrings; i++) { + const options = stringOptions[i].filter(opt => + opt.fret === 0 || (opt.fret >= startFret && opt.fret <= startFret + 4) + ); + + if (options.length > 0) { + // Prefer options closer to startFret + const sorted = [...options].sort((a, b) => { + if (a.fret === 0 && b.fret === 0) return 0; + if (a.fret === 0) return 1; // Open strings less preferred in barre position + if (b.fret === 0) return -1; + return Math.abs(a.fret - startFret) - Math.abs(b.fret - startFret); + }); + voicing[i] = sorted[0]; + } + } + + // Validate + const presentNotes = new Set(voicing.filter(v => v !== null).map(v => v.note)); + const hasRoot = voicing.some(v => v !== null && normalizeNote(v.note) === normalizeNote(rootNote)); + const hasEnoughNotes = presentNotes.size >= Math.min(3, chordNotes.length); + + if (hasRoot && hasEnoughNotes) { + return voicing; + } + } + + // Fallback: return whatever we can find + const voicing = new Array(numStrings).fill(null); + for (let i = 0; i < numStrings; i++) { + if (stringOptions[i].length > 0) { + voicing[i] = stringOptions[i][0]; + } + } + return voicing; + } + + function assignFingers(positions) { + // Assign finger numbers based on fret positions + const fretted = positions.filter(p => p.fret > 0).sort((a, b) => a.fret - b.fret); + + if (fretted.length === 0) return; + + // Group by fret (for barres) + const fretGroups = {}; + fretted.forEach(p => { + if (!fretGroups[p.fret]) fretGroups[p.fret] = []; + fretGroups[p.fret].push(p); + }); + + let fingerNum = 1; + const sortedFrets = Object.keys(fretGroups).map(Number).sort((a, b) => a - b); + + sortedFrets.forEach(fret => { + const group = fretGroups[fret]; + if (group.length > 1) { + // Potential barre - use same finger for all + group.forEach(p => p.finger = fingerNum); + } else { + group[0].finger = fingerNum; + } + fingerNum++; + if (fingerNum > 4) fingerNum = 4; // Max 4 fingers + }); + } + + function detectBarre(positions) { + // Detect if there's a barre (same fret on multiple strings) + const fretGroups = {}; + positions.forEach((p, idx) => { + if (p.fret > 0) { + if (!fretGroups[p.fret]) fretGroups[p.fret] = []; + fretGroups[p.fret].push(idx); + } + }); + + for (const [fret, indices] of Object.entries(fretGroups)) { + if (indices.length >= 2) { + const sortedIndices = indices.sort((a, b) => a - b); + const isConsecutive = sortedIndices.every((val, idx) => + idx === 0 || val - sortedIndices[idx - 1] <= 2 + ); + + if (isConsecutive) { + return { + fret: parseInt(fret), + fromString: Math.min(...sortedIndices), + toString: Math.max(...sortedIndices) + }; + } + } + } + + return null; + } + + // Generate SVG chord diagram + function generateChordDiagramSVG(voicing, chordName, tuningName, fingerNotation = 'numbers') { + const width = 160; + const height = 200; + const marginLeft = 30; + const marginTop = 40; + const marginRight = 20; + const marginBottom = 30; + + const positions = voicing.positions; + const numStrings = positions.length; + const numFrets = 5; + const baseFret = voicing.baseFret; + + const stringSpacing = (width - marginLeft - marginRight) / (numStrings - 1); + const fretSpacing = (height - marginTop - marginBottom) / numFrets; + + let svg = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">`; + + // Background + svg += `<rect width="${width}" height="${height}" fill="white"/>`; + + // Nut (thick line at top) - only show if baseFret is 1 + if (baseFret === 1) { + svg += `<rect x="${marginLeft - 5}" y="${marginTop - 3}" width="${(numStrings - 1) * stringSpacing + 10}" height="6" fill="#1a1a1a"/>`; + } else { + // Show fret number for higher positions + svg += `<text x="${marginLeft - 20}" y="${marginTop + fretSpacing / 2}" font-size="12" font-weight="bold" text-anchor="middle">${baseFret}fr</text>`; + } + + // Fret lines + for (let i = 0; i <= numFrets; i++) { + const y = marginTop + i * fretSpacing; + svg += `<line x1="${marginLeft}" y1="${y}" x2="${marginLeft + (numStrings - 1) * stringSpacing}" y2="${y}" stroke="#333" stroke-width="${i === 0 ? 2 : 1}"/>`; + } + + // String lines + for (let i = 0; i < numStrings; i++) { + const x = marginLeft + i * stringSpacing; + svg += `<line x1="${x}" y1="${marginTop}" x2="${x}" y2="${marginTop + numFrets * fretSpacing}" stroke="#333" stroke-width="1.5"/>`; + } + + // Open/muted string indicators and finger positions + positions.forEach((pos, idx) => { + // Reverse index for display (5th string on left) + const displayIdx = numStrings - 1 - idx; + const x = marginLeft + displayIdx * stringSpacing; + + if (pos.muted) { + // X for muted string + svg += `<text x="${x}" y="${marginTop - 12}" font-size="14" font-weight="bold" text-anchor="middle" fill="#666">✕</text>`; + } else if (pos.fret === 0) { + // Open string circle + svg += `<circle cx="${x}" cy="${marginTop - 15}" r="6" fill="none" stroke="#333" stroke-width="2"/>`; + } else { + // Fretted position - adjust for base fret + const displayFret = pos.fret - baseFret + 1; + if (displayFret >= 1 && displayFret <= numFrets) { + const y = marginTop + (displayFret - 0.5) * fretSpacing; + + // Root notes get filled circle + if (pos.isRoot) { + svg += `<circle cx="${x}" cy="${y}" r="10" fill="#1a1a1a" stroke="#1a1a1a" stroke-width="2"/>`; + // Finger number in white + if (pos.finger) { + const fingerLabel = FINGER_NOTATION[fingerNotation][pos.finger] || pos.finger; + svg += `<text x="${x}" y="${y + 4}" font-size="11" font-weight="bold" text-anchor="middle" fill="white">${fingerLabel}</text>`; + } + } else { + svg += `<circle cx="${x}" cy="${y}" r="10" fill="white" stroke="#1a1a1a" stroke-width="2"/>`; + // Finger number + if (pos.finger) { + const fingerLabel = FINGER_NOTATION[fingerNotation][pos.finger] || pos.finger; + svg += `<text x="${x}" y="${y + 4}" font-size="11" font-weight="bold" text-anchor="middle">${fingerLabel}</text>`; + } + } + } + } + }); + + // Barre indicator (arc connecting notes on same fret) + if (voicing.barre) { + const barre = voicing.barre; + const displayFret = barre.fret - baseFret + 1; + if (displayFret >= 1 && displayFret <= numFrets) { + const y = marginTop + (displayFret - 0.5) * fretSpacing; + const x1 = marginLeft + (numStrings - 1 - barre.toString) * stringSpacing; + const x2 = marginLeft + (numStrings - 1 - barre.fromString) * stringSpacing; + + // Draw barre as a rounded rectangle + svg += `<rect x="${Math.min(x1, x2) - 10}" y="${y - 8}" width="${Math.abs(x2 - x1) + 20}" height="16" rx="8" fill="#1a1a1a" opacity="0.8"/>`; + } + } + + // Degree labels below strings + positions.forEach((pos, idx) => { + const displayIdx = numStrings - 1 - idx; + const x = marginLeft + displayIdx * stringSpacing; + const y = marginTop + numFrets * fretSpacing + 18; + + if (pos.degree && !pos.muted) { + svg += `<text x="${x}" y="${y}" font-size="10" text-anchor="middle" fill="#666">${pos.degree}</text>`; + } else if (pos.muted) { + svg += `<text x="${x}" y="${y}" font-size="10" text-anchor="middle" fill="#999">-</text>`; + } + }); + + svg += '</svg>'; + return svg; + } + // ============================================ // TUNINGS DATA (24 tunings including 4-string) // ============================================ @@ -1414,7 +2044,12 @@ showNotes: true, selectedFretboardTuning: 'Open G', fretboardFamilyFilter: 'all', - useSolfege: false + useSolfege: false, + // Chord tab state + chordTuning: 'Open G', + chordKey: 'G', + chordTypeFilter: 'all', + fingerNotation: 'numbers' }; function loadState() { @@ -1978,13 +2613,18 @@ showNotes: true, selectedFretboardTuning: 'Open G', fretboardFamilyFilter: 'all', - useSolfege: false + useSolfege: false, + chordTuning: 'Open G', + chordKey: 'G', + chordTypeFilter: 'all', + fingerNotation: 'numbers' }; saveState(); applyStateToUI(); populateTuningList(); renderTunings(); renderFretboard(); + initChordTab(); } function applyStateToUI() { @@ -1994,6 +2634,181 @@ document.getElementById('showNotesToggle').checked = state.showNotes; document.getElementById('solfegeToggle').checked = state.useSolfege || false; document.getElementById('fretboardFamilyFilter').value = state.fretboardFamilyFilter || 'all'; + + // Chord tab UI + const chordTuningSelect = document.getElementById('chordTuningSelect'); + if (chordTuningSelect) { + chordTuningSelect.value = state.chordTuning || 'Open G'; + } + + // Finger notation radio + const fingerRadio = document.querySelector(`input[name="fingerNotation"][value="${state.fingerNotation || 'numbers'}"]`); + if (fingerRadio) { + fingerRadio.checked = true; + } + } + + // ============================================ + // CHORD TAB FUNCTIONS + // ============================================ + function populateChordTuningSelect() { + const select = document.getElementById('chordTuningSelect'); + if (!select) return; + + select.innerHTML = ''; + + // Group tunings by family + const families = { + 'g-family': 'G-Based', + 'c-family': 'C-Based', + 'd-family': 'D-Based', + 'other-family': 'Other (4-String)' + }; + + for (const [familyKey, familyName] of Object.entries(families)) { + const group = document.createElement('optgroup'); + group.label = familyName; + + for (const [name, data] of Object.entries(TUNINGS_DATA)) { + if (data.family === familyKey) { + const option = document.createElement('option'); + option.value = name; + option.textContent = name; + if (name === state.chordTuning) { + option.selected = true; + } + group.appendChild(option); + } + } + + select.appendChild(group); + } + } + + function populateKeyButtons() { + const container = document.getElementById('keyButtons'); + if (!container) return; + + container.innerHTML = CHORD_KEYS.map(key => { + const isActive = state.chordKey === key.root ? 'active' : ''; + return `<button class="key-btn ${isActive}" data-key="${key.root}">${key.display}</button>`; + }).join(''); + + // Add click handlers + container.querySelectorAll('.key-btn').forEach(btn => { + btn.addEventListener('click', () => { + state.chordKey = btn.dataset.key; + saveState(); + populateKeyButtons(); + renderChords(); + }); + }); + } + + function populateChordTypeButtons() { + const container = document.getElementById('chordTypeButtons'); + if (!container) return; + + const typeEntries = Object.entries(CHORD_TYPES); + container.innerHTML = ` + <button class="chord-type-btn ${state.chordTypeFilter === 'all' ? 'active' : ''}" data-type="all">All</button> + ${typeEntries.map(([key, type]) => { + const isActive = state.chordTypeFilter === key ? 'active' : ''; + return `<button class="chord-type-btn ${isActive}" data-type="${key}">${type.name}</button>`; + }).join('')} + `; + + // Add click handlers + container.querySelectorAll('.chord-type-btn').forEach(btn => { + btn.addEventListener('click', () => { + state.chordTypeFilter = btn.dataset.type; + saveState(); + populateChordTypeButtons(); + renderChords(); + }); + }); + } + + function renderChords() { + const container = document.getElementById('chordContainer'); + if (!container) return; + + const tuningData = TUNINGS_DATA[state.chordTuning]; + if (!tuningData) { + container.innerHTML = '<div class="no-chord-message">Select a tuning to view chords</div>'; + return; + } + + const fingerNotation = document.querySelector('input[name="fingerNotation"]:checked')?.value || 'numbers'; + + // Determine which chord types to show + const typesToShow = state.chordTypeFilter === 'all' + ? Object.keys(CHORD_TYPES) + : [state.chordTypeFilter]; + + let html = ''; + + typesToShow.forEach(chordType => { + const type = CHORD_TYPES[chordType]; + const chordName = `${state.chordKey}${type.symbol}`; + + // Find voicing for this chord + const voicing = findChordVoicing(tuningData, state.chordKey, chordType, 12); + + if (voicing) { + const svg = generateChordDiagramSVG(voicing, chordName, state.chordTuning, fingerNotation); + + html += ` + <div class="chord-diagram-container"> + <div class="chord-diagram-title">${displayNote(state.chordKey)}${type.symbol}</div> + <div class="chord-diagram-subtitle">${type.name}</div> + ${svg} + <div class="chord-notes-display"> + Notes: ${voicing.chordNotes.join(' - ')} + </div> + </div> + `; + } else { + html += ` + <div class="chord-diagram-container"> + <div class="chord-diagram-title">${displayNote(state.chordKey)}${type.symbol}</div> + <div class="chord-diagram-subtitle">${type.name}</div> + <div class="no-chord-message">Voicing not available for this tuning</div> + </div> + `; + } + }); + + container.innerHTML = html; + } + + function setupChordEventListeners() { + // Tuning selector + const tuningSelect = document.getElementById('chordTuningSelect'); + if (tuningSelect) { + tuningSelect.addEventListener('change', (e) => { + state.chordTuning = e.target.value; + saveState(); + renderChords(); + }); + } + + // Finger notation toggle + document.querySelectorAll('input[name="fingerNotation"]').forEach(radio => { + radio.addEventListener('change', () => { + state.fingerNotation = document.querySelector('input[name="fingerNotation"]:checked')?.value || 'numbers'; + saveState(); + renderChords(); + }); + }); + } + + function initChordTab() { + populateChordTuningSelect(); + populateKeyButtons(); + populateChordTypeButtons(); + setupChordEventListeners(); + renderChords(); } // ============================================ @@ -2006,6 +2821,7 @@ setupEventListeners(); renderTunings(); renderFretboard(); + initChordTab(); } document.addEventListener('DOMContentLoaded', init);