UNPKG

xen-dev-utils

Version:

Utility functions used by the Scale Workshop ecosystem

566 lines 22.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const index_1 = require("../index"); const FUZZ = 'FUZZ' in process.env; (0, vitest_1.describe)('Array equality tester', () => { (0, vitest_1.it)('works on integer arrays', () => { (0, vitest_1.expect)((0, index_1.arraysEqual)([1, 2, 3], [1, 2, 3])).toBeTruthy(); (0, vitest_1.expect)((0, index_1.arraysEqual)([1, 2, 3], [1, 2, 3, 4])).toBeFalsy(); (0, vitest_1.expect)((0, index_1.arraysEqual)([1, 2], [1, 2, 3])).toBeFalsy(); }); }); (0, vitest_1.describe)('extended Euclidean algorithm', () => { (0, vitest_1.it)('finds the Bézout coefficients for 15 and 42', () => { const a = 15; const b = 42; const result = (0, index_1.extendedEuclid)(15, 42); (0, vitest_1.expect)(a * result.coefA + b * result.coefB).toBe((0, index_1.gcd)(a, b)); (0, vitest_1.expect)(result.gcd).toBe((0, index_1.gcd)(a, b)); (0, vitest_1.expect)((0, index_1.div)(a, (0, index_1.gcd)(a, b))).toBe(result.quotientA); (0, vitest_1.expect)((0, index_1.div)(b, (0, index_1.gcd)(a, b))).toBe(result.quotientB); }); }); (0, vitest_1.describe)('iterated (extended) Euclidean algorithm', () => { (0, vitest_1.it)('works for a bunch of random numbers', () => { const l = Math.floor(Math.random() * 10); const params = []; for (let i = 0; i < l; ++i) { params.push(Math.floor(Math.random() * 100)); } const coefs = (0, index_1.iteratedEuclid)(params); if (!params.length) { (0, vitest_1.expect)(coefs.length).toBe(0); } else { (0, vitest_1.expect)(params.map((p, i) => p * coefs[i]).reduce((a, b) => a + b)).toBe(params.reduce(index_1.gcd)); } }); }); (0, vitest_1.describe)('binomial coefficient', () => { (0, vitest_1.it)('tells you how many ways you can pick d unique elements out of n', () => { const n = 7; const d = 3; let numSubsets = 0; // This is d levels deep for (let i = 0; i < n; ++i) { for (let j = i + 1; j < n; ++j) { for (let k = j + 1; k < n; ++k) { numSubsets++; } } } (0, vitest_1.expect)(numSubsets).toBe((0, index_1.binomial)(n, d)); }); (0, vitest_1.it)('calculates 11 choose 7', () => { (0, vitest_1.expect)((0, index_1.binomial)(11, 7)).toBe(330); }); }); (0, vitest_1.describe)('Dot product', () => { (0, vitest_1.it)('can be used with all number arrays', () => { const a = new Float32Array([1, 2, 3, 4]); const b = new Int8Array([5, 6, 7]); (0, vitest_1.expect)((0, index_1.dot)(a, b)).toBe(38); (0, vitest_1.expect)((0, index_1.dot)(b, a)).toBe(38); }); }); (0, vitest_1.describe)('Value clamper', () => { (0, vitest_1.it)('works for lower bounds', () => { const value = -123.4; const clamped = (0, index_1.clamp)(0, 128, value); (0, vitest_1.expect)(clamped).toBe(0); }); (0, vitest_1.it)('works for upper bounds', () => { const value = 13881.818; const clamped = (0, index_1.clamp)(0, 12800, value); (0, vitest_1.expect)(clamped).toBe(12800); }); }); (0, vitest_1.describe)('Norm', () => { (0, vitest_1.it)('calculates an euclidean norm (float32)', () => { const a = new Float32Array([-3, 4]); (0, vitest_1.expect)((0, index_1.norm)(a)).toBeCloseTo(5); }); (0, vitest_1.it)('calculates a taxicab norm (int8)', () => { const a = new Int8Array([3, -4]); (0, vitest_1.expect)((0, index_1.norm)(a, 'taxicab')).toBeCloseTo(7); }); (0, vitest_1.it)('calculates a max norm (number[])', () => { const a = [-3, -4]; (0, vitest_1.expect)((0, index_1.norm)(a, 'maximum')).toBeCloseTo(4); }); }); (0, vitest_1.describe)('Pitch difference with circle equivalence', () => { (0, vitest_1.it)('calculates the difference between 700.0 and 701.955', () => { const diff = (0, index_1.circleDifference)(700.0, 701.955); (0, vitest_1.expect)(diff).toBeCloseTo(-1.955); }); (0, vitest_1.it)('calculates the octave-equivalent difference between 5/1 and 4\\12', () => { const diff = (0, index_1.circleDifference)((0, index_1.valueToCents)(5), 400.0); (0, vitest_1.expect)(diff).toBeCloseTo(-13.686); }); (0, vitest_1.it)('calculates the tritave-equivalent difference between 5/1 and 13/1', () => { const diff = (0, index_1.circleDifference)((0, index_1.valueToCents)(5), (0, index_1.valueToCents)(13), (0, index_1.valueToCents)(3)); (0, vitest_1.expect)(diff).toBeCloseTo(247.741); }); }); (0, vitest_1.describe)('Pitch distance with circle equivalence', () => { (0, vitest_1.it)('calculates the distance between 700.0 and 701.955', () => { const diff = (0, index_1.circleDistance)(700.0, 701.955); (0, vitest_1.expect)(diff).toBeCloseTo(1.955); }); (0, vitest_1.it)('calculates the octave-equivalent distance between 5/1 and 4\\12', () => { const diff = (0, index_1.circleDistance)((0, index_1.valueToCents)(5), 400.0); (0, vitest_1.expect)(diff).toBeCloseTo(13.686); }); (0, vitest_1.it)('calculates the tritave-equivalent distance between 5/1 and 13/1', () => { const diff = (0, index_1.circleDistance)((0, index_1.valueToCents)(5), (0, index_1.valueToCents)(13), (0, index_1.valueToCents)(3)); (0, vitest_1.expect)(diff).toBeCloseTo(247.741); }); }); (0, vitest_1.describe)('Ceiling power of two', () => { (0, vitest_1.it)('works with small values', () => { const x = 1 + Math.random() * (2 ** 30 - 2); const p2 = (0, index_1.ceilPow2)(x); (0, vitest_1.expect)(x).toBeLessThanOrEqual(p2); (0, vitest_1.expect)(p2).toBeLessThan(2 * x); (0, vitest_1.expect)(Math.log2(p2)).toBeCloseTo(Math.round(Math.log2(p2))); }); (0, vitest_1.it)('works with tiny values', () => { const x = Math.random(); const p2 = (0, index_1.ceilPow2)(x); (0, vitest_1.expect)(x).toBeLessThanOrEqual(p2); (0, vitest_1.expect)(p2).toBeLessThan(2 * x); (0, vitest_1.expect)(Math.log2(p2)).toBeCloseTo(Math.round(Math.log2(p2))); }); (0, vitest_1.it)('works with large values', () => { const x = 2 ** 31 + Math.random() * 2 ** 37; const p2 = (0, index_1.ceilPow2)(x); (0, vitest_1.expect)(x).toBeLessThanOrEqual(p2); (0, vitest_1.expect)(p2).toBeLessThan(2 * x); (0, vitest_1.expect)(Math.log2(p2)).toBeCloseTo(Math.round(Math.log2(p2))); }); }); (0, vitest_1.describe)('Farey sequence generator', () => { (0, vitest_1.it)('generates all fractions with max denominator 6 between 0 and 1 inclusive', () => { const F6 = Array.from((0, index_1.fareySequence)(6)).map(f => f.toFraction()); (0, vitest_1.expect)(F6).toEqual([ '0', '1/6', '1/5', '1/4', '1/3', '2/5', '1/2', '3/5', '2/3', '3/4', '4/5', '5/6', '1', ]); }); (0, vitest_1.it)('agrees with the brute force method', () => { const everything = new index_1.FractionSet(); const N = Math.floor(Math.random() * 50) + 1; for (let d = 1; d <= N; ++d) { for (let n = 0; n <= d; ++n) { everything.add(new index_1.Fraction(n, d)); } } const brute = Array.from(everything); brute.sort((a, b) => a.compare(b)); const farey = (0, index_1.fareySequence)(N); for (const entry of brute) { const f = farey.next().value; (0, vitest_1.expect)(entry.equals(f)).toBe(true); } (0, vitest_1.expect)(farey.next().done).toBe(true); }); }); (0, vitest_1.describe)('Farey interior generator', () => { (0, vitest_1.it)('generates all fractions with max denominator 8 between 0 and 1 exclusive', () => { const Fi8 = Array.from((0, index_1.fareyInterior)(8)).map(f => f.toFraction()); (0, vitest_1.expect)(Fi8).toEqual([ '1/8', '1/7', '1/6', '1/5', '1/4', '2/7', '1/3', '3/8', '2/5', '3/7', '1/2', '4/7', '3/5', '5/8', '2/3', '5/7', '3/4', '4/5', '5/6', '6/7', '7/8', ]); }); (0, vitest_1.it)('agrees with the brute force method', () => { const everything = new index_1.FractionSet(); const N = Math.floor(Math.random() * 50) + 1; for (let d = 1; d <= N; ++d) { for (let n = 1; n < d; ++n) { everything.add(new index_1.Fraction(n, d)); } } const brute = Array.from(everything); brute.sort((a, b) => a.compare(b)); const farey = (0, index_1.fareyInterior)(N); for (const entry of brute) { const f = farey.next().value; (0, vitest_1.expect)(entry.equals(f)).toBe(true); } (0, vitest_1.expect)(farey.next().done).toBe(true); }); }); (0, vitest_1.describe)('Constant structure falsifier', () => { (0, vitest_1.it)('Rejects diatonic in 12-tone equal temperament with F-to-B against B-to-F', () => { const steps = [2, 4, 5, 7, 9, 11, 12]; const [[lowAug4, highAug4], [lowDim5, highDim5]] = (0, index_1.falsifyConstantStructure)(steps); // C = -1 // D = 0 // E = 1 (0, vitest_1.expect)(lowAug4).toBe(2); // F (0, vitest_1.expect)(highAug4).toBe(5); // B (0, vitest_1.expect)(lowDim5).toBe(5); // B (0, vitest_1.expect)(highDim5 % 7).toBe(2); // F }); (0, vitest_1.it)('Accepts diatonic in 19-tone equal temperament', () => { const steps = [3, 6, 8, 11, 14, 17, 19]; (0, vitest_1.expect)((0, index_1.falsifyConstantStructure)(steps)).toBe(null); }); (0, vitest_1.it)("Produces Zarlino's sequence in 311-tone equal temperament", () => { const sizes = []; const gs = [100, 182]; for (let i = 3; i < 60; ++i) { const zarlino = [...gs]; zarlino.push(311); zarlino.sort((a, b) => a - b); if ((0, index_1.falsifyConstantStructure)(zarlino) === null) { sizes.push(i); } const last = gs[gs.length - 1]; if (i & 1) { gs.push((last + 100) % 311); } else { gs.push((last + 82) % 311); } } (0, vitest_1.expect)(sizes).toEqual([3, 4, 7, 10, 17, 34, 58]); }); (0, vitest_1.it)('Accepts the empty scale', () => { (0, vitest_1.expect)((0, index_1.falsifyConstantStructure)([])).toBe(null); }); (0, vitest_1.it)('Accepts the trivial scale', () => { (0, vitest_1.expect)((0, index_1.falsifyConstantStructure)([1])).toBe(null); }); (0, vitest_1.it)('Rejects a scale with a repeated step (early)', () => { (0, vitest_1.expect)((0, index_1.falsifyConstantStructure)([0, 1200])).toEqual([ [-1, 1], [0, 1], ]); }); (0, vitest_1.it)('Rejects a scale with a repeated step (late)', () => { (0, vitest_1.expect)((0, index_1.falsifyConstantStructure)([1200, 1200])).toEqual([ [-1, 1], [-1, 2], ]); }); }); (0, vitest_1.describe)('Constant structure checker with a margin of equivalence', () => { (0, vitest_1.it)('Rejects diatonic in 12-tone equal temperament (zero margin)', () => { const scaleCents = [200, 400, 500, 700, 900, 1100, 1200]; (0, vitest_1.expect)((0, index_1.hasMarginConstantStructure)(scaleCents, 0)).toBe(false); }); (0, vitest_1.it)('Accepts diatonic in 19-tone equal temperament (margin of 1 cent)', () => { const scaleCents = [189.5, 378.9, 505.3, 694.7, 884.2, 1073.7, 1200]; (0, vitest_1.expect)((0, index_1.hasMarginConstantStructure)(scaleCents, 1)).toBe(true); }); const zarlino = [386.313714, 701.955001]; for (let i = 0; i < 31; ++i) { const last = zarlino[zarlino.length - 1]; if (i & 1) { zarlino.push((last + 315.641287) % 1200); } else { zarlino.push((last + 386.313714) % 1200); } } zarlino.sort((a, b) => a - b); zarlino.push(1200); (0, vitest_1.it)('Accepts Zarlino[34] with a margin of 1 cent', () => { (0, vitest_1.expect)((0, index_1.hasMarginConstantStructure)(zarlino, 1)).toBe(true); }); (0, vitest_1.it)('Rejects Zarlino[34] with a margin of 2 cents', () => { (0, vitest_1.expect)((0, index_1.hasMarginConstantStructure)(zarlino, 2)).toBe(false); }); (0, vitest_1.it)('Accepts the empty scale', () => { (0, vitest_1.expect)((0, index_1.hasMarginConstantStructure)([], 0)).toBe(true); }); (0, vitest_1.it)('Accepts the trivial scale', () => { (0, vitest_1.expect)((0, index_1.hasMarginConstantStructure)([1200], 0)).toBe(true); }); (0, vitest_1.it)('Rejects a scale with a comma step (early)', () => { (0, vitest_1.expect)((0, index_1.hasMarginConstantStructure)([1, 1200], 2)).toBe(false); }); (0, vitest_1.it)('Rejects a scale with a comma step (late)', () => { (0, vitest_1.expect)((0, index_1.hasMarginConstantStructure)([1199, 1200], 2)).toBe(false); }); }); (0, vitest_1.describe)('Monzo size measure', () => { (0, vitest_1.it)('calculates the size of the perfect fourth accurately', () => { (0, vitest_1.expect)((0, index_1.monzoToCents)([2, -1])).toBeCloseTo(498.0449991346125, 12); }); (0, vitest_1.it)('calculates the size of the rascal accurately', () => { (0, vitest_1.expect)((0, index_1.monzoToCents)([-7470, 2791, 1312])).toBeCloseTo(5.959563411893381e-6, 24); }); (0, vitest_1.it)('calculates the size of the neutrino accurately', () => { (0, vitest_1.expect)((0, index_1.monzoToCents)([1889, -2145, 138, 424])).toBeCloseTo(1.6361187484440885e-10, 24); }); (0, vitest_1.it)('calculates the size of the demiquartervice comma accurately', () => { (0, vitest_1.expect)((0, index_1.monzoToCents)([-3, 2, -1, -1, 0, 0, -1, 0, 2])).toBeCloseTo(0.3636664332386927, 15); }); (0, vitest_1.it)('calculates the size of the negative junebug comma accurately', () => { (0, vitest_1.expect)((0, index_1.monzoToCents)([-1, 1, -1, -1, 1, -1, -1, 1, 1, -1, 1])).toBeCloseTo(-6.104006661651758, 15); }); }); (0, vitest_1.describe)('Tenney complexity measure', () => { (0, vitest_1.it)('calculates the complexity of 88', () => { (0, vitest_1.expect)((0, index_1.tenneyHeight)(88)).toBeCloseTo(4.477); }); (0, vitest_1.it)('calculates the complexity of 11/8', () => { (0, vitest_1.expect)((0, index_1.tenneyHeight)('11/8')).toBeCloseTo(4.477); }); (0, vitest_1.it)('calculates the complexity of -11/8', () => { (0, vitest_1.expect)((0, index_1.tenneyHeight)('-11/8')).toBeCloseTo(4.477); }); (0, vitest_1.it)('calculates the complexity of 8/11', () => { (0, vitest_1.expect)((0, index_1.tenneyHeight)([3, 0, 0, 0, -1])).toBeCloseTo(4.477); }); (0, vitest_1.it)('has a value for zero', () => { (0, vitest_1.expect)((0, index_1.tenneyHeight)(0)).toBe(Infinity); }); vitest_1.it.skip('fuzzes the fraction property', () => { for (let i = 0; i < 100000; ++i) { const n = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); const d = Math.floor(Math.random() * (Number.MAX_SAFE_INTEGER - 1)) + 1; const f = new index_1.Fraction(n, d); (0, vitest_1.expect)((0, index_1.tenneyHeight)(f.n) + (0, index_1.tenneyHeight)(f.d)).toBeCloseTo((0, index_1.tenneyHeight)(f)); } }); vitest_1.it.skip('fuzzes the multiplicative property', () => { for (let i = 0; i < 100000; ++i) { const x = Math.floor(Math.random() * 94906265); const y = Math.floor(Math.random() * 94906265); (0, vitest_1.expect)((0, index_1.tenneyHeight)(x) + (0, index_1.tenneyHeight)(y)).toBeCloseTo((0, index_1.tenneyHeight)(x * y)); } }); }); (0, vitest_1.describe)('Wilson complexity measure', () => { (0, vitest_1.it)('calculates the complexity of 88', () => { (0, vitest_1.expect)((0, index_1.wilsonHeight)(88)).toBe(17); }); (0, vitest_1.it)('calculates the complexity of 11/8', () => { (0, vitest_1.expect)((0, index_1.wilsonHeight)('11/8')).toBe(17); }); (0, vitest_1.it)('calculates the complexity of -11/8', () => { (0, vitest_1.expect)((0, index_1.wilsonHeight)('-11/8')).toBe(17); }); (0, vitest_1.it)('calculates the complexity of 8/11', () => { (0, vitest_1.expect)((0, index_1.wilsonHeight)([3, 0, 0, 0, -1])).toBe(17); }); (0, vitest_1.it)('calculates the complexity of -8/11', () => { (0, vitest_1.expect)((0, index_1.wilsonHeight)(new Map([ [-1, 1], [2, 3], [11, -1], ]))).toBe(17); }); (0, vitest_1.it)('has a value for zero', () => { (0, vitest_1.expect)((0, index_1.wilsonHeight)(0)).toBe(Infinity); }); vitest_1.it.each([ ['2/1', 2], ['3/2', 5], ['4/3', 7], ['5/4', 9], ['6/5', 10], ['7/6', 12], ['9/8', 12], ['8/7', 13], ['10/9', 13], ['16/15', 16], ['15/14', 17], ['11/10', 18], ['12/11', 18], ['21/20', 19], ['25/24', 19], ['13/12', 20], ['28/27', 20], ['14/13', 22], ['36/35', 22], ['22/21', 23], ['27/26', 24], ['33/32', 24], ['17/16', 25], ['18/17', 25], ['26/25', 25], ['49/48', 25], ['64/63', 25], ['81/80', 25], ['45/44', 26], ['50/49', 26], ['19/18', 27], ['40/39', 27], ['55/54', 27], ['20/19', 28], ['56/55', 29], ['65/64', 30], ['35/34', 31], ['100/99', 31], ['24/23', 32], ['51/50', 32], ['34/33', 33], ['91/90', 33], ['99/98', 33], ['66/65', 34], ['57/56', 35], ['23/22', 36], ['46/45', 36], ['76/75', 36], ['78/77', 36], ['85/84', 36], ['39/38', 37], ['52/51', 37], ['96/95', 37], ['30/29', 39], ['29/28', 40], ['70/69', 40], ['31/30', 41], ['32/31', 41], ['77/76', 41], ['63/62', 46], ['37/36', 47], ['69/68', 47], ['92/91', 47], ['88/87', 49], ['41/40', 52], ['75/74', 52], ['42/41', 53], ['58/57', 53], ['43/42', 55], ['82/81', 55], ['38/37', 58], ['44/43', 58], ['48/47', 58], ['93/92', 61], ['54/53', 64], ['86/85', 67], ['53/52', 70], ['60/59', 71], ['47/46', 72], ['61/60', 73], ['95/94', 73], ['87/86', 77], ['67/66', 83], ['72/71', 83], ['94/93', 83], ['71/70', 85], ['73/72', 85], ['68/67', 88], ['59/58', 90], ['80/79', 92], ['62/61', 94], ['79/78', 97], ['84/83', 97], ['90/89', 102], ['89/88', 106], ['97/96', 110], ['74/73', 112], ['98/97', 113], ['83/82', 126], ])('Agrees with XenWiki on %s ~ %s', (fraction, height) => { (0, vitest_1.expect)((0, index_1.wilsonHeight)(fraction)).toBe(height); }); vitest_1.it.runIf(FUZZ)('fuzzes the fraction property', () => { for (let i = 0; i < 100; ++i) { const n = Math.floor(Math.random() * 1073741823); const d = Math.floor(Math.random() * 1073741822) + 1; const f = new index_1.Fraction(n, d); (0, vitest_1.expect)((0, index_1.wilsonHeight)(f.n) + (0, index_1.wilsonHeight)(f.d)).toBe((0, index_1.wilsonHeight)(f)); } }); vitest_1.it.runIf(FUZZ)('fuzzes the multiplicative property', () => { for (let i = 0; i < 10000; ++i) { const x = Math.floor(Math.random() * 32768); const y = Math.floor(Math.random() * 32768); (0, vitest_1.expect)((0, index_1.wilsonHeight)(x) + (0, index_1.wilsonHeight)(y), `Failed on ${x} * ${y}`).toBe((0, index_1.wilsonHeight)(x * y)); } }); }); (0, vitest_1.describe)('Modular inverse calculator', () => { (0, vitest_1.it)('finds modular inverses when they exist', () => { for (let a = -30; a < 30; ++a) { for (let b = 2; b < 20; ++b) { const c = Math.abs((0, index_1.gcd)(a, b)); if (c !== 1) { (0, vitest_1.expect)(() => (0, index_1.modInv)(a, b)).toThrow(); } else { (0, vitest_1.expect)((0, index_1.mmod)(a * (0, index_1.modInv)(a, b), b)).toBe(1); } } } }); (0, vitest_1.it)("finds the next best alternative even if a true modular inverse doesn't exist", () => { for (let a = -30; a < 30; ++a) { for (let b = 1; b < 20; ++b) { let c = Math.abs((0, index_1.gcd)(a, b)); if (c === b) { c = 0; } (0, vitest_1.expect)((0, index_1.mmod)(a * (0, index_1.modInv)(a, b, false), b)).toBe(c); } } }); (0, vitest_1.it)('can be used to break 15edo into 3 interleaved circles of fifths', () => { const edo = 15; const gen = 9; const genInv = (0, index_1.modInv)(gen, edo, false); const numCycles = (0, index_1.mmod)(gen * genInv, edo); function f(step) { const c = (0, index_1.mmod)(step, numCycles); return (0, index_1.mmod)((step - c) * genInv, edo) + c; } (0, vitest_1.expect)([...Array(16).keys()].map(f)).toEqual([ 0, // Root 1, // Root + 1 2, // Root + 2 6, // Gen * 2 7, // +1 8, // +2 12, // Gen * 4 13, // +1 14, // +2 3, // Gen 4, // Gen + 1 5, // Gen + 1 9, // Gen * 3 10, // +1 11, // +2 0, // Circle closes ]); }); }); //# sourceMappingURL=index.spec.js.map