xen-dev-utils
Version:
Utility functions used by the Scale Workshop ecosystem
473 lines • 16.6 kB
JavaScript
;
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