index.js
'use strict'
// util
function fillStr (s, num) { return Array(num + 1).join(s) }
function isNum (x) { return typeof x === 'number' }
function isStr (x) { return typeof x === 'string' }
function isDef (x) { return typeof x !== 'undefined' }
function midiToFreq (midi, tuning) {
return Math.pow(2, (midi - 69) / 12) * (tuning || 440)
}
var REGEX = /^([a-gA-G])(#{1,}|b{1,}|x{1,}|)(-?\d*)\s*(.*)\s*$/
/**
* A regex for matching note strings in scientific notation.
*
* @name regex
* @function
* @return {RegExp} the regexp used to parse the note name
*
* The note string should have the form `letter[accidentals][octave][element]`
* where:
*
* - letter: (Required) is a letter from A to G either upper or lower case
* - accidentals: (Optional) can be one or more `b` (flats), `#` (sharps) or `x` (double sharps).
* They can NOT be mixed.
* - octave: (Optional) a positive or negative integer
* - element: (Optional) additionally anything after the duration is considered to
* be the element name (for example: 'C2 dorian')
*
* The executed regex contains (by array index):
*
* - 0: the complete string
* - 1: the note letter
* - 2: the optional accidentals
* - 3: the optional octave
* - 4: the rest of the string (trimmed)
*
* @example
* var parser = require('note-parser')
* parser.regex.exec('c#4')
* // => ['c#4', 'c', '#', '4', '']
* parser.regex.exec('c#4 major')
* // => ['c#4major', 'c', '#', '4', 'major']
* parser.regex().exec('CMaj7')
* // => ['CMaj7', 'C', '', '', 'Maj7']
*/
export function regex () { return REGEX }
var SEMITONES = [0, 2, 4, 5, 7, 9, 11]
/**
* Parse a note name in scientific notation an return it's components,
* and some numeric properties including midi number and frequency.
*
* @name parse
* @function
* @param {String} note - the note string to be parsed
* @param {Boolean} isTonic - true the strings it's supposed to contain a note number
* and some category (for example an scale: 'C# major'). It's false by default,
* but when true, en extra tonicOf property is returned with the category ('major')
* @param {Float} tunning - The frequency of A4 note to calculate frequencies.
* By default it 440.
* @return {Object} the parsed note name or null if not a valid note
*
* The parsed note name object will ALWAYS contains:
* - letter: the uppercase letter of the note
* - acc: the accidentals of the note (only sharps or flats)
* - pc: the pitch class (letter + acc)
* - step: s a numeric representation of the letter. It's an integer from 0 to 6
* where 0 = C, 1 = D ... 6 = B
* - alt: a numeric representation of the accidentals. 0 means no alteration,
* positive numbers are for sharps and negative for flats
* - chroma: a numeric representation of the pitch class. It's like midi for
* pitch classes. 0 = C, 1 = C#, 2 = D ... 11 = B. Can be used to find enharmonics
* since, for example, chroma of 'Cb' and 'B' are both 11
*
* If the note has octave, the parser object will contain:
* - oct: the octave number (as integer)
* - midi: the midi number
* - freq: the frequency (using tuning parameter as base)
*
* If the parameter `isTonic` is set to true, the parsed object will contain:
* - tonicOf: the rest of the string that follows note name (left and right trimmed)
*
* @example
* var parse = require('note-parser').parse
* parse('Cb4')
* // => { letter: 'C', acc: 'b', pc: 'Cb', step: 0, alt: -1, chroma: -1,
* oct: 4, midi: 59, freq: 246.94165062806206 }
* // if no octave, no midi, no freq
* parse('fx')
* // => { letter: 'F', acc: '##', pc: 'F##', step: 3, alt: 2, chroma: 7 })
*/
export function parse (str, isTonic, tuning) {
if (typeof str !== 'string') return null
var m = REGEX.exec(str)
if (!m || (!isTonic && m[4])) return null
var p = { letter: m[1].toUpperCase(), acc: m[2].replace(/x/g, '##') }
p.pc = p.letter + p.acc
p.step = (p.letter.charCodeAt(0) + 3) % 7
p.alt = p.acc[0] === 'b' ? -p.acc.length : p.acc.length
var pos = SEMITONES[p.step] + p.alt
p.chroma = pos < 0 ? 12 + pos : pos % 12
if (m[3]) { // has octave
p.oct = +m[3]
p.midi = pos + 12 * (p.oct + 1)
p.freq = midiToFreq(p.midi, tuning)
}
if (isTonic) p.tonicOf = m[4]
return p
}
var LETTERS = 'CDEFGAB'
function accStr (n) { return !isNum(n) ? '' : n < 0 ? fillStr('b', -n) : fillStr('#', n) }
function octStr (n) { return !isNum(n) ? '' : '' + n }
/**
* Create a string from a parsed object or `step, alteration, octave` parameters
* @param {Object} obj - the parsed data object
* @return {String} a note string or null if not valid parameters
* @since 1.2
* @example
* parser.build(parser.parse('cb2')) // => 'Cb2'
*
* @example
* // it accepts (step, alteration, octave) parameters:
* parser.build(3) // => 'F'
* parser.build(3, -1) // => 'Fb'
* parser.build(3, -1, 4) // => 'Fb4'
*/
export function build (s, a, o) {
if (s === null || typeof s === 'undefined') return null
if (s.step) return build(s.step, s.alt, s.oct)
if (s < 0 || s > 6) return null
return LETTERS.charAt(s) + accStr(a) + octStr(o)
}
/**
* Get midi of a note
*
* @name midi
* @function
* @param {String|Integer} note - the note name or midi number
* @return {Integer} the midi number of the note or null if not a valid note
* or the note does NOT contains octave
* @example
* var parser = require('note-parser')
* parser.midi('A4') // => 69
* parser.midi('A') // => null
* @example
* // midi numbers are bypassed (even as strings)
* parser.midi(60) // => 60
* parser.midi('60') // => 60
*/
export function midi (note) {
if ((isNum(note) || isStr(note)) && note >= 0 && note < 128) return +note
var p = parse(note)
return p && isDef(p.midi) ? p.midi : null
}
/**
* Get freq of a note in hertzs (in a well tempered 440Hz A4)
*
* @name freq
* @function
* @param {String} note - the note name or note midi number
* @param {String} tuning - (Optional) the A4 frequency (440 by default)
* @return {Float} the freq of the number if hertzs or null if not valid note
* @example
* var parser = require('note-parser')
* parser.freq('A4') // => 440
* parser.freq('A') // => null
* @example
* // can change tuning (440 by default)
* parser.freq('A4', 444) // => 444
* parser.freq('A3', 444) // => 222
* @example
* // it accepts midi numbers (as numbers and as strings)
* parser.freq(69) // => 440
* parser.freq('69', 442) // => 442
*/
export function freq (note, tuning) {
var m = midi(note)
return m === null ? null : midiToFreq(m, tuning)
}
export function letter (src) { return (parse(src) || {}).letter }
export function acc (src) { return (parse(src) || {}).acc }
export function pc (src) { return (parse(src) || {}).pc }
export function step (src) { return (parse(src) || {}).step }
export function alt (src) { return (parse(src) || {}).alt }
export function chroma (src) { return (parse(src) || {}).chroma }
export function oct (src) { return (parse(src) || {}).oct }