Created
May 8, 2016 03:42
-
-
Save danigb/4efd66e704204d8cbebf105c856ebe03 to your computer and use it in GitHub Desktop.
One file tonal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// # Tonal | |
// __tonal__ is a functional music theory library. It deals with abstract music | |
// concepts like picthes and intervals, not actual music. | |
// `tonal` is also the result of my journey of learning how to implement a music | |
// theory library in javascript in a functional way. | |
// You are currently reading the source code of the library. It's written in | |
// [literate programming](https://en.wikipedia.org/wiki/Literate_programming) as | |
// a tribute to the The Haskell School of Music and it's impressive book/source | |
// code ["From Signals to | |
// Symphonies"](http://haskell.cs.yale.edu/wp-content/uploads/2015/03/HSoM.pdf) | |
// that has a big influence over tonal development. | |
// This page is generated using the documentation tool | |
// [docco](http://jashkenas.github.io/docco/) | |
// #### Prelude | |
// Parse note names with `note-parser` | |
const noteParse = require('note-parser').parse | |
// Parse interval names with `interval-notation` | |
const ivlNttn = require('interval-notation') | |
// Utilities | |
// Is an array? | |
export const isArr = Array.isArray | |
// Is a number? | |
export const isNum = (n) => typeof n === 'number' | |
// Is string? | |
export const isStr = (o) => typeof o === 'string' | |
// Is defined? (can be null) | |
export const isDef = (o) => typeof o !== 'undefined' | |
// Is a value? | |
export const isValue = (v) => v !== null && typeof v !== 'undefined' | |
// __Functional helpers__ | |
// Identity function | |
export const id = (x) => x | |
// ## 1. Pitches | |
// An array with the signature: `['tnl', fifths, octaves, direction]`: | |
/** | |
* Create a pitch class in array notation | |
* | |
* @function | |
* @param {Integer} fifhts - the number of fifths from C | |
* @return {Pitch} the pitch in array notation | |
*/ | |
export const pcArr = (f) => ['tnl', f] | |
/** | |
* Create a note pitch in array notation | |
* | |
* @function | |
* @param {Integer} fifhts - the number of fifths from C | |
* @param {Integer} octaves - the number of encoded octaves | |
* @return {Pitch} the pitch in array notation | |
*/ | |
export const noteArr = (f, o) => ['tnl', f, o] | |
// calculate interval direction | |
const calcDir = (f, o) => encDir(7 * f + 12 * o) | |
/** | |
* Create an interval in array notation | |
* | |
* @function | |
* @param {Integer} fifhts - the number of fifths from C | |
* @param {Integer} octaves - the number of encoded octaves | |
* @param {Integer} dir - (Optional) the direction | |
* @return {Pitch} the pitch in array notation | |
*/ | |
export const ivlPitch = (f, o, d) => ['tnl', f, o, d || calcDir(f, o) ] | |
/** | |
* Test if a given object is a pitch | |
* @function | |
* @param {Object} obj - the object to test | |
* @return {Boolean} | |
*/ | |
export const isPitch = (p) => p && p[0] === 'tnl' | |
/** | |
* Test if a given object is a pitch class | |
* @function | |
* @param {Object} obj - the object to test | |
* @return {Boolean} | |
*/ | |
export const isPitchClass = (p) => isPitch(p) && p.length === 2 | |
/** | |
* Test if a given object is a pitch with octave (note pitch or interval) | |
* @function | |
* @param {Object} obj - the object to test | |
* @return {Boolean} | |
*/ | |
export const hasOct = (p) => isPitch(p) && isNum(p[2]) | |
/** | |
* Test if a given object is a note pitch | |
* @function | |
* @param {Object} obj - the object to test | |
* @return {Boolean} | |
*/ | |
export const isNotePitch = (p) => hasOct(p) && p.length === 3 | |
/** | |
* Test if a given object is a pitch interval | |
* @function | |
* @param {Object} obj - the object to test | |
* @return {Boolean} | |
*/ | |
export const isIvlPitch = (i) => hasOct(i) && isNum(i[3]) | |
/** | |
* Test if a given object is a pitch, but not an interval | |
* @function | |
* @param {Object} obj - the object to test | |
* @return {Boolean} | |
*/ | |
export const isPitchNotIvl = (i) => isPitch(i) && !isDef(i[3]) | |
// #### Pitch encoding | |
// Map from letter step to number of fifths and octaves | |
// equivalent to: { C: 0, D: 2, E: 4, F: -1, G: 1, A: 3, B: 5 } | |
const FIFTHS = [0, 2, 4, -1, 1, 3, 5] | |
// Encode a pitch class using the step number and alteration | |
const encPC = (step, alt) => FIFTHS[step] + 7 * alt | |
// Given a number of fifths, return the octaves they span | |
const fOcts = (f) => Math.floor(f * 7 / 12) | |
// Get the number of octaves it span each step | |
const FIFTH_OCTS = FIFTHS.map(fOcts) | |
// Encode octaves | |
const encOct = (step, alt, oct) => oct - FIFTH_OCTS[step] - 4 * alt | |
// Encode direction | |
const encDir = (n) => n < 0 ? -1 : 1 | |
/** | |
* Create a pitch. A pitch in tonal may refer to a pitch class, the pitch | |
* of a note or an interval. | |
* | |
* @param {Integer} step - an integer from 0 to 6 representing letters | |
* from C to B or simple interval numbers from unison to seventh | |
* @param {Integer} alt - the alteration | |
* @param {Integer} oct - the pitch octave | |
* @param {Integer} dir - (Optional, intervals only) The interval direction | |
* @return {Pitch} the pitch encoded as array notation | |
* | |
*/ | |
export function encode (step, alt, oct, dir) { | |
// is valid step? | |
if (step < 0 || step > 6) return null | |
const pc = encPC(step, alt || 0) | |
// if not octave, return the pitch class | |
if (!isNum(oct)) return pcArr(pc) | |
const o = encOct(step, alt, oct) | |
// if not direction, return a note pitch | |
if (!isNum(dir)) return noteArr(pc, o) | |
const d = encDir(dir) | |
// return the interval | |
return ivlPitch(d * pc, d * o, d) | |
} | |
// ### Pitch decoding | |
// remove accidentals to a pitch class | |
// it gets an array and return a number of fifths | |
function unaltered (f) { | |
const i = (f + 1) % 7 | |
return i < 0 ? 7 + i : i | |
} | |
const decodeStep = (f) => STEPS[unaltered(f)] | |
const decodeAlt = (f) => Math.floor((f + 1) / 7) | |
// 'FCGDAEB' steps numbers | |
const STEPS = [3, 0, 4, 1, 5, 2, 6] | |
/** | |
* Decode a pitch to its numeric properties | |
* @param {Pitch} | |
* @return {Object} | |
*/ | |
export function decode (p) { | |
const s = decodeStep(p[1]) | |
const a = decodeAlt(p[1]) | |
const o = isNum(p[2]) ? p[2] + 4 * a + FIFTH_OCTS[s] : null | |
return { step: s, alt: a, oct: o, dir: p[3] || null } | |
} | |
// #### Pitch parsers | |
// Convert from string to pitches is a quite expensive operation that it's | |
// executed a lot of times. Some caching will help: | |
const cached = (parser) => { | |
const cache = {} | |
return (str) => { | |
if (typeof str !== 'string') return null | |
return cache[str] || (cache[str] = parser(str)) | |
} | |
} | |
/** | |
* Parse a note name | |
* @function | |
* @param {String} | |
* @return {Pitch} | |
*/ | |
export const parseNote = cached((str) => { | |
const n = noteParse(str) | |
return n ? encode(n.step, n.alt, n.oct) : null | |
}) | |
/** | |
* Test if the given string is a note name | |
* @function | |
* @param {String} | |
* @return {Boolean} | |
*/ | |
export const isNoteStr = (s) => parseNote(s) !== null | |
/** | |
* Parses an interval name in shorthand notation | |
* @function | |
* @param {String} | |
* @return {Pitch} | |
*/ | |
export const parseIvl = cached((str) => { | |
const i = ivlNttn.parse(str) | |
return i ? encode(i.simple - 1, i.alt, i.oct, i.dir) : null | |
}) | |
/** | |
* Test if the given string is an interval name | |
* @function | |
* @param {String} | |
* @return {Boolean} | |
*/ | |
export const isIvlPitchStr = (s) => parseIvl(s) !== null | |
const parsePitch = (str) => parseNote(str) || parseIvl(str) | |
// ### Pitch to string | |
/** | |
* Given a step number return the letter | |
* @function | |
* @param {Integer} | |
* @return {String} | |
*/ | |
export const toLetter = (s) => 'CDEFGAB'[s % 7] | |
// Repeat a string num times | |
const fillStr = (s, num) => Array(Math.abs(num) + 1).join(s) | |
/** | |
* Given an alteration number, return the accidentals | |
* | |
* @function | |
* @param {Integer} | |
* @return {String} | |
*/ | |
export const toAcc = (n) => fillStr(n < 0 ? 'b' : '#', n) | |
const strNum = (n) => n !== null ? n : '' | |
/** | |
* Given a pitch class or a pitch note, get the string in scientific | |
* notation | |
* | |
* @param {Pitch} | |
* @return {String} | |
*/ | |
export function strNote (n) { | |
const p = isPitch(n) && !n[3] ? decode(n) : null | |
return p ? toLetter(p.step) + toAcc(p.alt) + strNum(p.oct) : null | |
} | |
// is an interval ascending? | |
const isAsc = (p) => p.dir === 1 | |
// is an interval perfectable? | |
const isPerf = (p) => ivlNttn.type(p.step + 1) === 'P' | |
// calculate interval number | |
const calcNum = (p) => isAsc(p) ? p.step + 1 + 7 * p.oct : (8 - p.step) - 7 * (p.oct + 1) | |
// calculate interval alteration | |
const calcAlt = (p) => isAsc(p) ? p.alt : isPerf(p) ? -p.alt : -(p.alt + 1) | |
/** | |
* Given an interval, get the string in scientific | |
* notation | |
* | |
* @param {Pitch} | |
* @return {String} | |
*/ | |
export function strIvl (pitch) { | |
const p = isIvlPitch(pitch) ? decode(pitch) : null | |
if (!p) return null | |
const num = calcNum(p) | |
return p.dir * num + ivlNttn.altToQ(num, calcAlt(p)) | |
} | |
const strPitch = (p) => p[3] ? strIvl(p) : strNote(p) | |
// #### Decorate pitch transform functions | |
const notation = (parse, str) => (v) => !isPitch(v) ? parse(v) : str(v) | |
const asNote = notation(parseNote, id) | |
const asIvl = notation(parseIvl, id) | |
const asPitch = notation(parsePitch, id) | |
const toNoteStr = notation(id, strNote) | |
const toIvlStr = notation(id, strIvl) | |
const toPitchStr = notation(id, strPitch) | |
// create a function decorator to work with pitches | |
const pitchOp = (parse, to) => (fn) => (v) => { | |
// is value in array notation?... | |
const isP = isPitch(v) | |
// then no transformation is required | |
if (isP) return fn(v) | |
// else parse the pitch | |
const p = parse(v) | |
// if parsed, apply function and back to string | |
return p ? to(fn(p)) : null | |
} | |
const noteFn = pitchOp(parseNote, toNoteStr) | |
const ivlFn = pitchOp(parseIvl, toIvlStr) | |
const pitchFn = pitchOp(parsePitch, toPitchStr) | |
/** | |
* Given a string return a note string in scientific notation or null | |
* if not valid string | |
* | |
* @function | |
* @param {String} | |
* @return {String} | |
* @example | |
* ['c', 'db3', '2', 'g+', 'gx4'].map(tonal.note) | |
* // => ['C', 'Db3', null, null, 'G##4'] | |
*/ | |
export const note = noteFn(id) | |
// #### Pitch properties | |
/** | |
* Get pitch class of a note. The note can be a string or a pitch array. | |
* | |
* @function | |
* @param {String|Pitch} | |
* @return {String} the pitch class | |
* @example | |
* tonal.pc('Db3') // => 'Db' | |
*/ | |
export const pc = noteFn((p) => [ 'tnl', p[1] ]) | |
/** | |
* Return the chroma of a pitch. | |
* | |
* @function | |
* @param {String|Pitch} | |
* @return {Integer} | |
*/ | |
export const chroma = pitchFn((n) => { | |
return 7 * n[1] - 12 * fOcts(n[1]) | |
}) | |
/** | |
* Return the letter of a pitch | |
* | |
* @function | |
* @param {String|Pitch} | |
* @return {String} | |
*/ | |
export const letter = noteFn((n) => toLetter(decode(n).step)) | |
export const accidentals = noteFn((n) => toAcc(decode(n).alt)) | |
export const octave = pitchFn((p) => decode(p).oct) | |
export const simplify = ivlFn(function (i) { | |
const d = i[3] | |
const s = decodeStep(d * i[1]) | |
const a = decodeAlt(d * i[1]) | |
return ivlPitch(i[1], -d * (FIFTH_OCTS[s] + 4 * a), d) | |
}) | |
export const simplifyAsc = ivlFn((i) => { | |
var s = simplify(i) | |
return (s[3] === 1) ? s : ivlPitch(s[1], s[2] + 1, 1) | |
}) | |
export const simpleNum = ivlFn(function (i) { | |
const p = decode(i) | |
return p.step + 1 | |
}) | |
export const number = ivlFn((i) => calcNum(decode(i))) | |
export const quality = ivlFn((i) => { | |
const p = decode(i) | |
return ivlNttn.altToQ(p.step + 1, p.alt) | |
}) | |
// __semitones__ | |
// get pitch height | |
const height = (p) => p[1] * 7 + 12 * p[2] | |
export const semitones = ivlFn(height) | |
// #### Midi pitch numbers | |
// The midi note number can have a value between 0-127 | |
// http://www.midikits.net/midi_analyser/midi_note_numbers_for_octaves.htm | |
/** | |
* Test if the given number is a valid midi note number | |
* @function | |
* @param {Object} num - the number to test | |
* @return {Boolean} true if it's a valid midi note number | |
*/ | |
export const isMidi = (m) => isValue(m) && !isArr(m) && m >= 0 && m < 128 | |
// To match the general midi specification where `C4` is 60 we must add 12 to | |
// `height` function: | |
/** | |
* Get midi number for a pitch | |
* @function | |
* @param {Array|String} pitch - the pitch | |
* @return {Integer} the midi number or null if not valid pitch | |
* @example | |
* midi('C4') // => 60 | |
*/ | |
export const midi = function (val) { | |
const p = asNote(val) | |
return hasOct(p) ? height(p) + 12 | |
: isMidi(val) ? +val | |
: null | |
} | |
const PCS = 'C Db D Eb E F Gb G Ab A Bb B'.split(' ') | |
/** | |
* Given a midi number, returns a note name. The altered notes will have | |
* flats. | |
* @function | |
* @param {Integer} midi - the midi note number | |
* @return {String} the note name | |
* @example | |
* tonal.fromMidi(61) // => 'Db4' | |
*/ | |
export const fromMidi = (m) => { | |
const pc = PCS[m % 12] | |
const o = Math.floor(m / 12) - 1 | |
return pc + o | |
} | |
// #### Frequency conversions | |
// The most popular way (in western music) to calculate the frequency of a pitch | |
// is using the [well | |
// temperament](https://en.wikipedia.org/wiki/Well_temperament) tempered tuning. | |
// It assumes the octave to be divided in 12 equally sized semitones and tune | |
// all the notes against a reference: | |
/** | |
* Get a frequency calculator function that uses well temperament and a tuning reference. | |
* @function | |
* @param {Float} ref - the tuning reference | |
* @return {Function} the frequency calculator. It accepts a pitch in array or scientific notation and returns the frequency in herzs. | |
*/ | |
export const wellTempered = (ref) => (pitch) => { | |
const m = midi(pitch) | |
return m ? Math.pow(2, (m - 69) / 12) * ref : null | |
} | |
// The common tuning reference is `A4 = 440Hz`: | |
/** | |
* Get the frequency of a pitch using well temperament scale and A4 equal to 440Hz | |
* @function | |
* @param {Array|String} pitch - the pitch to get the frequency from | |
* @return {Float} the frequency in herzs | |
* @example | |
* tonal.freq('C4') // => 261.6255653005986 | |
*/ | |
export const freq = wellTempered(440) | |
// 2. PITCH DISTANCES | |
// ================== | |
// ### 2.1 Tansposition | |
function trBy (i, p) { | |
if (p === null) return null | |
const f = i[1] + p[1] | |
if (p.length === 2) return [ 'tnl', f ] | |
const o = i[2] + p[2] | |
if (p.length === 3) return [ 'tnl', f, o ] | |
return [ 'tnl', f, o, calcDir(f, o) ] | |
} | |
/** | |
* Transpose notes. Can be used to add intervals | |
* @function | |
*/ | |
export function transpose (a, b) { | |
if (arguments.length === 1) return (b) => transpose(a, b) | |
const pa = asPitch(a) | |
const pb = asPitch(b) | |
const r = isIvlPitch(pa) ? trBy(pa, pb) | |
: isIvlPitch(pb) ? trBy(pb, pa) : null | |
return toPitchStr(r) | |
} | |
/** | |
* Transpose notes. An alias for `transpose` | |
* @function | |
*/ | |
export const tr = transpose | |
// ### 2.2 Distances (in intervals) | |
// substract two pitches | |
function substr (a, b) { | |
if (a.length !== b.length) return null | |
return isPitchClass(a) | |
? ivlPitch(b[1] - a[1], -fOcts(b[1] - a[1]), 1) | |
: ivlPitch(b[1] - a[1], b[2] - a[2]) | |
} | |
/** | |
* Find distance between two pitches. Both pitches MUST be of the same type. | |
* Distances between pitch classes always returns ascending intervals. | |
* Distances between intervals substract one from the other. | |
* | |
* @param {Pitch|String} from - distance from | |
* @param {Pitch|String} to - distance to | |
* @return {Interval} the distance between pitches | |
* @example | |
* var tonal = require('tonal') | |
* tonal.distance('C2', 'C3') // => 'P8' | |
* tonal.distance('G', 'B') // => 'M3' | |
* tonal.distance('M2', 'P5') // => 'P4' | |
*/ | |
export function distance (a, b) { | |
if (arguments.length === 1) return (b) => distance(a, b) | |
const pa = asPitch(a) | |
const pb = asPitch(b) | |
const i = substr(pa, pb) | |
// if a and b are in array notation, no conversion back | |
return a === pa && b === pb ? i : toIvlStr(i) | |
} | |
/** | |
* An alias for `distance` | |
* @function | |
*/ | |
export const dist = distance | |
/** | |
* An alias for `distance` | |
* @function | |
*/ | |
export const interval = distance | |
// ## 3. Lists | |
// items can be separated by spaces, bars and commas | |
const SEP = /\s*\|\s*|\s*,\s*|\s+/ | |
/** | |
* Split a string by spaces (or commas or bars). Always returns an array, even if its empty | |
* @param {String|Array|Object} source - the thing to get an array from | |
* @return {Array} the object as an array | |
*/ | |
export function asArr (src) { | |
return isArr(src) ? src | |
: typeof src === 'string' ? src.trim().split(SEP) | |
: (src === null || typeof src === 'undefined') ? [] | |
: [ src ] | |
} | |
/** | |
* Map a list with a function | |
* | |
* Can be partially applied. | |
* | |
* @param {Function} | |
* @param {String|Array} | |
* @return {Array} | |
*/ | |
export function map (fn, list) { | |
return arguments.length > 1 ? map(fn)(list) : (l) => asArr(l).map(fn) | |
} | |
/** | |
* Filter a list with a function | |
* | |
* Can be partially applied. | |
* | |
* @param {Function} | |
* @param {String|Array} | |
* @return {Array} | |
*/ | |
export function filter (fn, list) { | |
return arguments.length > 1 ? filter(fn)(list) : (l) => asArr(l).filter(fn) | |
} | |
// #### Transform lists in array notation | |
const listToStr = (v) => isPitch(v) ? toPitchStr(v) : isArr(v) ? v.map(toPitchStr) : v | |
/** | |
* Decorates a function to work with lists in pitch array notation | |
* @function | |
*/ | |
export const listFn = (fn) => (src) => { | |
const param = asArr(src).map(asPitch) | |
const result = fn(param) | |
return listToStr(result) | |
} | |
// #### Transpose lists | |
/** | |
* Create an harmonizer: a function that given a note returns a list of notes. | |
* | |
* @function | |
* @param {String|Array} list | |
* @return {Function} | |
*/ | |
export const harmonizer = (list) => (pitch) => { | |
return listFn((list) => list.map(transpose(pitch || 'P1')).filter(id))(list) | |
} | |
/** | |
* Harmonizes a list with a pitch | |
* | |
* @function | |
* @param {String|Array} list | |
* @param {String|Pitch} pitch | |
* @return {Array} | |
*/ | |
export const harmonize = function (list, pitch) { | |
return arguments.length > 1 ? harmonizer(list)(pitch) : harmonizer(list) | |
} | |
// #### Ranges | |
// ascending range | |
const ascR = (b, n) => { for (var a = []; n-- ; a[n] = n + b ); return a; } | |
// descending range | |
const descR = (b, n) => { for (var a = []; n-- ; a[n] = b - n ) ; return a; } | |
/** | |
* Create a range. It works with numbers or note names | |
* @function | |
*/ | |
export function range (a, b) { | |
const ma = isNum(a) ? a : midi(a) | |
const mb = isNum(b) ? b : midi(b) | |
return ma === null || mb === null ? [] | |
: ma < mb ? ascR(ma, mb - ma + 1) : descR(ma, ma - mb + 1) | |
} | |
/** | |
* Create a note range | |
* @function | |
*/ | |
export function noteRange (fn, a, b) { | |
if (arguments.length === 1) return (a, b) => noteRange(fn, a, b) | |
return range(a, b).map(fn).filter((x) => x !== null ) | |
} | |
/** | |
* Create a range of chromatic notes | |
* @function | |
* @example | |
* tonal.chromatic('C2', 'E2') // => ['C2', 'Db2', 'D2', 'Eb2', 'E2'] | |
*/ | |
export const chromatic = noteRange(fromMidi) | |
// #### Cycle of fifths | |
/** | |
* Transpose a tonic a number of perfect fifths. | |
* @function | |
*/ | |
export function fifthsFrom (t, n) { | |
if (arguments.length > 1) return fifthsFrom(t)(n) | |
return (n) => tr(t, ivlPitch(n, 0)) | |
} | |
// #### Sort lists | |
const objHeight = function (p) { | |
if (!p) return -Infinity | |
const f = p[1] * 7 | |
const o = isNum(p[2]) ? p[2] : -Math.floor(f / 12) - 10 | |
return f + o * 12 | |
} | |
const ascComp = (a, b) => objHeight(a) - objHeight(b) | |
const descComp = (a, b) => -ascComp(a, b) | |
export function sort (comp, list) { | |
if (arguments.length > 1) return sort(comp)(list) | |
const fn = comp === true || comp === null ? ascComp | |
: comp === false ? descComp : comp | |
return listFn((arr) => arr.sort(fn)) | |
} | |
// Fin. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment