abc-notation-transposition
Version:
A robust utility for transposing ABC Notation by half steps.
193 lines (186 loc) • 9.41 kB
JavaScript
const { DIATONIC_PITCHES, ENHARMONIC_PITCHES } = require('../constants');
const {
getPitchLetter,
getAccidental,
getStoredAccidental,
updateStoredAccidentals,
getTransposedPitchAtOctave
} = require('./transposition-utils');
module.exports.transposePitchByKey = function (
originalPitch,
voiceState,
halfSteps
) {
const {originalKey, transposedKey, mode} = voiceState;
//get pitch & key letters
const originalPitchLetter = getPitchLetter(originalPitch); //returns a capital letter
const originalKeyLetter = getPitchLetter(originalKey[mode]);
const transposedKeyLetter = getPitchLetter(transposedKey[mode]);
//get the scale degree of the pitch
const originalScaleDegree = getScaleDegreeFromPitchLetter(originalPitchLetter, originalKeyLetter); //returns a number
//get the the letter in the new key
const transposedPitchLetter = getPitchLetterFromScaleDegree(originalScaleDegree, transposedKeyLetter); //returns a letter
//get the note's accidental or null
let originalAccidental = getAccidental(originalPitch); //returns "", "=", "_", "__", "^", "^^" or null
//if null, get the lastOriginalAccidental
if(!originalAccidental) originalAccidental = getStoredAccidental(voiceState.originalAccidentals, originalPitchLetter); //returns "=", "_", "__", "^", or "^^"
//if it does, update voiceState.originalAccidentals
else updateStoredAccidentals(voiceState.originalAccidentals, originalPitchLetter, originalAccidental);
//get the amount the pitch is modified by
const deviationFromKeySignature = getDeviationFromKeySignature(originalPitchLetter, originalAccidental, originalKey.keySig);
const [transposedPitch, transposedAccidental, displayAccidental] = applyDeviation(transposedPitchLetter, transposedKey.keySig, deviationFromKeySignature, voiceState.transposedAccidentals);
//calculate octaves
let transposedPitchAtOctave = getTransposedPitchAtOctave(originalPitch, transposedPitch, halfSteps);
if(displayAccidental) transposedPitchAtOctave = transposedAccidental + transposedPitchAtOctave;
return transposedPitchAtOctave;
}
//returns a scale degree (a number). throws an error if the key or pitch letters passed in aren't A-G (case sensitive).
function getScaleDegreeFromPitchLetter(pitchLetter, keyLetter) {
if(!pitchLetter.match(/[A-G]/) || !keyLetter.match(/[A-G]/)) {
throw new Error("pitchLetter and keyLetter must be capital letters between A and G");
}
const scale = [];
const rootIndex = DIATONIC_PITCHES.findIndex(pitch => pitch === keyLetter);
const octaveIndex = rootIndex + 7;
for(let i = rootIndex; i < octaveIndex; i++) {
scale.push(DIATONIC_PITCHES.get(i));
}
return scale.indexOf(pitchLetter);
}
//returns a letter from A-G. throws an error if the scale degree is not a number between 0 and 6 or the keyLetter is not
//a letter between A-G
function getPitchLetterFromScaleDegree(scaleDegree, keyLetter) {
if(typeof scaleDegree !== 'number' || scaleDegree < 0 || scaleDegree > 6) {
throw new Error("scaleDegree should be a number between 0 and 6 (inclusive)");
}
if(!keyLetter.match(/[A-G]/)) throw new Error("keyLetter must be a capital letter between A and G");
const scale = [];
const rootIndex = DIATONIC_PITCHES.findIndex(pitch => pitch === keyLetter);
const octaveIndex = rootIndex + 7;
for(let i = rootIndex; i < octaveIndex; i++) {
scale.push(DIATONIC_PITCHES.get(i));
}
return scale[scaleDegree];
}
//gets the number of half steps that the pitch+accidental deviates from its pitch level in the key signature
//throws an error if the pitch letter is not A-G, the accidental is not __, _, ^^, ^ or =, or the keySignature doesn't
//have the note as a property
function getDeviationFromKeySignature(pitchLetter, accidental, keySignature) {
if(!pitchLetter.match(/[A-G]/)) throw new Error("pitchLetter must be a capital letter between A and G");
if(!accidental.match(/__|_|\^\^|\^|=/)) throw new Error("Invalid accidental.");
const keySignatureAccidental = keySignature[pitchLetter];
if(!keySignatureAccidental) throw new Error("Could not find pitchLetter in the supplied key signature object.");
switch(keySignatureAccidental) {
case "=" :
switch(accidental) {
case "__" :
return -2;
case "_" :
return -1;
case "=" :
return 0;
case "^" :
return 1;
case "^^" :
return 2;
}
case "_" :
switch(accidental) {
case "__" :
return -1;
case "_" :
return 0;
case "=" :
return 1;
case "^" :
return 2;
case "^^" :
return 3;
}
case "^" :
switch(accidental) {
case "__" :
return -3;
case "_" :
return -2;
case "=" :
return -1;
case "^" :
return 0;
case "^^" :
return 1;
}
}
}
//returns an array containing the transposedPitch, the transposedAccidental, displayAccidental (a boolean value)
function applyDeviation(pitchLetter, keySignature, deviationFromKeySignature, storedAccidentals) {
const keySignatureAccidental = keySignature[pitchLetter];
let pitch = pitchLetter;
let accidental;
if(deviationFromKeySignature === 0) accidental = keySignatureAccidental;
else if(deviationFromKeySignature === 1) {
if(keySignatureAccidental === "_") accidental = "=";
else if(keySignatureAccidental === "=") accidental = "^";
else accidental = "^^";
} else if(deviationFromKeySignature === -1) {
if(keySignatureAccidental === "_") accidental = "__";
else if(keySignatureAccidental === "=") accidental = "_";
else accidental = "=";
} else if(deviationFromKeySignature === 2) {
if(keySignatureAccidental === "_") accidental = "^";
else if(keySignatureAccidental === "=") accidental = "^^";
else return transposePitchChromatically(pitchLetter, keySignatureAccidental, deviationFromKeySignature, storedAccidentals);
} else if(deviationFromKeySignature === -2) {
if(keySignatureAccidental === "_") return transposePitchChromatically(pitchLetter, keySignatureAccidental, deviationFromKeySignature, storedAccidentals);
else if(keySignatureAccidental === "=") accidental = "__";
else accidental = "_";
} else return transposePitchChromatically(pitchLetter, keySignatureAccidental, deviationFromKeySignature, storedAccidentals);
//determine whether to display the accidental
let displayAccidental;
if(storedAccidentals[pitch] === accidental) displayAccidental = false;
else {
storedAccidentals[pitch] = accidental;
displayAccidental = true;
}
return [pitch, accidental, displayAccidental];
}
function transposePitchChromatically(pitchLetter, keySignatureAccidental, deviationFromKeySignature, storedAccidentals) {
const originalPitch = keySignatureAccidental + pitchLetter;
const pitchGroupIndex = ENHARMONIC_PITCHES.findIndex((pitchGroup) => {
return pitchGroup.includes(originalPitch);
});
const transposedPitchGroup = ENHARMONIC_PITCHES.get(pitchGroupIndex + deviationFromKeySignature);
let transposedPitchLetter;
let transposedAccidental;
let displayAccidental;
for(let pitchLetter in storedAccidentals) {
const storedAccidental = storedAccidentals[pitchLetter];
const storedPitch = storedAccidental + pitchLetter;
if(transposedPitchGroup.includes(storedPitch)) {
transposedPitchLetter = pitchLetter;
transposedAccidental = storedAccidental;
displayAccidental = false;
break;
}
}
if(!transposedPitchLetter) {
//if the pitch group contains a natural note, prefer that. otherwise, prefer a flat if the note was transposed down or
//a sharp if it was transposed up
let transposedPitch = transposedPitchGroup.find(pitch => pitch.match(/=/));
/* istanbul ignore else */
if(!transposedPitch) {
if(deviationFromKeySignature < 0) transposedPitch = transposedPitchGroup.find(pitch => pitch.match(/^_[A-G]$/));
else transposedPitch = transposedPitchGroup.find(pitch => pitch.match(/^\^[A-G]$/))
}
transposedPitchLetter = getPitchLetter(transposedPitch);
transposedAccidental = getAccidental(transposedPitch);
storedAccidentals[transposedPitchLetter] = transposedAccidental;
displayAccidental = true;
}
return [transposedPitchLetter, transposedAccidental, displayAccidental];
}
module.exports.getScaleDegreeFromPitchLetter = getScaleDegreeFromPitchLetter;
module.exports.getPitchLetterFromScaleDegree = getPitchLetterFromScaleDegree;
module.exports.getDeviationFromKeySignature = getDeviationFromKeySignature;
module.exports.applyDeviation = applyDeviation;
module.exports.transposePitchChromatically = transposePitchChromatically;