Initial commit - Church Music Database
This commit is contained in:
275
new-site/frontend/src/utils/chordEngine.js
Normal file
275
new-site/frontend/src/utils/chordEngine.js
Normal file
@@ -0,0 +1,275 @@
|
||||
// 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,
|
||||
};
|
||||
Reference in New Issue
Block a user