Skip to content

Instantly share code, notes, and snippets.

@Enkerli
Created January 14, 2025 02:10
Show Gist options
  • Save Enkerli/ff500518c6dd8b8920020c4574856dfc to your computer and use it in GitHub Desktop.
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
<!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