UNPKG

twelvetet

Version:

A minimalistic twelve-tone equal temperament libray for Javascript

479 lines (397 loc) 15.8 kB
(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; })));