/** A helper singleton for converting MIDI note numbers into other representations of musical notes. */
class MIDINumber {
/**
* Calculates the name of a chord from an array of MIDI note numbers.
* If a name can't be found, lists all held notes, separated by long dashes.
* @param {number[]} nums An array of MIDI note numbers.
* @param {boolean} [sharp=true] Format accidentals as sharp or flat
* @param {boolean} [verbose=false] Format as short or verbose form of chord names
* @returns {string} The chord name of the held notes.
*/
static toChord (nums, sharp = true, verbose = false) {
const ns = this.makeUnique(nums)
return this.toPentaChord(ns, sharp, verbose) ||
this.toTetrachord(ns, sharp, verbose) ||
this.toTriad(ns, sharp, verbose) ||
(verbose && this.toInterval(nums)) ||
nums.map(n => this.toLetter(n, sharp)).join('–')
}
/** Removes any notes which are an octave multiple of a lower note.
* @example
* // returns [60, 64, 67, 74]
* MIDINumber.makeUnique([60, 64, 67, 72, 74, 76, 79])
* @param {number[]} nums An array of MIDI note numbers
* @returns {number[]} An array of MIDI note numbers
*/
static makeUnique (nums) {
const indices = []
for (const n in nums) {
if (nums[n] - nums[0] < 12 || indices.every(i => nums[i] % 12 !== nums[n] % 12)) {
indices.push(n)
}
}
return indices.map(i => nums[i])
}
/** Converts a MIDI number to its frequency in equal temperament.
* @param {number} num A MIDI note number. Floating point numbers are interpolated logarithmically.
* @returns {number} The note's frequency in Hertz
*/
static toFrequency (num) {
return 13.75 * Math.pow(2, (num - 9) / 12)
}
static get sharpnotes () {
return ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B']
}
static get flatnotes () {
return ['C', 'D♭', 'D', 'E♭', 'E', 'F', 'G♭', 'G', 'A♭', 'A', 'B♭', 'B']
}
/** Converts a MIDI number to its note name.
* @param {number} num A MIDI number.
* @param {boolean} [sharp=true] Format accidentals as sharp or flat.
* @returns {string} The note name
*/
static toLetter (num, sharp = true) {
return (sharp ? this.sharpnotes : this.flatnotes)[num % 12]
}
/** Converts a MIDI number to scientific pitch notation.
* @example
* //returns 'C5'
* MIDINumber.toScientificPitch(60)
* @param {number} num A MIDI number.
* @param {boolean} [sharp=true] Format accidentals as sharp or flat.
* @returns {string} The note name and octave number
*/
static toScientificPitch (num, sharp = true) {
return this.toLetter(num, sharp) + Math.floor(num / 12)
}
/** Converts from scientific pitch back to a MIDI note number
* @param {string} pitch A music note in scientific pitch notation
* @returns {number} the pitch as a MIDI note number
*/
static fromScientificPitch (pitch) {
const matches = pitch.match(/(\D+)(\d+)/),
letter = matches[1].toUpperCase()
let n
if (this.sharpnotes.includes(letter)) {
n = this.sharpnotes.indexOf(letter)
} else if (this.flatnotes.includes(letter)) {
n = this.flatnotes.indexOf(letter)
} else {
throw 'Note name could not be found'
}
return n + 12 * (8 + Number(matches[2]))
}
/** Calculates the interval between a pair of MIDI numbers.
* @param {number[]} nums The MIDI numbers to find the interval between.
* @returns {string} The interval between the notes.
*/
static toInterval (nums) {
return (nums.length !== 2) ? '' : [
'octave', 'minor second', 'major second', 'minor third',
'major third', 'perfect fourth', 'diminished fifth', 'perfect fifth',
'minor sixth', 'major sixth', 'minor seventh', 'major seventh'
][(nums[1] - nums[0]) % 12]
}
/** Calculates the chord represented by three MIDI numbers.
* @param {number[]} nums The MIDI numbers to find the chord of.
* @param {boolean} [sharp=true] Format accidentals as sharp or flat.
* @param {boolean} [verbose=false] Format as short or verbose form of chord names
* @returns {string} The calculated chord, or an empty string if no chord is found.
*/
static toTriad (nums, sharp = true, verbose = false) {
if (nums.length !== 3) return ''
const tl = l => this.toLetter(nums[l], sharp)
return (verbose ? {
'047': tl(0) + ' major',
'0716': tl(0) + ' major',
'038': tl(2) + ' major',
'0815': tl(1) + ' major',
'059': tl(1) + ' major',
'0917': tl(2) + ' major',
'037': tl(0) + ' minor',
'049': tl(2) + ' minor',
'058': tl(1) + ' minor',
'0715': tl(0) + ' minor',
'0916': tl(1) + ' minor',
'0817': tl(2) + ' minor',
'036': tl(0) + ' diminished',
'039': tl(2) + ' diminished',
'069': tl(1) + ' diminished',
'0615': tl(0) + ' diminished',
'0915': tl(2) + ' diminished',
'0918': tl(1) + ' diminished',
'027': tl(0) + ' suspended second',
'0714': tl(0) + ' suspended second',
'057': tl(0) + ' suspended fourth',
'048': tl(0) + ' augmented',
'0410': tl(0) + ' seventh, no fifth',
'0411': tl(0) + ' major seventh, no fifth',
'0310': tl(0) + ' minor seventh, no fifth',
'0311': tl(0) + ' minor major seventh, no fifth'
} : {
'047': tl(0),
'0716': tl(0),
'038': tl(2),
'0815': tl(1),
'059': tl(1),
'0917': tl(2),
'037': tl(0) + 'm',
'058': tl(1) + 'm',
'049': tl(2) + 'm',
'0715': tl(0) + 'm',
'0916': tl(1) + 'm',
'0817': tl(2) + 'm',
'036': tl(0) + 'ᴼ',
'069': tl(1) + 'ᴼ',
'039': tl(2) + 'ᴼ',
'0615': tl(0) + 'ᴼ',
'0918': tl(1) + 'ᴼ',
'0915': tl(2) + 'ᴼ',
'027': tl(0) + 'sus2',
'0714': tl(0) + 'sus2',
'057': tl(0) + 'sus4',
'048': tl(0) + '+',
'0410': tl(0) + '7no5',
'0411': tl(0) + 'M7no5',
'0310': tl(0) + 'm7no5',
'0311': tl(0) + 'mM7no5'
})[nums.map(n => n - nums[0]).join('')] || ''
}
/** Calculates the chord represented by four MIDI numbers.
* @param {number[]} nums The MIDI numbers to find the chord of.
* @param {boolean} [sharp=true] Format accidentals as sharp or flat.
* @param {boolean} [verbose=false] Format as short or verbose form of chord names
* @param {boolean} [slashOkay=true] Return a slash chord if no tetrachord is found.
* @returns {string} The calculated chord, or an empty string if no chord is found.
*/
static toTetrachord (nums, sharp = true, verbose = false, slashOkay = true) {
if (nums.length !== 4) return ''
const tl = this.toLetter(nums[0], sharp)
const chord = (verbose ? {
'04710': ' dominant seventh',
'04610': ' dominant seventh flat five',
'04711': ' major seventh',
'04714': ' added ninth',
'03710': ' minor seventh',
'03711': ' minor major seventh',
'0369': ' diminished seventh',
'03610': ' half-diminished seventh',
'04811': ' augmented major seventh',
'04810': ' augmented seventh',
'0479': ' major sixth',
'0379': ' minor sixth',
'0457': ' added fourth',
'0357': ' minor added fourth',
'0347': ' mixed third'
} : {
'04710': '7',
'04610': '7♭5',
'04711': 'M7',
'04714': 'add9',
'03710': 'm7',
'03711': 'mM7',
'0369': 'ᴼ7',
'03610': 'Ø7',
'04811': '+M7',
'04810': '+7',
'0479': '6',
'0379': 'm6',
'0457': 'add4',
'0357': 'madd4',
'0347': '¬'
})[nums.map(n => n - nums[0]).join('')]
if (chord) {
return tl + chord
}
const upperChord = this.toTriad(nums.slice(1), sharp, verbose)
if (slashOkay && upperChord) {
return upperChord + (verbose ? ' over ' : '/') + tl
} else {
return ''
}
}
/** Calculates the chord represented by five MIDI numbers.
* @param {number[]} nums The MIDI numbers to find the chord of.
* @param {boolean} [sharp=true] Format accidentals as sharp or flat.
* @param {boolean} [verbose=false] Format as short or verbose form of chord names
* @param {boolean} [slashOkay=true] Return a slash chord if no pentachord is found.
* @returns {string} The calculated chord, or an empty string if no chord is found.
*/
static toPentaChord (nums, sharp = true, verbose = false, slashOkay = true) {
if (nums.length !== 5) { return '' }
const tl = this.toLetter(nums[0], sharp)
const chord = (verbose ? {
'0471114': ' major ninth',
'0471014': ' dominant ninth',
'0371114': ' minor major ninth',
'0371014': ' minor ninth',
'0481114': ' augmented major ninth',
'0481014': ' augmented dominant ninth',
'0361014': ' half-diminished ninth',
'0361013': ' half-diminished minor ninth',
'036914': ' diminished ninth',
'036913': ' diminished minor ninth'
} : {
'0471114': 'M9',
'0471014': '9',
'0371114': 'mM9',
'0371014': 'm9',
'0481114': '+M9',
'0481014': '+9',
'0361014': 'ø9',
'0361013': 'ø♭9',
'036914': 'ᴼ9',
'036913': 'ᴼ♭9'
})[nums.map(n => n - nums[0]).join('')]
if (chord) {
return tl + chord
}
const upperChord = this.toTetrachord(nums.slice(1), sharp, verbose, false)
if (slashOkay && upperChord) {
return upperChord + (verbose ? ' over ' : '/') + tl
} else {
return ''
}
}
}