Files
Church-Music/new-site/frontend/src/utils/chordEngine.js

276 lines
6.4 KiB
JavaScript

// Chord Engine - Industry-standard chord transposition and parsing
const NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
const FLAT_NOTES = [
"C",
"Db",
"D",
"Eb",
"E",
"F",
"Gb",
"G",
"Ab",
"A",
"Bb",
"B",
];
// Keywords to ignore when parsing chords (section markers)
const SECTION_KEYWORDS = [
"verse",
"chorus",
"pre-chorus",
"prechorus",
"bridge",
"intro",
"outro",
"tag",
"coda",
"interlude",
"instrumental",
"hook",
"refrain",
"vamp",
"ending",
"turnaround",
"solo",
"break",
];
// Chord quality patterns
const CHORD_QUALITIES = {
major: "",
minor: "m",
diminished: "dim",
augmented: "aug",
suspended2: "sus2",
suspended4: "sus4",
dominant7: "7",
major7: "maj7",
minor7: "m7",
diminished7: "dim7",
augmented7: "aug7",
add9: "add9",
add11: "add11",
6: "6",
m6: "m6",
9: "9",
m9: "m9",
11: "11",
13: "13",
};
// Regex to match chord patterns
const CHORD_REGEX =
/^([A-G][#b]?)(m|min|maj|dim|aug|sus[24]?|add[0-9]+|[0-9]+)?([0-9]*)?(\/[A-G][#b]?)?$/i;
/**
* Parse a potential chord string
* @param {string} text - Text that might be a chord
* @returns {object|null} Parsed chord object or null if not a valid chord
*/
export function parseChord(text) {
const trimmed = text.trim();
// Check if it's a section keyword
if (
SECTION_KEYWORDS.some((keyword) => trimmed.toLowerCase().includes(keyword))
) {
return null;
}
const match = trimmed.match(CHORD_REGEX);
if (!match) return null;
const [, root, quality = "", extension = "", bass = ""] = match;
return {
root: normalizeNote(root),
quality,
extension,
bass: bass ? normalizeNote(bass.slice(1)) : null,
original: trimmed,
};
}
/**
* Normalize note name (handle enharmonics)
* @param {string} note - Note name
* @returns {string} Normalized note name
*/
function normalizeNote(note) {
const upper = note.charAt(0).toUpperCase() + note.slice(1);
// Convert flats to sharps for internal processing
const flatIndex = FLAT_NOTES.indexOf(upper);
if (flatIndex !== -1) {
return NOTES[flatIndex];
}
return upper;
}
/**
* Get the semitone index of a note
* @param {string} note - Note name
* @returns {number} Semitone index (0-11)
*/
function getNoteIndex(note) {
const normalized = normalizeNote(note);
const index = NOTES.indexOf(normalized);
return index !== -1 ? index : FLAT_NOTES.indexOf(normalized);
}
/**
* Transpose a single chord by a number of semitones
* @param {string} chord - Chord string
* @param {number} semitones - Number of semitones to transpose
* @param {boolean} useFlats - Whether to use flats instead of sharps
* @returns {string} Transposed chord string
*/
export function transposeChord(chord, semitones, useFlats = false) {
const parsed = parseChord(chord);
if (!parsed) return chord;
const noteArray = useFlats ? FLAT_NOTES : NOTES;
const rootIndex = getNoteIndex(parsed.root);
const newRootIndex = (rootIndex + semitones + 12) % 12;
const newRoot = noteArray[newRootIndex];
let result = newRoot + parsed.quality + parsed.extension;
if (parsed.bass) {
const bassIndex = getNoteIndex(parsed.bass);
const newBassIndex = (bassIndex + semitones + 12) % 12;
result += "/" + noteArray[newBassIndex];
}
return result;
}
/**
* Get semitones between two keys
* @param {string} fromKey - Starting key
* @param {string} toKey - Target key
* @returns {number} Number of semitones
*/
export function getSemitonesBetween(fromKey, toKey) {
const fromIndex = getNoteIndex(fromKey);
const toIndex = getNoteIndex(toKey);
return (toIndex - fromIndex + 12) % 12;
}
/**
* Transpose all chords in a lyrics string
* @param {string} lyrics - Lyrics with chords in brackets [C] or above words
* @param {number} semitones - Number of semitones to transpose
* @param {boolean} useFlats - Whether to use flats
* @returns {string} Transposed lyrics
*/
export function transposeLyrics(lyrics, semitones, useFlats = false) {
// Handle chords in brackets [C]
const bracketPattern = /\[([^\]]+)\]/g;
return lyrics.replace(bracketPattern, (match, chord) => {
const transposed = transposeChord(chord, semitones, useFlats);
return `[${transposed}]`;
});
}
/**
* Extract all unique chords from lyrics
* @param {string} lyrics - Lyrics with chords
* @returns {string[]} Array of unique chords
*/
export function extractChords(lyrics) {
const bracketPattern = /\[([^\]]+)\]/g;
const chords = new Set();
let match;
while ((match = bracketPattern.exec(lyrics)) !== null) {
const chord = match[1];
if (parseChord(chord)) {
chords.add(chord);
}
}
return Array.from(chords);
}
/**
* Detect the likely key of a song based on its chords
* @param {string[]} chords - Array of chords
* @returns {string} Detected key
*/
export function detectKey(chords) {
if (chords.length === 0) return "C";
// Simple heuristic: first and last chords often indicate the key
const firstChord = parseChord(chords[0]);
const lastChord = parseChord(chords[chords.length - 1]);
// If they match, high confidence
if (firstChord && lastChord && firstChord.root === lastChord.root) {
return firstChord.root + (firstChord.quality.includes("m") ? "m" : "");
}
// Otherwise use first chord
if (firstChord) {
return firstChord.root + (firstChord.quality.includes("m") ? "m" : "");
}
return "C";
}
/**
* Format chord for display (convert internal format to display format)
* @param {string} chord - Chord in internal format
* @param {object} options - Display options
* @returns {string} Formatted chord
*/
export function formatChord(chord, options = {}) {
const { useFlats = false, useSuperscript = false } = options;
let formatted = chord;
if (useFlats) {
NOTES.forEach((sharp, i) => {
if (sharp.includes("#")) {
formatted = formatted.replace(sharp, FLAT_NOTES[i]);
}
});
}
return formatted;
}
/**
* Get all keys for key selector
* @param {boolean} useFlats - Whether to use flats
* @returns {string[]} Array of keys
*/
export function getAllKeys(useFlats = false) {
const notes = useFlats ? FLAT_NOTES : NOTES;
const keys = [];
notes.forEach((note) => {
keys.push(note); // Major
keys.push(note + "m"); // Minor
});
return keys;
}
export default {
parseChord,
transposeChord,
transposeLyrics,
extractChords,
detectKey,
formatChord,
getAllKeys,
getSemitonesBetween,
NOTES,
FLAT_NOTES,
SECTION_KEYWORDS,
};