UNPKG

12tet

Version:

Music theory library for generating and working with chords, modes, intervals, etc.

271 lines (270 loc) 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.locrianKey = exports.aeolianKey = exports.mixolydianKey = exports.lydianKey = exports.phrygianKey = exports.dorianKey = exports.ionianKey = exports.key = exports.keySignatureToAccidentals = exports.getDegreesByNote = exports.generateNoteByDegree = exports.adjustNote = exports.convertDiatonicKeyTonesToNotes = exports.getKeyTonesByDegree = exports.getKeyTones = exports.getKeySignatureFromKeyNotes = void 0; const utils_1 = require("../utils"); const note_1 = require("../note"); const mode_1 = require("../mode"); // Given the notes of a key, return the key signature function getKeySignatureFromKeyNotes(notes) { if (notes.length != 7) { return TypeError(`Invalid key. Notes array ${notes} does not have enough notes to make a valid key`); } // Count the total number of sharps and flats present in the notes array let sharps = 0; let flats = 0; notes.forEach(note => { sharps += (note.match(/#/g) || []).length; flats += (note.match(/b/g) || []).length; }); if (sharps !== 0 && flats !== 0) { return TypeError(`Invalid key. Notes array ${notes} cannot contain both sharps and flats`); } else if (sharps > 14 || flats > 14) { return TypeError(`Invalid key. Cannot have more than 14 sharps or flats`); } else if (sharps === 0 && flats === 0) { return ''; } else { const keySignature = flats === 0 ? `${sharps}#` : `${flats}b`; return (0, mode_1.isModeKeySignature)(keySignature) ? keySignature : TypeError(`Key signature ${keySignature} is not a valid key signature`); } } exports.getKeySignatureFromKeyNotes = getKeySignatureFromKeyNotes; // Given a tonic note and a mode, return an array of Tones function getKeyTones(tonic, modeName) { const tone = note_1.tonesByNote[tonic]; let toneIndexes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; toneIndexes = (0, utils_1.rotateArray)(toneIndexes, tone.index); const modeTonePattern = mode_1.modeBaseByName[modeName].semitoneStructure; // Generate tones const keyTones = []; let index = -1; while (keyTones.length < 7) { // If we're still at the tonic, just use the first toneIndex if (index === -1) { keyTones.push(note_1.tones[toneIndexes[0]]); // If not, count up the appropriate number of semitones for the mode } else { keyTones.push(note_1.tones[toneIndexes[(0, utils_1.sumTo)([...modeTonePattern], index)]]); } index++; } return keyTones; } exports.getKeyTones = getKeyTones; function getKeyTonesByDegree(tonic, modeName) { const keyTonesByDegree = {}; const tonicTone = note_1.tonesByNote[tonic]; let toneIndexes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; const modeSemitoneLengths = mode_1.modeBaseByName[modeName].semitoneStructure; // Start toneIndexes on the index of the tonic tone toneIndexes = (0, utils_1.rotateArray)(toneIndexes, tonicTone.index); mode_1.standardModeDegrees.forEach(standardModeDegree => { // Get the number of semitones between the tonic and this tone // -1 to make an array index out of a mode degree // -1 because the modeTonePattern maps the semitones at the 0th element to the II degree of the mode const semitones = standardModeDegree === '1' ? 0 : (0, utils_1.sumTo)([...modeSemitoneLengths], parseInt(standardModeDegree) - 1 - 1); // Get the tone index of the degree and its # and b equivalents const flatToneIndex = (0, utils_1.getWrappedArrayElement)(toneIndexes, semitones - 1); const toneIndex = (0, utils_1.getWrappedArrayElement)(toneIndexes, semitones); const sharpToneIndex = (0, utils_1.getWrappedArrayElement)(toneIndexes, semitones + 1); // Set the values of the degree tone and its # and b equivalents keyTonesByDegree[`b${standardModeDegree}`] = note_1.tones[flatToneIndex]; keyTonesByDegree[standardModeDegree] = note_1.tones[toneIndex]; keyTonesByDegree[`#${standardModeDegree}`] = note_1.tones[sharpToneIndex]; }); return keyTonesByDegree; } exports.getKeyTonesByDegree = getKeyTonesByDegree; // Given a tonic and an array of Tones, simplify to an array of notes function convertDiatonicKeyTonesToNotes(tonic, keyTones) { if (keyTones.length != 7) { return TypeError(`The list of key tones does not have 7 tones`); } if (!keyTones[0].includes(tonic)) { return TypeError(`The first key tone ${keyTones[0]} does not include the tonic note ${tonic}`); } const notes = []; // Starting with the supplied tonic, iterate over the list of tones and get the next in the sequence // We exploit the fact that every valid mode has only a single instance of each of A, B, C, D, E, F, G // and that every tone always contains different 2-3 instances of each of A, B, C, D, E, F, G // So if the last note was B# we know the next note has to have a root C for (let i = 0; i < keyTones.length; i++) { const tone = keyTones[i]; if (i === 0) { notes.push(tonic); } else { const lastNaturalNote = notes[i - 1][0]; if ((0, note_1.isNaturalNote)(lastNaturalNote)) { const nextNaturalNote = (0, note_1.getNextNaturalNote)(lastNaturalNote); if ((0, utils_1.isTypeError)(nextNaturalNote)) { return nextNaturalNote; } tone.forEach(note => { if (note[0] === nextNaturalNote) { notes.push(note); } }); } else { return TypeError(`Note ${lastNaturalNote} is not a valid natural note`); } } } if (notes.length != 7) { return TypeError(`The generated list of notes ${notes} does not contain 7 notes`); } return notes; } exports.convertDiatonicKeyTonesToNotes = convertDiatonicKeyTonesToNotes; // Get the note a sharp or flat above or below a given note function adjustNote(note, adjustment) { const toneAdjustment = adjustment === 'b' ? -1 : 1; const tone = note_1.tones[(0, utils_1.wrapValue)(note_1.tonesByNote[note].index + toneAdjustment, 12)]; const adjustedToneNaturalNote = tone.filter(note_1.isNaturalNote); const adjustedToneStandardSharpNote = tone.filter(note_1.isStandardSharpNote); const adjustedToneStandardFlatNote = tone.filter(note_1.isStandardFlatNote); const adjustedToneTheoreticalSharpNote = tone.filter(note_1.isTheoreticalSharpNote); const adjustedToneTheoreticalFlatNote = tone.filter(note_1.isTheoreticalFlatNote); // Adjust the note directly if ((0, note_1.isNaturalNote)(note)) { return `${note}${adjustment}`; // or subtract a b } else if (((0, note_1.isStandardFlatNote)(note) || (0, note_1.isTheoreticalFlatNote)(note)) && adjustment == '#') { return note.slice(0, -1); // or subtract a # } else if (((0, note_1.isStandardSharpNote)(note) || (0, note_1.isTheoreticalSharpNote)(note)) && adjustment == 'b') { return note.slice(0, -1); // or add a # } else if ((0, note_1.isStandardSharpNote)(note) && adjustment === '#') { return `${note}${adjustment}`; // or add a b } else if ((0, note_1.isStandardFlatNote)(note) && adjustment === 'b') { return `${note}${adjustment}`; // or use another theoretical sharp } else if ((0, note_1.isTheoreticalSharpNote)(note) && adjustedToneTheoreticalSharpNote.length) { return adjustedToneTheoreticalSharpNote[0]; // or use another theoretical flat } else if ((0, note_1.isTheoreticalFlatNote)(note) && adjustedToneTheoreticalFlatNote.length) { return adjustedToneTheoreticalFlatNote[0]; // or use a standard sharp if given a theoretical sharp } else if ((0, note_1.isTheoreticalSharpNote)(note) && adjustedToneStandardSharpNote.length) { return adjustedToneStandardSharpNote[0]; // or use a standard flat if given a theoretical flat } else if ((0, note_1.isTheoreticalFlatNote)(note) && adjustedToneStandardFlatNote.length) { return adjustedToneStandardFlatNote[0]; // or just use a natural note } else if (adjustedToneNaturalNote.length) { return adjustedToneNaturalNote[0]; // and if all else fails use any standard note } else { return adjustedToneNaturalNote[0] || adjustedToneStandardSharpNote[0] || adjustedToneStandardFlatNote[0]; } } exports.adjustNote = adjustNote; // Given a list of notes, return an object with degree keys and note values // It is assumed that the notes are given in order - i.e. index 0 is the first degree of the key function generateNoteByDegree(notes) { const notesByDegree = {}; mode_1.standardModeDegrees.forEach((degree, index) => { notesByDegree[degree] = notes[index]; }); mode_1.alteredModeDegrees.forEach(degree => { notesByDegree[degree] = adjustNote(notes[parseInt(degree[1]) - 1], degree[0]); }); return notesByDegree; } exports.generateNoteByDegree = generateNoteByDegree; function getDegreesByNote(notesByDegree) { const degreesByNote = {}; Object.keys(notesByDegree).forEach(degree => { degreesByNote[notesByDegree[degree]] = degree; }); return degreesByNote; } exports.getDegreesByNote = getDegreesByNote; function keySignatureToAccidentals(keySignature) { const sharps = ['F#', 'C#', 'G#', 'D#', 'A#', 'E#', 'B#', 'F##', 'C##', 'G##', 'D##', 'A##', 'E##', 'B##']; const flats = ['Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Cb', 'Fb', 'Bbb', 'Ebb', 'Abb', 'Dbb', 'Gbb', 'Cbb', 'Fbb']; if ((0, mode_1.isNeutralModeKeySignature)(keySignature)) { return []; } else if ((0, mode_1.isSharpModeKeySignature)(keySignature)) { return sharps.slice(0, parseInt(keySignature.slice(0, -1))); } else { return flats.slice(0, parseInt(keySignature.slice(0, -1))); } } exports.keySignatureToAccidentals = keySignatureToAccidentals; function key(tonic, modeName) { if (!mode_1.isModeTonicByModeName[modeName](tonic)) { return TypeError(`There is no key in mode ${modeName} with tonic ${tonic}`); } const tonesByDegree = getKeyTonesByDegree(tonic, modeName); const diatonicTones = mode_1.standardModeDegrees.map(degree => tonesByDegree[degree]); const notes = convertDiatonicKeyTonesToNotes(tonic, diatonicTones); if ((0, utils_1.isTypeError)(notes)) { return notes; } const notesByDegree = generateNoteByDegree(notes); const degreesByNote = getDegreesByNote(notesByDegree); const signature = getKeySignatureFromKeyNotes(notes); if ((0, utils_1.isTypeError)(signature)) { return signature; } return { tonic: tonic, mode: modeName, notes: notes, accidentals: keySignatureToAccidentals(signature), toneByDegree: tonesByDegree, noteByDegree: notesByDegree, degreeByNote: degreesByNote, enharmonicEquivalents: diatonicTones[0].filter(note => note !== tonic && mode_1.isModeTonicByModeName[modeName](note)), signature: signature, theoreticalKey: parseInt(signature.slice(0, -1)) > 7 }; } exports.key = key; function ionianKey(tonic) { return key(tonic, 'Ionian'); } exports.ionianKey = ionianKey; function dorianKey(tonic) { return key(tonic, 'Dorian'); } exports.dorianKey = dorianKey; function phrygianKey(tonic) { return key(tonic, 'Phrygian'); } exports.phrygianKey = phrygianKey; function lydianKey(tonic) { return key(tonic, 'Lydian'); } exports.lydianKey = lydianKey; function mixolydianKey(tonic) { return key(tonic, 'Mixolydian'); } exports.mixolydianKey = mixolydianKey; function aeolianKey(tonic) { return key(tonic, 'Aeolian'); } exports.aeolianKey = aeolianKey; function locrianKey(tonic) { return key(tonic, 'Locrian'); } exports.locrianKey = locrianKey;