UNPKG

xen-dev-utils

Version:

Utility functions used by the Scale Workshop ecosystem

307 lines 13.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.approximateRadical = exports.continuedFraction = exports.approximatePrimeLimit = exports.approximatePrimeLimitWithErrors = exports.approximateOddLimit = exports.approximateOddLimitWithErrors = exports.getConvergents = void 0; const conversion_1 = require("./conversion"); const fraction_1 = require("./fraction"); const primes_1 = require("./primes"); /** * Calculate best rational approximations to a given fraction that are * closer than any approximation with a smaller or equal denominator * unless non-monotonic approximations are requested as well. * @param value The fraction to simplify. * @param maxDenominator Maximum denominator to include. * @param maxLength Maximum length of the array of approximations. * @param includeSemiconvergents Include semiconvergents. * @param includeNonMonotonic Include non-monotonically improving approximations. * @returns An array of (semi)convergents. */ function getConvergents(value, maxDenominator, maxLength, includeSemiconvergents = false, includeNonMonotonic = false) { const value_ = new fraction_1.Fraction(value); /* Glossary cfDigit : the continued fraction digit num : the convergent numerator den : the convergent denominator scnum : the semiconvergent numerator scden : the semiconvergen denominator cind : tracks indicies of convergents */ const result = []; const cf = value_.toContinued(); const cind = []; for (let d = 0; d < cf.length; d++) { const cfDigit = cf[d]; let num = cfDigit; let den = 1; // Calculate the convergent. for (let i = d; i > 0; i--) { [den, num] = [num, den]; num += den * cf[i - 1]; } if (includeSemiconvergents && d > 0) { const lowerBound = includeNonMonotonic ? 1 : Math.ceil(cfDigit / 2); for (let i = lowerBound; i < cfDigit; i++) { const scnum = num - (cfDigit - i) * result[cind[d - 1]].n; const scden = den - (cfDigit - i) * result[cind[d - 1]].d; if (scden > maxDenominator) break; const convergent = new fraction_1.Fraction(scnum, scden); if (includeNonMonotonic) { result.push(convergent); } else { if (2 * i > cfDigit) { result.push(convergent); } else { // See https://en.wikipedia.org/wiki/Continued_fraction#Semiconvergents // for the origin of this half-rule try { const halfRule = convergent .sub(value_) .abs() .compare(result[result.length - 1].sub(value_).abs()) < 0; if (halfRule) { result.push(convergent); } } catch { } } } if (result.length >= maxLength) { return result; } } } if (den > maxDenominator) break; cind.push(result.length); result.push(new fraction_1.Fraction(num, den)); if (result.length >= maxLength) { return result; } } return result; } exports.getConvergents = getConvergents; // Cache of odd limit fractions. Expanded as necessary. const ODD_FRACTIONS = [new fraction_1.Fraction(1), new fraction_1.Fraction(1, 3), new fraction_1.Fraction(3)]; const ODD_CENTS = [0, -primes_1.PRIME_CENTS[1], primes_1.PRIME_CENTS[1]]; const ODD_BREAKPOINTS = [1, 3]; const TWO = new fraction_1.Fraction(2); /** * Approximate a musical interval by ratios of which neither the numerator or denominator * exceeds a specified limit, once all powers of 2 are removed. * @param cents Size of the musical interval measured in cents. * @param limit Maximum odd limit. * @returns All odd limit fractions within 600 cents of the input value sorted by closeness with cent offsets attached. */ function approximateOddLimitWithErrors(cents, limit) { const breakpointIndex = (limit - 1) / 2; // Expand cache. while (ODD_BREAKPOINTS.length <= breakpointIndex) { const newLimit = ODD_BREAKPOINTS.length * 2 + 1; for (let numerator = 1; numerator <= newLimit; numerator += 2) { for (let denominator = 1; denominator <= newLimit; denominator += 2) { const fraction = new fraction_1.Fraction(numerator, denominator); let novel = true; for (let i = 0; i < ODD_FRACTIONS.length; ++i) { if (fraction.equals(ODD_FRACTIONS[i])) { novel = false; break; } } if (novel) { ODD_FRACTIONS.push(fraction); ODD_CENTS.push((0, conversion_1.valueToCents)(fraction.valueOf())); } } } ODD_BREAKPOINTS.push(ODD_FRACTIONS.length); } // Find closest odd limit fractions modulo octaves. const results = []; for (let i = 0; i < ODD_BREAKPOINTS[breakpointIndex]; ++i) { const oddCents = ODD_CENTS[i]; const remainder = (0, fraction_1.mmod)(cents - oddCents, 1200); // Overshot if (remainder <= 600) { // Rounding done to eliminate floating point jitter. const exponent = Math.round((cents - oddCents - remainder) / 1200); const error = remainder; // Exponentiate to add the required number of octaves. results.push([ODD_FRACTIONS[i].mul(TWO.pow(exponent)), error]); } // Undershot else { const exponent = Math.round((cents - oddCents - remainder) / 1200) + 1; const error = 1200 - remainder; results.push([ODD_FRACTIONS[i].mul(TWO.pow(exponent)), error]); } } results.sort((a, b) => a[1] - b[1]); return results; } exports.approximateOddLimitWithErrors = approximateOddLimitWithErrors; /** * Approximate a musical interval by ratios of which neither the numerator or denominator * exceeds a specified limit, once all powers of 2 are removed. * @param cents Size of the musical interval measured in cents. * @param limit Maximum odd limit. * @returns All odd limit fractions within 600 cents of the input value sorted by closeness. */ function approximateOddLimit(cents, limit) { return approximateOddLimitWithErrors(cents, limit).map(result => result[0]); } exports.approximateOddLimit = approximateOddLimit; /** * Approximate a musical interval by ratios of which are within a prime limit with * exponents that do not exceed the maximimum, exponent of 2 ignored. * @param cents Size of the musical interval measured in cents. * @param limitIndex The ordinal of the prime of the limit. * @param maxError Maximum error from the interval for inclusion in the result. * @param maxLength Maximum number of approximations to return. * @returns All valid fractions within `maxError` cents of the input value sorted by closeness with cent offsets attached. */ function approximatePrimeLimitWithErrors(cents, limitIndex, maxExponent, maxError = 600, maxLength = 100) { if (maxError > 600) { throw new Error('Maximum search distance is 600 cents'); } const results = []; function push(error, result) { if (!result.n) { return; } if (results.length < maxLength) { results.push([result, error]); if (results.length === maxLength) { results.sort((a, b) => a[1] - b[1]); } } else { if (error > results[results.length - 1][1]) { return; } for (let index = results.length - 1;; index--) { if (error <= results[index][1]) { results.splice(index, 0, [result, error]); break; } } results.pop(); } } function accumulate(approximation, approximationCents, index) { if (approximation.n > 10e10 || approximation.d > 10e10) { return; } if (index > limitIndex) { // Procedure is the same as in approximateOddLimitWithErrors const remainder = (0, fraction_1.mmod)(cents - approximationCents, 1200); if (remainder <= 600) { const error = remainder; if (error > maxError) { return; } const exponent = Math.round((cents - approximationCents - remainder) / 1200); if (Math.abs(exponent) > 52) { return; } const result = approximation.mul(TWO.pow(exponent)); push(error, result); } else { const error = 1200 - remainder; if (error > maxError) { return; } const exponent = Math.round((cents - approximationCents - remainder) / 1200) + 1; if (Math.abs(exponent) > 52) { return; } const result = approximation.mul(TWO.pow(exponent)); push(error, result); } return; } accumulate(approximation, approximationCents, index + 1); for (let i = 1; i <= maxExponent; ++i) { accumulate(approximation.mul(primes_1.PRIMES[index] ** i), approximationCents + primes_1.PRIME_CENTS[index] * i, index + 1); accumulate(approximation.div(primes_1.PRIMES[index] ** i), approximationCents - primes_1.PRIME_CENTS[index] * i, index + 1); } } accumulate(new fraction_1.Fraction(1), 0, 1); results.sort((a, b) => a[1] - b[1]); return results; } exports.approximatePrimeLimitWithErrors = approximatePrimeLimitWithErrors; /** * Approximate a musical interval by ratios of which are within a prime limit with * exponents that do not exceed the maximimum, exponent of 2 ignored. * @param cents Size of the musical interval measured in cents. * @param limitIndex The ordinal of the prime of the limit. * @param maxError Maximum error from the interval for inclusion in the result. * @param maxLength Maximum number of approximations to return. * @returns All valid fractions within `maxError` cents of the input value sorted by closenesss. */ function approximatePrimeLimit(cents, limitIndex, maxExponent, maxError = 600, maxLength = 100) { return approximatePrimeLimitWithErrors(cents, limitIndex, maxExponent, maxError, maxLength).map(result => result[0]); } exports.approximatePrimeLimit = approximatePrimeLimit; /** * Calculate an array of continued fraction elements representing the value. * https://en.wikipedia.org/wiki/Continued_fraction * @param value Value to turn into a continued fraction. * @returns An array of continued fraction elements. */ function continuedFraction(value) { const result = []; let coeff = Math.floor(value); result.push(coeff); value -= coeff; while (value && coeff < 1e12 && result.length < 32) { value = 1 / value; coeff = Math.floor(value); result.push(coeff); value -= coeff; } return result; } exports.continuedFraction = continuedFraction; /** * Approximate a value with a radical expression. * @param value Value to approximate. * @param maxIndex Maximum index of the radical. 2 means square root, 3 means cube root, etc. * @param maxHeight Maximum Benedetti height of the radicand in the approximation. * @returns Object with index of the radical and the radicand. Result is "index'th root or radicand". */ function approximateRadical(value, maxIndex = 5, maxHeight = 50000) { let index = 1; let radicand = new fraction_1.Fraction(1); let bestError = Math.abs(value - 1); for (let i = 1; i <= maxIndex; ++i) { const cf = continuedFraction(value ** i); let candidate; for (let j = 0; j < cf.length - 1; ++j) { let convergent = new fraction_1.Fraction(cf[j]); for (let k = j - 1; k >= 0; --k) { convergent = convergent.inverse().add(cf[k]); } if (convergent.n * convergent.d > maxHeight) { break; } candidate = convergent; } if (candidate !== undefined) { const error = Math.abs(candidate.valueOf() ** (1 / i) - value); if (error < bestError) { index = i; radicand = candidate; bestError = error; } } } return { index, radicand }; } exports.approximateRadical = approximateRadical; //# sourceMappingURL=approximation.js.map