UNPKG

twelvetet

Version:

A minimalistic twelve-tone equal temperament libray for Javascript

393 lines (325 loc) 12.5 kB
'use strict'; var twelvetetSpn = require('twelvetet-spn'); 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 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(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(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 = twelvetetSpn.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 = twelvetetSpn.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(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; }(); module.exports = TwelveTet;