12tet
Version:
Music theory library for generating and working with chords, modes, intervals, etc.
271 lines (270 loc) • 12.1 kB
JavaScript
;
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;