UNPKG

twelvetet

Version:

A minimalistic twelve-tone equal temperament libray for Javascript

262 lines (237 loc) 9.2 kB
import { interval, isFrequency, next, normalize } from './FrequencyHelper' import { format, parse } from 'twelvetet-spn' const floor = Math.floor const round = 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. */ export default class Pitch { constructor(inputFrequency, tuningFrequency) { 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} */ 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(interval(this._tuningFrequency, this._frequency)) % 12) % 12 } /** * Returns the pitch octave. * * @function octave * @memberof Pitch * @instance * @returns {Number} An integer representing the pitch octave. */ 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. */ 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) * } */ next(semitones = 1) { if (!isInteger(semitones)) { throw new TypeError("Missing or invalid argument, 'semitones'. Integer expected.") } const 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) * } */ previous(semitones = 1) { if (!isInteger(semitones)) { throw new TypeError("Missing or invalid argument, 'semitones'. Integer expected.") } const 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> */ intervalTo(value) { return round(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> */ intervalFrom(value) { return round(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' */ toString(useFlat = false) { const 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 */ 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} */ equals(value) { const pitch = Pitch.create(value, this._tuningFrequency) return this.class() === pitch.class() && this.octave() === pitch.octave() } } 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') { const 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(value) { return typeof value === 'number' && isFinite(value) && floor(value) === value }