twelvetet
Version:
A minimalistic twelve-tone equal temperament libray for Javascript
479 lines (397 loc) • 15.8 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.TwelveTet = factory());
}(this, (function () { 'use strict';
var log2 = Math.log2;
var pow = Math.pow;
var round = Math.round;
var TWELFTH_ROOT_OF_TWO = pow(2, 1 / 12);
function next(frequency, semitones) {
return frequency * pow(TWELFTH_ROOT_OF_TWO, semitones);
}
function interval(fromFrequency, toFrequency) {
return 12 * log2(toFrequency / fromFrequency);
}
function isFrequency(value) {
return typeof value === 'number' && isFinite(value) && value > 0;
}
function normalize(frequency, tuningFrequency) {
var semitones = round(interval(tuningFrequency, frequency));
return next(tuningFrequency, semitones);
}
var data = [[null, 0], ['B#', 0], ['C', 0], [null, 0], ['Dbb', 0], ['Bx', 1], ['C#', 1], [null, 1], ['Db', 1], [null, 1], ['Cx', 2], [null, 2], ['D', 2], [null, 2], ['Ebb', 2], [null, 3], ['D#', 3], [null, 3], ['Eb', 3], ['Fbb', 3], ['Dx', 4], [null, 4], ['E', 4], ['Fb', 4], [null, 4], [null, 5], ['E#', 5], ['F', 5], [null, 5], ['Gbb', 5], ['Ex', 6], ['F#', 6], [null, 6], ['Gb', 6], [null, 6], ['Fx', 7], [null, 7], ['G', 7], [null, 7], ['Abb', 7], [null, 8], ['G#', 8], [null, 8], ['Ab', 8], [null, 8], ['Gx', 9], [null, 9], ['A', 9], [null, 9], ['Bbb', 9], [null, 10], ['A#', 10], [null, 10], ['Bb', 10], ['Cbb', 10], ['Ax', 11], [null, 11], ['B', 11], ['Cb', 11], [null, 11]];
var isArray = (function (value) {
return (/^\[object Array\]$/.test(Object.prototype.toString.call(value))
);
});
var isInteger = (function (value) {
return typeof value === 'number' && isFinite(value) && Math.floor(value) === value;
});
/**
* Formats a musical pitch to scientific pitch notation.
*
* @function format
* @memberof module:twelvetet-spn
* @param {Array.<Number>} value An array representing the musical pitch's class and octave.
* @param {Number} value.0 The pitch class of the musical pitch.
* @param {Number} value.1 The octave of the musical pitch.
* @returns {Array} An array of scientific pitch notation enharmonic equivalents of the musical pitch.
* The array always has 5 elements. The order is always `[<double sharp>, <sharp>, <natural>, <flat>, <double flat>]`.
* An element of the array is `null` if there is no corresponding enharmonic equivalent
* @throws {TypeError} Will throw an error if the `value` argument is not an array
*/
var format = (function (value) {
if (!isArray(value)) {
throw new Error('Missing or invalid value. Array expected.');
}
var pitchClass = value[0];
var octave = value[1];
if (!isInteger(pitchClass)) {
throw new Error('Missing or invalid pitch class. Integer expected.');
}
if (!isInteger(octave)) {
throw new Error('Missing or invalid octave. Integer expected.');
}
return data.filter(function (datum) {
return datum[1] === pitchClass;
}).map(function (datum) {
return datum[0] != null ? datum[0] + octave : null;
});
});
/**
* Parses scientific pitch notation of a musical pitch.
*
* @function parse
* @memberof module:twelvetet-spn
* @param {String} value Scientific pitch notation of a musical pitch.
* @returns {Array.<Number>} An array representing the musical pitch's class and octave.
* @throws {TypeError} Will throw an error if the `value` argument is not a string
*/
var parse = (function (value) {
if (typeof value !== 'string') {
throw new TypeError('Missing or invalid value. String expected.');
}
var re = /^(.*?)(-?\d+)$/i;
var found = value.match(re);
if (found == null) {
return null;
}
var name = found[1].toLowerCase().replace(/^./, function (l) {
return l.toUpperCase();
});
var datum = data.find(function (datum) {
return datum[0] === name;
});
if (datum == null) {
return null;
}
return [datum[1], +found[2]];
});
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
var floor = Math.floor;
var round$1 = Math.round;
/**
* Represents a pitch.
*
* @class Pitch
* @inner
* @param {Number} inputFrequency A positive number representing the input frequency in hertz.
* @param {Number} tuningFrequency A positive number representing the tuning frequency in hertz.
*/
var Pitch = function () {
function Pitch(inputFrequency, tuningFrequency) {
classCallCheck(this, Pitch);
if (!isFrequency(inputFrequency)) {
throw new TypeError("Missing or invalid argument, 'inputFrequency'.");
}
if (!isFrequency(tuningFrequency)) {
throw new TypeError("Missing or invalid argument, 'tuningFrequency'");
}
this._inputFrequency = inputFrequency;
this._tuningFrequency = tuningFrequency;
this._frequency = normalize(inputFrequency, tuningFrequency);
}
/**
* Returns the [pitch class]{@link https://en.wikipedia.org/wiki/Pitch_class}
*
* @function class
* @memberof Pitch
* @instance
* @returns {Number} An integer between 0 and 11 representing the [pitch class]{@link https://en.wikipedia.org/wiki/Pitch_class}
*/
Pitch.prototype.class = function _class() {
// NOTE: original formula was `(9 + semitones % 12) % 12` but `-11 % 12` returns `-11`
// instead of the expected `1` because the remainder from the modulo operation takes the
// sign of the dividend. https://mzl.la/2oCl8yz
return (9 + 12 + round$1(interval(this._tuningFrequency, this._frequency)) % 12) % 12;
};
/**
* Returns the pitch octave.
*
* @function octave
* @memberof Pitch
* @instance
* @returns {Number} An integer representing the pitch octave.
*/
Pitch.prototype.octave = function octave() {
return floor(4 + (9 + interval(this._tuningFrequency, this._frequency)) / 12);
};
/**
* Returns the number of semitones between the input and the normalized frequencies.
*
* @function offset
* @memberof Pitch
* @instance
* @returns {Number} The number of semitones between the input and the normalized frequencies.
*/
Pitch.prototype.offset = function offset() {
return interval(this._inputFrequency, this._frequency);
};
/**
* Returns the next pitch at the given number of semitones away from the current pitch.
*
* @function next
* @memberof Pitch
* @instance
* @param {Number} [semitones = 1] An integer representing the number of semitones.
* @returns {Pitch}
* @example
* import TwelveTet from 'twelvetet'
*
* const tuningFrequency = 440
* const twelvetet = new TwelveTet(tuningFrequency)
*
* const pitch = twelvetet.pitch('A4')
* const pitches = {
* 'A#4': pitch.next(), // or pitch.next(1)
* 'B4': pitch.next(2), // or pitch.next().next()
* 'G#4': pitch.next(-1)
* 'G4': pitch.next(-2)
* }
*/
Pitch.prototype.next = function next$$1() {
var semitones = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
if (!isInteger$1(semitones)) {
throw new TypeError("Missing or invalid argument, 'semitones'. Integer expected.");
}
var frequency = next(this._frequency, semitones);
return new Pitch(frequency, this._tuningFrequency);
};
/**
* Returns the previous pitch at the given number of semitones away from the current pitch.
*
* @function previous
* @memberof Pitch
* @instance
* @param {Number} [semitones = 1] An integer representing the number of semitones.
* @returns {Pitch}
* @example
* import TwelveTet from 'twelvetet'
*
* const tuningFrequency = 440
* const twelvetet = new TwelveTet(tuningFrequency)
*
* const pitch = twelvetet.pitch('A4')
* const pitches = {
* 'G#4': pitch.previous(), // or pitch.previous(1)
* 'G4': pitch.previous(2), // or pitch.previous().previous()
* 'A#4': pitch.previous(-1)
* 'B4': pitch.previous(-2)
* }
*/
Pitch.prototype.previous = function previous() {
var semitones = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
if (!isInteger$1(semitones)) {
throw new TypeError("Missing or invalid argument, 'semitones'. Integer expected.");
}
var frequency = next(this._frequency, -semitones);
return new Pitch(frequency, this._tuningFrequency);
};
/**
* Returns the number of semitones between the current pitch and the pitch represented by the given value
*
* @function intervalTo
* @memberof Pitch
* @instance
* @param {Number|String|Pitch} value A value representing a pitch. It can be any of the following:
* <ul>
* <li>a positive number representing a frequency in hertz. If the frequency is out-of-tune, `intervalTo` returns the interval between the frequency of the current pitch and the normalized frequency.</li>
* <li>a string representing scientific pitch notation</li>
* <li>an instance of [Pitch]{@link Pitch}.</li>
* </ul>
*/
Pitch.prototype.intervalTo = function intervalTo(value) {
return round$1(interval(this._frequency, castFrequency(value, this._tuningFrequency)));
};
/**
* Returns the number of semitones between the pitch represented by the given value and the current pitch.
*
* @function intervalFrom
* @memberof Pitch
* @instance
* @param {Number|String|Pitch} value A value representing a pitch. It can be any of the following:
* <ul>
* <li>a positive number representing a frequency in hertz. If the frequency is out-of-tune, `intervalFrom` returns the interval between the normalized frequency and the frequency of the current pitch.</li>
* <li>a string representing scientific pitch notation</li>
* <li>an instance of [Pitch]{@link Pitch}. If the pitch is from an out-of-tune frequency, `intervalFrom` returns the interval between the normalized frequency and the frequency of the current pitch.</li>
* </ul>
*/
Pitch.prototype.intervalFrom = function intervalFrom(value) {
return round$1(interval(castFrequency(value, this._tuningFrequency), this._frequency));
};
/**
* Returns scientific notation of the current pitch
* @function toString
* @memberof Pitch
* @instance
* @param {Boolean} [useFlat=false] If true, use the flat enharmonic equivalent.
* @example
* import TwelveTet from 'twelvetet'
*
* const tuningFrequency = 440
* const twelvetet = new TwelveTet(tuningFrequency)
*
* const pitch = twelvetet.pitch('A#4')
* console.log(pitch) // 'A#4'
* console.log(pitch.toString()) // 'A#4'
* console.log(pitch.toString(true)) // 'Bb4'
*/
Pitch.prototype.toString = function toString() {
var useFlat = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
var results = format([this.class(), this.octave()]);
return results[2] || results[useFlat ? 3 : 1];
};
/**
* Returns the normalized frequency of the pitch.
*
* @function valueOf
* @memberof Pitch
* @instance
* @example
* import TwelveTet from 'twelvetet'
*
* const tuningFrequency = 440
* const twelvetet = new TwelveTet(tuningFrequency)
*
* // returns the normalized frequency
* const pitch = twelvetet.pitch(438)
* console.log(+pitch) // 440
* console.log(pitch.valueOf()) // 440
*/
Pitch.prototype.valueOf = function valueOf() {
return this._frequency;
};
/**
* Returns a boolean indicating whether the two pitches are equal.
*
* @function equals
* @memberof Pitch
* @instance
* @param {Number|String|Pitch} value A value representing a pitch. It can be any of the following:
* <ul>
* <li>a positive number representing a frequency in hertz. If the frequency is out-of-tune, `intervalFrom` returns the interval between the normalized frequency and the frequency of the current pitch.</li>
* <li>a string representing scientific pitch notation</li>
* <li>an instance of [Pitch]{@link Pitch}. If the pitch is from an out-of-tune frequency, `intervalFrom` returns the interval between the normalized frequency and the frequency of the current pitch.</li>
* </ul>
* @returns {Boolean}
*/
Pitch.prototype.equals = function equals(value) {
var pitch = Pitch.create(value, this._tuningFrequency);
return this.class() === pitch.class() && this.octave() === pitch.octave();
};
return Pitch;
}();
Pitch.create = function (value, tuningFrequency) {
return new Pitch(castFrequency(value, tuningFrequency), tuningFrequency);
};
function castFrequency(value, tuningFrequency) {
if (value instanceof Pitch) {
return value._inputFrequency;
}
if (typeof value === 'string') {
var result = parse(value);
if (result == null) {
throw new Error("Invalid argument, 'value'.");
}
return next(tuningFrequency, result[1] * 12 + result[0] - 57);
}
if (!isFrequency(value)) {
throw new TypeError("Missing or invalid argument, 'value'.");
}
return value;
// if (isFrequency(value)) {
// return normalize(value, tuningFrequency)
// }
//
// throw new TypeError("Missing or invalid argument, 'value'.")
}
function isInteger$1(value) {
return typeof value === 'number' && isFinite(value) && floor(value) === value;
}
/**
* @class TwelveTet
* @param {Number} [tuningFrequency=440] The tuning frequency in hertz.
* @example
* import TwelveTet from 'twelvetet'
*
* let twelvetet
*
* // instantiate with default tuning frequency of 440 Hz
* twelvetet = new TwelveTet()
*
* // instantiate with given tuning frequency
* const tuningFrequency = 432
* twelvetet = new TwelveTet(tuningFrequency)
*/
var TwelveTet = function () {
function TwelveTet() {
var tuningFrequency = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 440;
classCallCheck(this, TwelveTet);
this._tuningFrequency = tuningFrequency;
}
/**
* Returns a pitch
*
* @function pitch
* @memberof TwelveTet
* @instance
* @param {Number|String|Pitch} value
* @example
* import TwelveTet from 'twelvetet'
*
* const tuningFrequency = 440
* const twelvetet = new TwelveTet(tuningFrequency)
*
*
* let pitch
*
* // create a pitch with the given frequency
* pitch = twelvetet.pitch(440)
* console.log(+pitch) // 440
* console.log(pitch.toString()) // 'A4'
*
* // create a pitch with an out-of-tune frequency
* pitch = twelvetet.pitch(438)
* console.log(+pitch) // 440
* console.log(pitch.toString()) // 'A4'
* console.log(pitch.offset()) // 0.07887184708183335
*
* // create a pitch with scientific notation
* pitch = twelvetet.pitch('A4')
*
* // create a pitch with another pitch
* pitch = twelvetet.pitch(pitch.next())
* console.log(pitch.toString()) // 'A#4'
*/
TwelveTet.prototype.pitch = function pitch(value) {
return Pitch.create(value, this._tuningFrequency);
};
return TwelveTet;
}();
return TwelveTet;
})));