Created
January 14, 2025 02:10
-
-
Save Enkerli/ff500518c6dd8b8920020c4574856dfc to your computer and use it in GitHub Desktop.
A simple artefact made with Claude AI 3.5 Sonnet to populate a chord dictionary through conversion into Pitch Class Sets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Chord to Pitch Class Set Converter</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
max-width: 800px; | |
margin: 2rem auto; | |
padding: 0 1rem; | |
} | |
.container { | |
background-color: #f5f5f5; | |
padding: 2rem; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
} | |
.input-group { | |
margin-bottom: 1rem; | |
} | |
input { | |
padding: 0.5rem; | |
font-size: 1rem; | |
border: 1px solid #ccc; | |
border-radius: 4px; | |
margin-right: 0.5rem; | |
} | |
button { | |
padding: 0.5rem 1rem; | |
font-size: 1rem; | |
background-color: #007bff; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
} | |
button:hover { | |
background-color: #0056b3; | |
} | |
#result { | |
margin-top: 1rem; | |
font-size: 1.1rem; | |
} | |
.chord-dictionary, .chord-management-section, .export-section { | |
margin-top: 2rem; | |
padding-top: 2rem; | |
border-top: 1px solid #ccc; | |
} | |
.tabs { | |
margin-bottom: 1rem; | |
} | |
.tab-button { | |
padding: 0.5rem 1rem; | |
margin-right: 0.5rem; | |
border: 1px solid #ccc; | |
background: #f5f5f5; | |
cursor: pointer; | |
border-radius: 4px; | |
} | |
.tab-button.active { | |
background: #007bff; | |
color: white; | |
border-color: #0056b3; | |
} | |
.tab-content { | |
padding: 1rem; | |
background: #f9f9f9; | |
border-radius: 4px; | |
} | |
select { | |
width: calc(100% - 1rem); | |
margin-bottom: 0.5rem; | |
} | |
.input-group input, .input-group select { | |
margin-bottom: 0.5rem; | |
width: calc(100% - 1rem); | |
} | |
.binary-format { | |
font-family: monospace; | |
background: #f0f0f0; | |
padding: 0.2rem 0.4rem; | |
border-radius: 3px; | |
} | |
select { | |
padding: 0.5rem; | |
font-size: 1rem; | |
margin-bottom: 1rem; | |
border: 1px solid #ccc; | |
border-radius: 4px; | |
} | |
#chordList { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
gap: 1rem; | |
} | |
.chord-item { | |
background-color: white; | |
padding: 1rem; | |
border-radius: 4px; | |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
} | |
.chord-name { | |
font-weight: bold; | |
margin-bottom: 0.5rem; | |
} | |
.chord-intervals { | |
color: #666; | |
font-size: 0.9rem; | |
} | |
.chord-fullname { | |
color: #444; | |
font-style: italic; | |
margin-bottom: 0.5rem; | |
} | |
.chord-forte { | |
font-family: monospace; | |
color: #666; | |
background: #f0f0f0; | |
padding: 0.2rem 0.4rem; | |
border-radius: 3px; | |
margin-bottom: 0.5rem; | |
} | |
.chord-scales, .chord-avoid { | |
color: #666; | |
font-size: 0.9rem; | |
margin-bottom: 0.5rem; | |
} | |
.chord-scales { | |
font-style: italic; | |
} | |
.chord-avoid { | |
color: #d32f2f; | |
} | |
.result-header { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 1rem; | |
} | |
.edit-button, .save-button { | |
padding: 0.5rem 1rem; | |
font-size: 0.9rem; | |
border-radius: 4px; | |
cursor: pointer; | |
} | |
.edit-button { | |
background-color: #007bff; | |
color: white; | |
border: none; | |
} | |
.save-button { | |
background-color: #28a745; | |
color: white; | |
border: none; | |
} | |
.result-edit .input-group { | |
display: flex; | |
flex-direction: column; | |
gap: 0.5rem; | |
} | |
.result-edit .input-group input { | |
width: 100%; | |
padding: 0.5rem; | |
font-size: 1rem; | |
border: 1px solid #ccc; | |
border-radius: 4px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Chord to Pitch Class Set Converter</h1> | |
<div class="input-group"> | |
<input type="text" id="chordInput" placeholder="Enter chord (e.g., Cmaj7)"> | |
<button id="convertButton" onclick="convertToPCS()">Convert</button> | |
</div> | |
<div id="result"></div> | |
<div class="chord-management-section"> | |
<h2>Manage Chord Qualities</h2> | |
<div class="tabs"> | |
<button onclick="switchTab('add')" class="tab-button active">Add New</button> | |
<button onclick="switchTab('edit')" class="tab-button">Edit Existing</button> | |
</div> | |
<div id="addChordTab" class="tab-content"> | |
<div class="input-group"> | |
<input type="text" id="newQualityName" placeholder="Quality name (e.g., maj7b5)"> | |
<input type="text" id="newQualityFullName" placeholder="Full name (e.g., major seventh flat five)"> | |
<input type="text" id="newQualityForte" placeholder="Forte number (e.g., 4-19A)"> | |
<input type="text" id="newQualityPCS" placeholder="PCS from C (e.g., 0,4,6,11)"> | |
<input type="text" id="newQualityAliases" placeholder="Alternate notations (comma-separated)"> | |
<input type="text" id="newQualityScales" placeholder="Compatible scales (comma-separated)"> | |
<input type="text" id="newQualityAvoid" placeholder="Avoid notes (comma-separated)"> | |
<button onclick="addNewChordQuality()">Add Chord Quality</button> | |
</div> | |
</div> | |
<div id="editChordTab" class="tab-content" style="display: none;"> | |
<div class="input-group"> | |
<select id="editQualitySelect" onchange="loadChordQuality()"> | |
<option value="">Select chord quality to edit</option> | |
</select> | |
<input type="text" id="editQualityFullName" placeholder="Full name (e.g., major seventh flat five)"> | |
<input type="text" id="editQualityForte" placeholder="Forte number (e.g., 4-19A)"> | |
<input type="text" id="editQualityPCS" placeholder="PCS from C (e.g., 0,4,6,11)"> | |
<input type="text" id="editQualityAliases" placeholder="Alternate notations (comma-separated)"> | |
<input type="text" id="editQualityScales" placeholder="Compatible scales (comma-separated)"> | |
<input type="text" id="editQualityAvoid" placeholder="Avoid notes (comma-separated)"> | |
<button onclick="updateChordQuality()">Update Chord Quality</button> | |
</div> | |
</div> | |
</div> | |
<div class="export-section"> | |
<h2>Import/Export Chord Dictionary</h2> | |
<button onclick="exportChordDictionary()">Export to JSON</button> | |
<button onclick="importChordDictionary()">Import from JSON</button> | |
<input type="file" id="jsonFileInput" accept=".json" style="display: none;" onchange="handleFileUpload(event)"> | |
</div> | |
<div class="chord-dictionary"> | |
<h2>Chord Dictionary</h2> | |
<select id="chordCategory" onchange="filterChordTypes()"> | |
<option value="all">All Chords</option> | |
<option value="triads">Triads</option> | |
<option value="seventh">Seventh Chords</option> | |
<option value="sixth">Sixth Chords</option> | |
<option value="extended">Extended Chords</option> | |
<option value="suspended">Suspended Chords</option> | |
<option value="altered">Altered Dominants</option> | |
</select> | |
<div id="chordList"></div> | |
</div> | |
</div> | |
<script> | |
const noteToPC = { | |
'C': 0, 'C#': 1, 'Db': 1, | |
'D': 2, 'D#': 3, 'Eb': 3, | |
'E': 4, | |
'F': 5, 'F#': 6, 'Gb': 6, | |
'G': 7, 'G#': 8, 'Ab': 8, | |
'A': 9, 'A#': 10, 'Bb': 10, | |
'B': 11 | |
}; | |
// Alternate notation mappings | |
const chordAliases = { | |
'maj7': ['Δ', 'Δ7', '∆', '∆7', 'M7'], | |
'maj9': ['Δ9', '∆9', 'M9'], | |
'maj11': ['Δ11', '∆11', 'M11'], | |
'maj13': ['Δ13', '∆13', 'M13'], | |
'min': ['m', '-'], | |
'dim': ['°', 'o'], | |
'aug': ['+'], | |
'minMaj7': ['mM7', 'm∆7', 'm∆', 'mΔ7', 'mΔ', '-∆7', '-∆'], | |
}; | |
const chordForteNumbers = { | |
// Triads (3-note sets) | |
'maj': '3-11B', // (047) | |
'min': '3-11A', // (037) | |
'dim': '3-10', // (036) | |
'aug': '3-12', // (048) | |
// Seventh chords (4-note sets) | |
'maj7': '4-20', // (0,4,7,11) | |
'min7': '4-26', // (0,3,7,10) | |
'7': '4-27B', // (0,4,7,10) | |
'dim7': '4-28', // (0,3,6,9) | |
'm7b5': '4-27A', // (0,3,6,10) | |
'minMaj7': '4-19A', // (0,3,7,11) | |
'aug7': '4-19B', // (0,4,8,10) | |
'augMaj7': '4-24', // (0,4,8,11) | |
// Extended sets can use more complex Forte numbers... | |
'9': '5-27A', // (0,2,4,7,10) | |
'maj9': '5-27B', // (0,2,4,7,11) | |
'min9': '5-27A' // (0,2,3,7,10) | |
}; | |
const chordFullNames = { | |
// Triads | |
'maj': 'major triad', | |
'min': 'minor triad', | |
'dim': 'diminished triad', | |
'aug': 'augmented triad', | |
// Seventh chords | |
'maj7': 'major seventh', | |
'min7': 'minor seventh', | |
'7': 'dominant seventh', | |
'dim7': 'diminished seventh', | |
'm7b5': 'half-diminished seventh', | |
'minMaj7': 'minor-major seventh', | |
'aug7': 'augmented seventh', | |
'augMaj7': 'augmented major seventh', | |
// Sixth chords | |
'6': 'major sixth', | |
'm6': 'minor sixth', | |
// Extended chords | |
'9': 'dominant ninth', | |
'maj9': 'major ninth', | |
'min9': 'minor ninth', | |
'11': 'dominant eleventh', | |
'maj11': 'major eleventh', | |
'min11': 'minor eleventh', | |
'13': 'dominant thirteenth', | |
'maj13': 'major thirteenth', | |
'min13': 'minor thirteenth', | |
// Suspended chords | |
'sus2': 'suspended second', | |
'sus4': 'suspended fourth', | |
'7sus4': 'dominant seventh suspended fourth', | |
'9sus4': 'dominant ninth suspended fourth', | |
// Altered dominants | |
'7b5': 'dominant seventh flat five', | |
'7#5': 'dominant seventh sharp five', | |
'7b9': 'dominant seventh flat nine', | |
'7#9': 'dominant seventh sharp nine', | |
'7#11': 'dominant seventh sharp eleven', | |
'7b13': 'dominant seventh flat thirteen' | |
}; | |
const chordQualities = { | |
// Triads | |
'maj': [0, 4, 7], | |
'min': [0, 3, 7], | |
'dim': [0, 3, 6], | |
'aug': [0, 4, 8], | |
// Seventh chords | |
'maj7': [0, 4, 7, 11], | |
'min7': [0, 3, 7, 10], | |
'7': [0, 4, 7, 10], | |
'dim7': [0, 3, 6, 9], | |
'm7b5': [0, 3, 6, 10], | |
'minMaj7': [0, 3, 7, 11], | |
'aug7': [0, 4, 8, 10], | |
'augMaj7': [0, 4, 8, 11], | |
// Sixth chords | |
'6': [0, 4, 7, 9], | |
'm6': [0, 3, 7, 9], | |
// Extended chords | |
'9': [0, 4, 7, 10, 2], | |
'maj9': [0, 4, 7, 11, 2], | |
'min9': [0, 3, 7, 10, 2], | |
'11': [0, 4, 7, 10, 2, 5], | |
'maj11': [0, 4, 7, 11, 2, 5], | |
'min11': [0, 3, 7, 10, 2, 5], | |
'13': [0, 4, 7, 10, 2, 5, 9], | |
'maj13': [0, 4, 7, 11, 2, 5, 9], | |
'min13': [0, 3, 7, 10, 2, 5, 9], | |
// Suspended chords | |
'sus2': [0, 2, 7], | |
'sus4': [0, 5, 7], | |
'7sus4': [0, 5, 7, 10], | |
'9sus4': [0, 5, 7, 10, 2], | |
// Altered dominants | |
'7b5': [0, 4, 6, 10], | |
'7#5': [0, 4, 8, 10], | |
'7b9': [0, 4, 7, 10, 1], | |
'7#9': [0, 4, 7, 10, 3], | |
'7#11': [0, 4, 7, 10, 2, 6], | |
'7b13': [0, 4, 7, 10, 2, 8] | |
}; | |
// Add these after the other constant definitions | |
const compatibleScales = { | |
// Major-based chords | |
'maj': ['major', 'lydian', 'major bebop'], | |
'maj7': ['major', 'lydian', 'major bebop'], | |
'maj9': ['major', 'lydian'], | |
'maj13': ['major', 'lydian'], | |
'6': ['major', 'major pentatonic'], | |
// Minor-based chords | |
'min': ['minor', 'dorian', 'phrygian', 'melodic minor'], | |
'min7': ['dorian', 'minor bebop', 'minor pentatonic', 'minor'], | |
'min9': ['dorian', 'melodic minor'], | |
'min11': ['dorian', 'minor'], | |
'min13': ['dorian', 'melodic minor'], | |
'm6': ['melodic minor', 'dorian'], | |
// Dominant chords | |
'7': ['mixolydian', 'lydian dominant', 'dominant bebop', 'blues'], | |
'9': ['mixolydian', 'lydian dominant'], | |
'13': ['mixolydian', 'lydian dominant'], | |
'7sus4': ['mixolydian', 'suspended pentatonic'], | |
'7alt': ['altered', 'diminished whole tone'], | |
// Half-diminished and diminished | |
'm7b5': ['locrian', 'locrian #2'], | |
'dim7': ['diminished', 'whole-half diminished'], | |
// Altered dominants | |
'7b9': ['half-whole diminished', 'altered'], | |
'7#9': ['altered', 'diminished whole tone'], | |
'7#11': ['lydian dominant', 'altered'], | |
'7b13': ['altered', 'phrygian dominant'] | |
}; | |
const avoidNotes = { | |
// Major-based chords | |
'maj7': ['4', '#4'], // avoid perfect 4th and tritone | |
'maj9': ['4'], // avoid perfect 4th | |
'maj13': ['4', '7'], // avoid perfect 4th and minor 7th | |
// Minor-based chords | |
'min7': ['6', 'b6'], // avoid both natural and flat 6th | |
'min9': ['6'], // avoid natural 6th | |
'min11': ['13'], // avoid 13th | |
// Dominant chords | |
'7': ['4', '11'], // avoid perfect 4th/11th | |
'9': ['4', '11'], // avoid perfect 4th/11th | |
'13': ['11'], // avoid 11th | |
// Half-diminished and diminished | |
'm7b5': ['5'], // avoid perfect 5th | |
'dim7': ['maj7'], // avoid major 7th | |
// Altered dominants | |
'7alt': ['5'], // avoid perfect 5th | |
'7b9': ['9'], // avoid natural 9th | |
'7#11': ['11'], // avoid perfect 11th | |
'7b13': ['13'] // avoid natural 13th | |
}; | |
function parseChord(chordName) { | |
// Match root note (with optional sharp/flat) and quality | |
const match = chordName.match(/^([A-G][b#]?)(.*)$/); | |
if (!match) return null; | |
const [_, root, quality] = match; | |
let chordQuality = quality || 'maj'; | |
// First check for exact quality match | |
if (chordQualities.hasOwnProperty(chordQuality)) { | |
return { root, quality: chordQuality }; | |
} | |
// Check aliases | |
for (const [standardQuality, aliases] of Object.entries(chordAliases)) { | |
if (aliases.includes(chordQuality)) { | |
return { root, quality: standardQuality }; | |
} | |
} | |
// Map common shorthand notations | |
const shorthandMappings = { | |
'm': 'min', | |
'-': 'min', | |
'º': 'dim', | |
'°': 'dim', | |
'o': 'dim', | |
'+': 'aug', | |
'Δ': 'maj7', | |
'∆': 'maj7', | |
'M': 'maj', | |
'^': 'maj7' | |
}; | |
// Try to match shorthand notations with added numbers | |
for (const [short, standard] of Object.entries(shorthandMappings)) { | |
if (chordQuality.startsWith(short)) { | |
const remainder = chordQuality.slice(short.length); | |
const fullQuality = standard + remainder; | |
if (chordQualities.hasOwnProperty(fullQuality)) { | |
return { root, quality: fullQuality }; | |
} | |
} | |
} | |
// Handle special cases | |
if (chordQuality.match(/^(Maj|MAJ|maj)/)) { | |
chordQuality = 'maj' + chordQuality.slice(3); | |
} | |
if (chordQuality.match(/^(Min|MIN|min)/)) { | |
chordQuality = 'min' + chordQuality.slice(3); | |
} | |
return { | |
root: root, | |
quality: chordQuality | |
}; | |
} | |
function orderByDegrees(pcSet) { | |
// Sort PCS in ascending order from root (0) | |
return [...pcSet].sort((a, b) => a - b); | |
} | |
function toBinaryPCS(pcSet) { | |
let binary = new Array(12).fill(0); | |
pcSet.forEach(pc => binary[pc] = 1); | |
return binary.join(''); | |
} | |
function transposePitchClassSet(pcSet, interval) { | |
return pcSet.map(pc => (pc + interval) % 12); | |
} | |
function normalForm(pcSet) { | |
let rotations = []; | |
for (let i = 0; i < pcSet.length; i++) { | |
let rotation = [...pcSet.slice(i), ...pcSet.slice(0, i)]; | |
rotation = rotation.map((pc, index) => | |
index === 0 ? pc : (pc < rotation[0] ? pc + 12 : pc) | |
); | |
rotations.push(rotation); | |
} | |
// Sort rotations to find the most compact one | |
rotations.sort((a, b) => { | |
for (let i = 0; i < a.length - 1; i++) { | |
if (a[i + 1] - a[i] !== b[i + 1] - b[i]) { | |
return (a[i + 1] - a[i]) - (b[i + 1] - b[i]); | |
} | |
} | |
return 0; | |
}); | |
// Return the most compact rotation, normalized to mod 12 | |
return rotations[0].map(pc => pc % 12); | |
} | |
function getAllTranspositions(pcSet) { | |
let transpositions = []; | |
// For each possible transposition (0-11) | |
for (let i = 0; i < 12; i++) { | |
// Transpose the pitch class set | |
const transposedSet = transposePitchClassSet(pcSet, i); | |
// Convert to binary | |
const binary = toBinaryPCS(transposedSet); | |
// Convert to decimal | |
const decimal = parseInt(binary, 2); | |
// Add to array | |
transpositions.push(decimal); | |
} | |
return transpositions; | |
} | |
// Add event listener for enter key on chord input | |
document.getElementById('chordInput').addEventListener('keypress', function(e) { | |
if (e.key === 'Enter') { | |
convertToPCS(); | |
} | |
}); | |
function switchTab(tabName) { | |
// Update button states | |
document.querySelectorAll('.tab-button').forEach(button => { | |
button.classList.remove('active'); | |
}); | |
event.target.classList.add('active'); | |
// Show/hide appropriate content | |
document.getElementById('addChordTab').style.display = tabName === 'add' ? 'block' : 'none'; | |
document.getElementById('editChordTab').style.display = tabName === 'edit' ? 'block' : 'none'; | |
if (tabName === 'edit') { | |
populateChordSelect(); | |
} | |
} | |
function populateChordSelect() { | |
const select = document.getElementById('editQualitySelect'); | |
select.innerHTML = '<option value="">Select chord quality to edit</option>'; | |
Object.keys(chordQualities).sort().forEach(quality => { | |
const option = document.createElement('option'); | |
option.value = quality; | |
option.textContent = quality; | |
select.appendChild(option); | |
}); | |
} | |
function loadChordQuality() { | |
const quality = document.getElementById('editQualitySelect').value; | |
if (!quality) return; | |
const pcs = chordQualities[quality]; | |
const aliases = chordAliases[quality] || []; | |
const fullName = chordFullNames[quality] || ''; | |
const forteNum = chordForteNumbers[quality] || ''; | |
const scales = compatibleScales[quality] || []; | |
const avoid = avoidNotes[quality] || []; | |
document.getElementById('editQualityPCS').value = pcs.join(','); | |
document.getElementById('editQualityAliases').value = aliases.join(','); | |
document.getElementById('editQualityFullName').value = fullName; | |
document.getElementById('editQualityForte').value = forteNum; | |
document.getElementById('editQualityScales').value = scales.join(','); | |
document.getElementById('editQualityAvoid').value = avoid.join(','); | |
} | |
function enableResultEdit(quality) { | |
document.getElementById('resultView').style.display = 'none'; | |
document.getElementById('resultEdit').style.display = 'block'; | |
} | |
function saveResultEdit(quality) { | |
const fullName = document.getElementById('editResultFullName').value.trim(); | |
const forteNum = document.getElementById('editResultForte').value.trim(); | |
const pcsInput = document.getElementById('editResultPCS').value.trim(); | |
const aliasesInput = document.getElementById('editResultAliases').value.trim(); | |
const scalesInput = document.getElementById('editResultScales').value.trim(); | |
const avoidInput = document.getElementById('editResultAvoid').value.trim(); | |
// Parse PCS input | |
const pcs = pcsInput.split(',').map(n => parseInt(n.trim())); | |
if (pcs.some(isNaN) || pcs.some(n => n < 0 || n > 11)) { | |
alert('Invalid PCS. Please use numbers 0-11 separated by commas'); | |
return; | |
} | |
// Update dictionaries | |
chordQualities[quality] = pcs; | |
if (fullName) { | |
chordFullNames[quality] = fullName; | |
} else { | |
delete chordFullNames[quality]; | |
} | |
if (forteNum) { | |
chordForteNumbers[quality] = forteNum; | |
} else { | |
delete chordForteNumbers[quality]; | |
} | |
const aliases = aliasesInput ? aliasesInput.split(',').map(a => a.trim()) : []; | |
if (aliases.length > 0) { | |
chordAliases[quality] = aliases; | |
} else { | |
delete chordAliases[quality]; | |
} | |
const scales = scalesInput ? scalesInput.split(',').map(s => s.trim()) : []; | |
if (scales.length > 0) { | |
compatibleScales[quality] = scales; | |
} else { | |
delete compatibleScales[quality]; | |
} | |
const avoid = avoidInput ? avoidInput.split(',').map(n => n.trim()) : []; | |
if (avoid.length > 0) { | |
avoidNotes[quality] = avoid; | |
} else { | |
delete avoidNotes[quality]; | |
} | |
// Update display | |
filterChordTypes(); | |
convertToPCS(); // Refresh the result display | |
document.getElementById('resultView').style.display = 'block'; | |
document.getElementById('resultEdit').style.display = 'none'; | |
} | |
function updateChordQuality() { | |
const quality = document.getElementById('editQualitySelect').value; | |
if (!quality) { | |
alert('Please select a chord quality to edit'); | |
return; | |
} | |
const pcsInput = document.getElementById('editQualityPCS').value.trim(); | |
const aliasesInput = document.getElementById('editQualityAliases').value.trim(); | |
const fullNameInput = document.getElementById('editQualityFullName').value.trim(); | |
const forteInput = document.getElementById('editQualityForte').value.trim(); | |
const scalesInput = document.getElementById('editQualityScales').value.trim(); | |
const avoidInput = document.getElementById('editQualityAvoid').value.trim(); | |
// Update dictionaries | |
if (forteInput) { | |
chordForteNumbers[quality] = forteInput; | |
} else { | |
delete chordForteNumbers[quality]; | |
} | |
// Parse PCS input | |
const pcs = pcsInput.split(',').map(n => parseInt(n.trim())); | |
if (pcs.some(isNaN) || pcs.some(n => n < 0 || n > 11)) { | |
alert('Invalid PCS. Please use numbers 0-11 separated by commas'); | |
return; | |
} | |
// Parse aliases | |
const aliases = aliasesInput ? aliasesInput.split(',').map(a => a.trim()) : []; | |
// Parse scales and avoid notes | |
const scales = scalesInput ? scalesInput.split(',').map(s => s.trim()) : []; | |
const avoid = avoidInput ? avoidInput.split(',').map(n => n.trim()) : []; | |
// Update dictionaries | |
chordQualities[quality] = pcs; | |
if (aliases.length > 0) { | |
chordAliases[quality] = aliases; | |
} else { | |
delete chordAliases[quality]; | |
} | |
if (fullNameInput) { | |
chordFullNames[quality] = fullNameInput; | |
} else { | |
delete chordFullNames[quality]; | |
} | |
if (scales.length > 0) { | |
compatibleScales[quality] = scales; | |
} else { | |
delete compatibleScales[quality]; | |
} | |
if (avoid.length > 0) { | |
avoidNotes[quality] = avoid; | |
} else { | |
delete avoidNotes[quality]; | |
} | |
// Update display | |
filterChordTypes(); | |
alert(`Chord quality "${quality}" has been updated`); | |
} | |
function addNewChordQuality() { | |
const qualityName = document.getElementById('newQualityName').value.trim(); | |
const pcsInput = document.getElementById('newQualityPCS').value.trim(); | |
const aliasesInput = document.getElementById('newQualityAliases').value.trim(); | |
const fullNameInput = document.getElementById('newQualityFullName').value.trim(); | |
const forteInput = document.getElementById('newQualityForte').value.trim(); | |
const scalesInput = document.getElementById('newQualityScales').value.trim(); | |
const avoidInput = document.getElementById('newQualityAvoid').value.trim(); | |
if (!qualityName || !pcsInput) { | |
alert('Please provide both quality name and PCS'); | |
return; | |
} | |
if (forteInput) { | |
chordForteNumbers[qualityName] = forteInput; | |
} | |
// Parse PCS input | |
const pcs = pcsInput.split(',').map(n => parseInt(n.trim())); | |
if (pcs.some(isNaN) || pcs.some(n => n < 0 || n > 11)) { | |
alert('Invalid PCS. Please use numbers 0-11 separated by commas'); | |
return; | |
} | |
// Parse aliases, scales and avoid notes | |
const aliases = aliasesInput ? aliasesInput.split(',').map(a => a.trim()) : []; | |
const scales = scalesInput ? scalesInput.split(',').map(s => s.trim()) : []; | |
const avoid = avoidInput ? avoidInput.split(',').map(n => n.trim()) : []; | |
// Add to dictionaries | |
chordQualities[qualityName] = pcs; | |
if (aliases.length > 0) { | |
chordAliases[qualityName] = aliases; | |
} | |
if (fullNameInput) { | |
chordFullNames[qualityName] = fullNameInput; | |
} | |
if (scales.length > 0) { | |
compatibleScales[qualityName] = scales; | |
} | |
if (avoid.length > 0) { | |
avoidNotes[qualityName] = avoid; | |
} | |
// Update display | |
filterChordTypes(); | |
// Clear inputs | |
document.getElementById('newQualityName').value = ''; | |
document.getElementById('newQualityPCS').value = ''; | |
document.getElementById('newQualityAliases').value = ''; | |
document.getElementById('newQualityFullName').value = ''; | |
document.getElementById('newQualityForte').value = ''; | |
document.getElementById('newQualityScales').value = ''; | |
document.getElementById('newQualityAvoid').value = ''; | |
} | |
function exportChordDictionary() { | |
const dictionary = {}; | |
Object.entries(chordQualities).forEach(([quality, pcs]) => { | |
const orderedPCS = orderByDegrees(pcs); | |
const binaryPCS = toBinaryPCS(pcs); | |
const decimalPCS = parseInt(binaryPCS, 2); | |
const transpositions = getAllTranspositions(pcs); | |
dictionary[quality] = { | |
pcs: orderedPCS, | |
aliases: chordAliases[quality] || [], | |
fullName: chordFullNames[quality] || quality, | |
binary: binaryPCS, | |
forteNumber: chordForteNumbers[quality] || '', | |
decimal: decimalPCS, | |
transpositions: { | |
'C': transpositions[0], | |
'Db': transpositions[1], | |
'D': transpositions[2], | |
'Eb': transpositions[3], | |
'E': transpositions[4], | |
'F': transpositions[5], | |
'Gb': transpositions[6], | |
'G': transpositions[7], | |
'Ab': transpositions[8], | |
'A': transpositions[9], | |
'Bb': transpositions[10], | |
'B': transpositions[11] | |
}, | |
compatibleScales: compatibleScales[quality] || [], | |
avoidNotes: avoidNotes[quality] || [] | |
}; | |
}); | |
// Create and trigger download | |
const dataStr = JSON.stringify(dictionary, null, 2); | |
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); | |
const linkElement = document.createElement('a'); | |
linkElement.setAttribute('href', dataUri); | |
linkElement.setAttribute('download', 'chord_dictionary.json'); | |
document.body.appendChild(linkElement); | |
linkElement.click(); | |
document.body.removeChild(linkElement); | |
} | |
function orderByDegreesFromRoot(pcs, root) { | |
// Normalize all PCs relative to the root | |
let normalizedPcs = pcs.map(pc => (pc - root + 12) % 12); | |
// Sort in ascending order | |
normalizedPcs.sort((a, b) => a - b); | |
// Shift back to original root | |
return normalizedPcs.map(pc => (pc + root) % 12); | |
} | |
function convertToPCS() { | |
const chordInput = document.getElementById('chordInput').value.trim(); | |
const resultDiv = document.getElementById('result'); | |
const parsed = parseChord(chordInput); | |
if (!parsed) { | |
resultDiv.innerHTML = 'Invalid chord format. Please try again.'; | |
return; | |
} | |
const { root, quality } = parsed; | |
if (!noteToPC.hasOwnProperty(root)) { | |
resultDiv.innerHTML = 'Unknown root note. Please try again.'; | |
return; | |
} | |
const rootPC = noteToPC[root]; | |
let pcs; | |
if (chordQualities.hasOwnProperty(quality)) { | |
// Use existing chord quality | |
pcs = chordQualities[quality].map(interval => (rootPC + interval) % 12); | |
} else { | |
resultDiv.innerHTML = ` | |
Unknown chord quality "${quality}". | |
<br>You can add it using the "Add New Chord Quality" section below. | |
`; | |
// Pre-fill the new quality form | |
document.getElementById('newQualityName').value = quality; | |
return; | |
} | |
// Order PCs by degree from the root | |
const orderedPCS = orderByDegreesFromRoot(pcs, rootPC); | |
const binaryPCS = toBinaryPCS(pcs); | |
const decimalPCS = parseInt(binaryPCS, 2); | |
// Format the result | |
const aliases = chordAliases[quality] ? `(${chordAliases[quality].join(', ')})` : ''; | |
resultDiv.innerHTML = ` | |
<div class="result-view" id="resultView"> | |
<div class="result-header"> | |
<h3>Chord: ${root}${quality} ${aliases}</h3> | |
<button onclick="enableResultEdit('${quality}')" class="edit-button">Edit</button> | |
</div> | |
<p>Full Name: ${root} ${chordFullNames[quality] || quality}</p> | |
<p>Forte Number: <span class="chord-forte">${chordForteNumbers[quality] || 'N/A'}</span></p> | |
<p>Root: ${root} (${rootPC})</p> | |
<p>Quality: ${quality}</p> | |
<p>Pitch Class Set (from root): [${orderedPCS.join(', ')}]</p> | |
<p>Compatible Scales: ${compatibleScales[quality]?.join(', ') || 'N/A'}</p> | |
<p>Avoid Notes: ${avoidNotes[quality]?.join(', ') || 'None'}</p> | |
<p>Binary: <span class="binary-format">${binaryPCS}</span></p> | |
<p>Decimal: ${decimalPCS}</p> | |
<p>Transpositions:</p> | |
<pre>${JSON.stringify(getAllTranspositions(chordQualities[quality]), null, 2)}</pre> | |
</div> | |
<div class="result-edit" id="resultEdit" style="display: none;"> | |
<div class="result-header"> | |
<h3>Editing: ${root}${quality}</h3> | |
<button onclick="saveResultEdit('${quality}')" class="save-button">Save</button> | |
</div> | |
<div class="input-group"> | |
<input type="text" id="editResultFullName" placeholder="Full name" value="${chordFullNames[quality] || ''}"> | |
<input type="text" id="editResultForte" placeholder="Forte number" value="${chordForteNumbers[quality] || ''}"> | |
<input type="text" id="editResultPCS" placeholder="PCS from C" value="${chordQualities[quality].join(',')}"> | |
<input type="text" id="editResultAliases" placeholder="Alternate notations" value="${(chordAliases[quality] || []).join(',')}"> | |
<input type="text" id="editResultScales" placeholder="Compatible scales" value="${(compatibleScales[quality] || []).join(',')}"> | |
<input type="text" id="editResultAvoid" placeholder="Avoid notes" value="${(avoidNotes[quality] || []).join(',')}"> | |
</div> | |
</div> | |
`; | |
} | |
// Chord categories for filtering | |
const chordCategories = { | |
'triads': ['maj', 'min', 'dim', 'aug'], | |
'seventh': ['maj7', 'min7', '7', 'dim7', 'm7b5', 'minMaj7', 'aug7', 'augMaj7'], | |
'sixth': ['6', 'm6'], | |
'extended': ['9', 'maj9', 'min9', '11', 'maj11', 'min11', '13', 'maj13', 'min13'], | |
'suspended': ['sus2', 'sus4', '7sus4', '9sus4'], | |
'altered': ['7b5', '7#5', '7b9', '7#9', '7#11', '7b13'] | |
}; | |
function intervalToString(intervals) { | |
return intervals.map((interval, index) => { | |
if (index === 0) return 'R'; | |
switch (interval) { | |
case 1: return '♭9'; | |
case 2: return '9'; | |
case 3: return '♯9'; | |
case 4: return '3'; | |
case 5: return '11'; | |
case 6: return '♯11'; | |
case 7: return '5'; | |
case 8: return '♭13'; | |
case 9: return '13'; | |
case 10: return '♭7'; | |
case 11: return '7'; | |
default: return interval; | |
} | |
}).join(', '); | |
} | |
function filterChordTypes() { | |
const category = document.getElementById('chordCategory').value; | |
const chordList = document.getElementById('chordList'); | |
chordList.innerHTML = ''; | |
let chordsToShow; | |
if (category === 'all') { | |
chordsToShow = Object.entries(chordQualities); | |
} else { | |
chordsToShow = Object.entries(chordQualities) | |
.filter(([quality]) => chordCategories[category].includes(quality)); | |
} | |
chordsToShow.forEach(([quality, intervals]) => { | |
const orderedPCS = orderByDegreesFromRoot(intervals, 0); // From C (0) | |
const binaryPCS = toBinaryPCS(intervals); | |
const decimalPCS = parseInt(binaryPCS, 2); | |
const aliases = chordAliases[quality] ? `(${chordAliases[quality].join(', ')})` : ''; | |
const chordItem = document.createElement('div'); | |
chordItem.className = 'chord-item'; | |
chordItem.innerHTML = ` | |
<div class="chord-name">C${quality} ${aliases}</div> | |
<div class="chord-fullname">${chordFullNames[quality] || quality}</div> | |
<div class="chord-forte">Forte: ${chordForteNumbers[quality] || 'N/A'}</div> | |
<div class="chord-scales">Scales: ${compatibleScales[quality]?.join(', ') || 'N/A'}</div> | |
<div class="chord-avoid">Avoid: ${avoidNotes[quality]?.join(', ') || 'None'}</div> | |
<div class="chord-intervals">Intervals: ${intervalToString(intervals)}</div> | |
<div class="chord-pcs">PCS: [${orderedPCS.join(', ')}]</div> | |
<div class="chord-binary">Binary: <span class="binary-format">${binaryPCS}</span></div> | |
<div class="chord-decimal">Decimal: ${decimalPCS}</div> | |
<div class="chord-transpositions"> | |
<details> | |
<summary>Transpositions</summary> | |
<pre>${JSON.stringify(getAllTranspositions(intervals), null, 2)}</pre> | |
</details> | |
</div> | |
`; | |
chordList.appendChild(chordItem); | |
}); | |
} | |
function importChordDictionary() { | |
document.getElementById('jsonFileInput').click(); | |
} | |
function handleFileUpload(event) { | |
const file = event.target.files[0]; | |
if (!file) return; | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
try { | |
const data = JSON.parse(e.target.result); | |
// Validate data format | |
if (typeof data !== 'object') { | |
throw new Error('Invalid JSON format'); | |
} | |
// Populate chord qualities and all related data | |
Object.entries(data).forEach(([quality, info]) => { | |
if (info.pcs && Array.isArray(info.pcs)) { | |
chordQualities[quality] = info.pcs; | |
} | |
if (info.aliases && Array.isArray(info.aliases)) { | |
chordAliases[quality] = info.aliases; | |
} | |
if (info.fullName) { | |
chordFullNames[quality] = info.fullName; | |
} | |
if (info.forteNumber) { | |
chordForteNumbers[quality] = info.forteNumber; | |
} | |
if (info.compatibleScales && Array.isArray(info.compatibleScales)) { | |
compatibleScales[quality] = info.compatibleScales; | |
} | |
if (info.avoidNotes && Array.isArray(info.avoidNotes)) { | |
avoidNotes[quality] = info.avoidNotes; | |
} | |
}); | |
// Update the UI | |
filterChordTypes(); | |
alert('Chord dictionary imported successfully!'); | |
} catch (error) { | |
alert('Failed to import JSON: ' + error.message); | |
} | |
}; | |
reader.readAsText(file); | |
} | |
// Initialize chord dictionary on load | |
window.onload = filterChordTypes; | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment