UNPKG

xen-dev-utils

Version:

Utility functions used by the Scale Workshop ecosystem

473 lines 16.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.wilsonHeight = exports.tenneyHeight = exports.monzoToCents = exports.hasMarginConstantStructure = exports.falsifyConstantStructure = exports.fareyInterior = exports.fareySequence = exports.ceilPow2 = exports.circleDistance = exports.circleDifference = exports.clamp = exports.binomial = exports.FractionSet = exports.modInv = exports.iteratedEuclid = exports.extendedEuclid = exports.div = exports.arraysEqual = exports.sum = void 0; const fraction_1 = require("./fraction"); const monzo_1 = require("./monzo"); const number_array_1 = require("./number-array"); const primes_1 = require("./primes"); __exportStar(require("./fraction"), exports); __exportStar(require("./primes"), exports); __exportStar(require("./conversion"), exports); __exportStar(require("./combinations"), exports); __exportStar(require("./monzo"), exports); __exportStar(require("./approximation"), exports); __exportStar(require("./number-array"), exports); __exportStar(require("./basis"), exports); __exportStar(require("./hnf"), exports); var sum_precise_1 = require("./polyfills/sum-precise"); Object.defineProperty(exports, "sum", { enumerable: true, get: function () { return sum_precise_1.sum; } }); /** * Check if the contents of two arrays are equal using '==='. * @param a The first array. * @param b The second array. * @returns True if the arrays are component-wise equal. */ function arraysEqual(a, b) { if (a === b) { return true; } if (a.length !== b.length) { return false; } for (let i = 0; i < a.length; ++i) { if (a[i] !== b[i]) { return false; } } return true; } exports.arraysEqual = arraysEqual; /** * Floor division. * @param a The dividend. * @param b The divisor. * @returns The quotient of Euclidean division of a by b. */ function div(a, b) { return Math.floor(a / b); } exports.div = div; // https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Pseudocode /** * Extended Euclidean algorithm for integers a and b: * Find x and y such that ax + by = gcd(a, b). * ```ts * result.gcd = a * result.coefA + b * result.coefB; // = gcd(a, b) * result.quotientA = div(a, gcd(a, b)); * result.quotientB = div(b, gcd(a, b)); * ``` * @param a The first integer. * @param b The second integer. * @returns Bézout coefficients, gcd and quotients. */ function extendedEuclid(a, b) { if (isNaN(a) || isNaN(b)) { throw new Error('Invalid input'); } let [rOld, r] = [a, b]; let [sOld, s] = [1, 0]; let [tOld, t] = [0, 1]; while (r !== 0) { const quotient = div(rOld, r); [rOld, r] = [r, rOld - quotient * r]; [sOld, s] = [s, sOld - quotient * s]; [tOld, t] = [t, tOld - quotient * t]; } return { coefA: sOld, coefB: tOld, gcd: rOld, quotientA: t, quotientB: Math.abs(s), }; } exports.extendedEuclid = extendedEuclid; /** * Iterated (extended) Euclidean algorithm. * @param params An iterable of integers. * @returns Bézout coefficients of the parameters. */ function iteratedEuclid(params) { const coefs = []; let a = undefined; for (const param of params) { if (a === undefined) { a = param; coefs.push(1); continue; } const ee = extendedEuclid(a, param); for (let j = 0; j < coefs.length; ++j) { coefs[j] *= ee.coefA; } a = ee.gcd; coefs.push(ee.coefB); } return coefs; } exports.iteratedEuclid = iteratedEuclid; /** * Find modular inverse of a (mod b). * @param a Number to find modular inverse of. * @param b Modulus. * @param strict Ensure that a * modInv(a, b) = 1 (mod b). If `strict = false` we have a * modInv(a, b) = gdc(a, b) (mod b) instead. * @returns The modular inverse in the range {0, 1, ..., b - 1}. */ function modInv(a, b, strict = true) { const { gcd, coefA } = extendedEuclid(a, b); if (strict && gcd !== 1) { throw new Error(`${a} does not have a modular inverse modulo ${b} since they're not coprime`); } return (0, fraction_1.mmod)(coefA, b); } exports.modInv = modInv; /** * Collection of unique fractions. */ class FractionSet extends Set { /** * Check `value` membership. * @param value Value to check for membership. * @returns A boolean asserting whether an element is present with the given value in the `FractionSet` object or not. */ has(value) { for (const other of this) { if (other.equals(value)) { return true; } } return false; } /** * Appends `value` to the `FractionSet` object. * @param value Value to append. * @returns The `FractionSet` object with added value. */ add(value) { if (this.has(value)) { return this; } super.add(value); return this; } /** * Removes the element associated to the `value`. * @param value Value to remove. * @returns A boolean asserting whether an element was successfully removed or not. `FractionSet.prototype.has(value)` will return `false` afterwards. */ delete(value) { for (const other of this) { if (other.equals(value)) { return super.delete(other); } } return false; } } exports.FractionSet = FractionSet; // https://stackoverflow.com/a/37716142 // step 1: a basic LUT with a few steps of Pascal's triangle const BINOMIALS = [ [1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1], [1, 5, 10, 10, 5, 1], [1, 6, 15, 20, 15, 6, 1], [1, 7, 21, 35, 35, 21, 7, 1], [1, 8, 28, 56, 70, 56, 28, 8, 1], ]; // step 2: a function that builds out the LUT if it needs to. /** * Calculate the Binomial coefficient *n choose k*. * @param n Size of the set to choose from. * @param k Number of elements to choose. * @returns The number of ways to choose `k` (unordered) elements from a set size `n`. */ function binomial(n, k) { while (n >= BINOMIALS.length) { const s = BINOMIALS.length; const lastRow = BINOMIALS[s - 1]; const nextRow = [1]; for (let i = 1; i < s; i++) { nextRow.push(lastRow[i - 1] + lastRow[i]); } nextRow.push(1); BINOMIALS.push(nextRow); } return BINOMIALS[n][k]; } exports.binomial = binomial; /** * Clamp a value to a finite range. * @param minValue Lower bound. * @param maxValue Upper bound. * @param value Value to clamp between bounds. * @returns Clamped value. */ function clamp(minValue, maxValue, value) { if (value < minValue) { return minValue; } if (value > maxValue) { return maxValue; } return value; } exports.clamp = clamp; /** * Calculate the difference between two cents values such that equave equivalence is taken into account. * @param a The first pitch measured in cents. * @param b The second pitch measured in cents. * @param equaveCents The interval of equivalence measured in cents. * @returns The first pitch minus the second pitch but on a circle such that large differences wrap around. */ function circleDifference(a, b, equaveCents = 1200.0) { const half = 0.5 * equaveCents; return (0, fraction_1.mmod)(a - b + half, equaveCents) - half; } exports.circleDifference = circleDifference; /** * Calculate the distance between two cents values such that equave equivalence is taken into account. * @param a The first pitch measured in cents. * @param b The second pitch measured in cents. * @param equaveCents The interval of equivalence measured in cents. * @returns The absolute distance between the two pitches measured in cents but on a circle such that large distances wrap around. */ function circleDistance(a, b, equaveCents = 1200.0) { return Math.abs(circleDifference(a, b, equaveCents)); } exports.circleDistance = circleDistance; /** * Calculate the smallest power of two greater or equal to the input value. * @param x Value to compare to. * @returns Smallest `2**n` such that `x <= 2**n`. */ function ceilPow2(x) { if (x >= 1 && x < 0x40000000) { return 1 << (32 - Math.clz32(x - 1)); } if (x <= 0) { return 0; } return 2 ** Math.ceil(Math.log2(x)); } exports.ceilPow2 = ceilPow2; /** * Create an iterator over the n'th Farey sequence. (All fractions between 0 and 1 inclusive.) * @param maxDenominator Maximum denominator in the sequence. * @yields Fractions in ascending order starting from 0/1 and ending at 1/1. */ function* fareySequence(maxDenominator) { let a = 0; let b = 1; let c = 1; let d = maxDenominator; yield new fraction_1.Fraction(a, b); while (0 <= c && c <= maxDenominator) { const k = Math.floor((maxDenominator + b) / d); [a, b, c, d] = [c, d, k * c - a, k * d - b]; yield new fraction_1.Fraction(a, b); } } exports.fareySequence = fareySequence; /** * Create an iterator over the interior of n'th Farey sequence. (All fractions between 0 and 1 exclusive.) * @param maxDenominator Maximum denominator in the sequence. * @yields Fractions in ascending order starting from 1/maxDenominator and ending at (maxDenominator-1)/maxDenominator. */ function* fareyInterior(maxDenominator) { if (maxDenominator < 2) { return; } let a = 1; let b = maxDenominator; let c = 1; let d = maxDenominator - 1; yield new fraction_1.Fraction(a, b); while (d > 1) { const k = Math.floor((maxDenominator + b) / d); [a, b, c, d] = [c, d, k * c - a, k * d - b]; yield new fraction_1.Fraction(a, b); } } exports.fareyInterior = fareyInterior; /** * Determine if an equally tempered scale has constant structure i.e. you can tell the interval class from the size of an interval. * @param steps Musical intervals measured in steps not including the implicit 0 at the start, but including the interval of repetition at the end. * @returns A pair of pairs of indices that have the same stepspan but different subtension. `null` if the scale has constant structure. */ function falsifyConstantStructure(steps) { const n = steps.length; if (!n) { return null; } const period = steps[n - 1]; const scale = [...steps]; for (const step of steps) { scale.push(period + step); } // Map from interval sizes to pairs of [index, subtension] const subtensions = new Map(); // Against implicit unison for (let i = 0; i < n; i++) { if (subtensions.has(scale[i])) { return [subtensions.get(scale[i]), [-1, i + 1]]; } subtensions.set(scale[i], [-1, i + 1]); } // Against each other for (let i = 0; i < n - 1; ++i) { for (let j = 1; j < n; ++j) { const width = scale[i + j] - scale[i]; if (subtensions.has(width)) { const [k, l] = subtensions.get(width); if (j !== l) { return [ [k, k + l], [i, i + j], ]; } } // Add the observed width to the collection subtensions.set(width, [i, j]); } } return null; } exports.falsifyConstantStructure = falsifyConstantStructure; /** * Determine if a scale has constant structure i.e. you can tell the interval class from the size of an interval. * @param scaleCents Musical intervals measured in cents not including the implicit 0 at the start, but including the interval of repetition at the end. * @param margin Margin of equivalence between two intervals measured in cents. * @returns `true` if the scale definitely has constant structure. (A `false` result may convert to `true` using a smaller margin.) */ function hasMarginConstantStructure(scaleCents, margin) { const n = scaleCents.length; if (!n) { return true; } const period = scaleCents[n - 1]; const scale = [...scaleCents]; for (const cents of scaleCents) { scale.push(period + cents); } // Map from interval sizes to (zero-indexed) interval classes a.k.a. subtensions const subtensions = new Map(); // Against unison for (let i = 0; i < n; i++) { // Check for margin equivalence for (const existing of subtensions.keys()) { if (Math.abs(existing - scale[i]) <= margin) { return false; } } subtensions.set(scale[i], i + 1); } // Against each other for (let i = 0; i < n - 1; ++i) { for (let j = 1; j < n; ++j) { const width = scale[i + j] - scale[i]; // Try to get lucky with an exact match if (subtensions.has(width) && subtensions.get(width) !== j) { return false; } // Check for margin equivalence for (const [existing, subtension] of subtensions.entries()) { if (subtension === j) { continue; } if (Math.abs(existing - width) <= margin) { return false; } } // Add the observed width to the collection subtensions.set(width, j); } } return true; } exports.hasMarginConstantStructure = hasMarginConstantStructure; const NATS_TO_CENTS = 1200 / Math.LN2; const IEEE_LIMIT = 2n ** 1024n; /** * Measure the size of a monzo in cents. * Monzos representing small rational numbers (commas) are measured accurately. * @param monzo Array or prime exponents, possibly fractional. * @returns The size of the represented number in cents (1200ths of an octave). */ function monzoToCents(monzo) { const result = (0, number_array_1.dotPrecise)(monzo, primes_1.PRIME_CENTS); if (Math.abs(result) > 10) { return result; } for (const component of monzo) { if (!Number.isInteger(component)) { return result; } } let { numerator, denominator } = (0, monzo_1.monzoToBigNumeratorDenominator)(monzo); let delta = numerator - denominator; // The answer is smaller than 10 cents so no need to check delta here or worry about its sign while (denominator >= IEEE_LIMIT) { delta >>= 1n; denominator >>= 1n; } return Math.log1p(Number(delta) / Number(denominator)) * NATS_TO_CENTS; } exports.monzoToCents = monzoToCents; /** * Given fraction p/q calculate log(abs(p*q)). * @param value Rational number or an array of its prime exponents. * @returns The Tenney-height of the number. */ function tenneyHeight(value) { if (Array.isArray(value)) { return (0, number_array_1.dotPrecise)(value.map(x => Math.abs(x)), primes_1.LOG_PRIMES); } const { s, n, d } = new fraction_1.Fraction(value); if (!s) { return Infinity; } return Math.log(n) + Math.log(d); } exports.tenneyHeight = tenneyHeight; /** * Given fraction p/q calculate sopfr(p) + sopfr(q), ignoring sign. * @param value Rational number, an array of its prime exponents or a `Map` of its prime exponents. * @returns Sum of prime factors with repetition of p*q. */ function wilsonHeight(value) { if (Array.isArray(value)) { return (0, number_array_1.dot)(value.map(x => Math.abs(x)), primes_1.PRIMES); } if (!(value instanceof Map)) { value = (0, monzo_1.primeFactorize)(value); } if (value.has(0)) { return Infinity; } value.delete(-1); let result = 0; for (const [prime, exponent] of value) { result += Math.abs(exponent) * prime; } return result; } exports.wilsonHeight = wilsonHeight; //# sourceMappingURL=index.js.map