From 52ade080291557728614c4cc5003438403744b04 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Sun, 1 Jan 2023 19:43:40 +0200 Subject: [PATCH] WIP: Implement lightweight tempering algorithms for large subgroups ref #397 --- package-lock.json | 151 +++++++++++++++++++++++++++----- package.json | 5 +- src/__tests__/tempering.spec.ts | 80 ++++++++++++++++- src/tempering.ts | 119 +++++++++++++++++++++++++ 4 files changed, 330 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad75e6b2..4e5a099a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "2.0.1", "dependencies": { "jszip": "^3.10.1", + "mathjs": "^11.5.0", "moment-of-symmetry": "github:xenharmonic-devs/moment-of-symmetry#v0.2.1", "qs": "^6.11.0", "scale-workshop-core": "github:xenharmonic-devs/scale-workshop-core#v0.0.1", @@ -53,11 +54,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", - "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", "dependencies": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.13.11" }, "engines": { "node": ">=6.9.0" @@ -1374,6 +1375,18 @@ "node": ">=4.0.0" } }, + "node_modules/complex.js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz", + "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1566,10 +1579,9 @@ } }, "node_modules/decimal.js": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", - "integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==", - "dev": true + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, "node_modules/deep-eql": { "version": "3.0.1", @@ -2041,6 +2053,11 @@ "node": ">=12" } }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3262,6 +3279,11 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==" + }, "node_modules/jazz-midi": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/jazz-midi/-/jazz-midi-1.7.6.tgz", @@ -3666,6 +3688,28 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, + "node_modules/mathjs": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.5.0.tgz", + "integrity": "sha512-vJ/+SqWtxjW6/aeDRt8xL3TlOVKqwN15BIyTGVqGbIWuiqgY4SxZ0yLuna82YH9CB757iFP7uJ4m3KvVBX7Qcg==", + "dependencies": { + "@babel/runtime": "^7.20.6", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^4.2.0", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.1.0" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4222,9 +4266,9 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regexpp": { "version": "3.2.0", @@ -4416,6 +4460,11 @@ "xen-dev-utils": "github:xenharmonic-devs/xen-dev-utils#v0.1.1" } }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -4770,6 +4819,11 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, "node_modules/tinypool": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.1.3.tgz", @@ -4920,6 +4974,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-function": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.0.tgz", + "integrity": "sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg==", + "engines": { + "node": ">= 14" + } + }, "node_modules/typescript": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", @@ -5397,11 +5459,11 @@ "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==" }, "@babel/runtime": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", - "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", "requires": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.13.11" } }, "@colors/colors": { @@ -6388,6 +6450,11 @@ "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "dev": true }, + "complex.js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz", + "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6550,10 +6617,9 @@ } }, "decimal.js": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", - "integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==", - "dev": true + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, "deep-eql": { "version": "3.0.1", @@ -6820,6 +6886,11 @@ "dev": true, "optional": true }, + "escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7707,6 +7778,11 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==" + }, "jazz-midi": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/jazz-midi/-/jazz-midi-1.7.6.tgz", @@ -8031,6 +8107,22 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, + "mathjs": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.5.0.tgz", + "integrity": "sha512-vJ/+SqWtxjW6/aeDRt8xL3TlOVKqwN15BIyTGVqGbIWuiqgY4SxZ0yLuna82YH9CB757iFP7uJ4m3KvVBX7Qcg==", + "requires": { + "@babel/runtime": "^7.20.6", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^4.2.0", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.1.0" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -8432,9 +8524,9 @@ } }, "regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regexpp": { "version": "3.2.0", @@ -8560,6 +8652,11 @@ "xen-dev-utils": "github:xenharmonic-devs/xen-dev-utils#v0.1.1" } }, + "seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -8821,6 +8918,11 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, "tinypool": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.1.3.tgz", @@ -8934,6 +9036,11 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "typed-function": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.0.tgz", + "integrity": "sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg==" + }, "typescript": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", diff --git a/package.json b/package.json index f4f602de..dd738e45 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,15 @@ }, "dependencies": { "jszip": "^3.10.1", + "mathjs": "^11.5.0", "moment-of-symmetry": "github:xenharmonic-devs/moment-of-symmetry#v0.2.1", "qs": "^6.11.0", + "scale-workshop-core": "github:xenharmonic-devs/scale-workshop-core#v0.0.1", "temperaments": "github:xenharmonic-devs/temperaments#v0.3.0", "vue": "^3.2.33", "vue-router": "^4.1.5", "webmidi": "^3.0.21", - "xen-dev-utils": "github:xenharmonic-devs/xen-dev-utils#v0.1.0", - "scale-workshop-core": "github:xenharmonic-devs/scale-workshop-core#v0.0.1" + "xen-dev-utils": "github:xenharmonic-devs/xen-dev-utils#v0.1.0" }, "devDependencies": { "@rushstack/eslint-patch": "^1.2.0", diff --git a/src/__tests__/tempering.spec.ts b/src/__tests__/tempering.spec.ts index 9e600a0f..337feb8d 100644 --- a/src/__tests__/tempering.spec.ts +++ b/src/__tests__/tempering.spec.ts @@ -7,10 +7,88 @@ import { makeRank2FromVals, mosPatternsRank2FromCommas, mosPatternsRank2FromVals, + vanishCommas, + tenneyVals, } from "../tempering"; -import { arraysEqual, Fraction, valueToCents } from "xen-dev-utils"; +import { + arraysEqual, + dot, + Fraction, + PRIMES, + PRIME_CENTS, + toMonzoAndResidual, + valueToCents, +} from "xen-dev-utils"; import { ExtendedMonzo, Interval, Scale } from "scale-workshop-core"; +describe("Comma vanisher", () => { + it("can make the syntonic comma disappear (tempered octaves)", () => { + const inital = PRIME_CENTS.slice(0, 3); + const syntonic = [-4, 4, -1]; + const mapping = vanishCommas(inital, [syntonic]); + expect(dot(mapping, syntonic)).toBeCloseTo(0); + + const octave = mapping[0]; + expect(octave).not.toBe(1200); + expect(octave).toBeLessThan(1203); + expect(octave).toBeGreaterThan(1197); + }); + + it("can make the syntonic comma disappear (pure octaves)", () => { + const inital = PRIME_CENTS.slice(0, 3); + const syntonic = [-4, 4, -1]; + const mapping = vanishCommas(inital, [syntonic], false); + expect(dot(mapping, syntonic)).toBeCloseTo(0); + + const octave = mapping[0]; + expect(octave).toBe(1200); + + const twelfth = mapping[1]; + expect(twelfth).not.toBe(PRIME_CENTS[1]); + expect(twelfth).toBeLessThan(1905); + expect(twelfth).toBeGreaterThan(1896); + + const five = mapping[2]; + expect(five).not.toBe(PRIME_CENTS[2]); + expect(five).toBeLessThan(2788); + expect(five).toBeGreaterThan(2783); + }); + + it("can handle big subgroups (ratio units)", () => { + const initial = PRIMES.slice(0, 25); + const commas = [ + toMonzoAndResidual(new Fraction(621, 620), 25)[0], + toMonzoAndResidual(new Fraction(87, 86), 25)[0], + toMonzoAndResidual(new Fraction(98, 97), 25)[0], + ]; + const mapping = vanishCommas(initial, commas, false, "ratio"); + + for (const comma of commas) { + let interval = 1; + for (let i = 0; i < 25; ++i) { + interval *= mapping[i] ** comma[i]; + } + expect(interval).toBeCloseTo(1); + } + }); +}); + +describe("Tenney-Euclid optimal val combiner", () => { + it("calculates POTE meantone", () => { + const jip = PRIME_CENTS.slice(0, 3); + const twelve = [12, 19, 28]; + const nineteen = [19, 30, 44]; + + const mapping = tenneyVals(jip, [twelve, nineteen]); + + const syntonic = [-4, 4, -1]; + expect(dot(mapping, syntonic)).toBeCloseTo(0); + + const pote = mapping.map((m) => (1200 * m) / mapping[0]); + expect(dot(pote, [-1, 1, 0])).toBeCloseTo(696.239); + }); +}); + describe("Temperament Mapping", () => { it("calculates POTE meantone", () => { const mapping = Mapping.fromCommas(["81/80"], 3); diff --git a/src/tempering.ts b/src/tempering.ts index 96a6ac3a..1f3da517 100644 --- a/src/tempering.ts +++ b/src/tempering.ts @@ -8,16 +8,135 @@ import { type Weights, Subgroup, type JipOrLimit, + type Comma, + type PitchUnits, } from "temperaments"; import { DEFAULT_NUMBER_OF_COMPONENTS } from "./constants"; import { + centsToNats, + dot, + natsToCents, + natsToSemitones, PRIME_CENTS, + semitonesToNats, valueToCents, type FractionValue, type Monzo, } from "xen-dev-utils"; import { Interval, Scale } from "scale-workshop-core"; +import { pinv, multiply } from "mathjs"; + +/** + * Find a Tenney-Euclid optimal linear combination of vals. + * @param jip Just intonation point (JIP). + * @param vals Array of vals in using the JIP's semantics. + * @param weights Importance weighting for the JIP's coordinates (defaults to 1 / JIP). + * @param units The units pitch of intervals and mappings are measured in. + * @returns A Tenney-Euclid optimal mapping representing a temperament supported by all of the vals. + */ +export function tenneyVals( + jip: number[], + vals: Val[], + weights?: Weights, + units: PitchUnits = "cents" +) { + if (units === "cents") { + jip = jip.map(centsToNats); + } else if (units === "semitones") { + jip = jip.map(semitonesToNats); + } else if (units === "ratio") { + jip = jip.map(Math.log); + } + + if (weights === undefined) { + weights = jip.map((j) => 1 / j); + } + + vals = vals.map((val) => val.map((v, i) => v * weights![i])); + jip = jip.map((j, i) => j * weights![i]); + + const mapping = multiply(jip, multiply(pinv(vals), vals)).map( + (m, i) => m / weights![i] + ); + + if (units === "cents") { + return mapping.map(natsToCents); + } else if (units === "semitones") { + return mapping.map(natsToSemitones); + } else if (units === "ratio") { + return mapping.map(Math.exp); + } + return mapping; +} + +function norm(monzo: Monzo) { + let total = 0; + monzo.forEach((component) => { + total += component * component; + }); + return Math.sqrt(total); +} + +function scalarMul(scalar: number, monzo: Monzo) { + return monzo.map((component) => scalar * component); +} + +function subInPlace(a: Monzo, b: Monzo) { + for (let i = 0; i < a.length; ++i) { + a[i] -= b[i]; + } +} + +/** + * Adjust mapping until specified commas vanish. + * @param initial Initial guess for the optimal mapping. + * @param commas Array of commas in the mapping's semantics. + * @param temperEquaves If `true` tempering is applied to octaves as well. + * @param units The units pitch of intervals and mappings are measured in. + * @param numberOfIterations Number of iterations to adjust the mapping. + * @returns A close-by mapping where the commas vanish. + */ +export function vanishCommas( + initial: number[], + commas: Comma[], + temperEquaves = true, + units: PitchUnits = "cents", + numberOfIterations = 1000 +) { + // The process is linear and scale-free in log-space so the particular units don't matter. + if (units === "ratio") { + initial = initial.map(Math.log); + } + + const mapping = initial.slice(); + + const normalizedCommas = commas.map((comma) => + scalarMul(1 / norm(comma), comma) + ); + + for (let i = 0; i < numberOfIterations; ++i) { + for (const comma of normalizedCommas) { + const delta = scalarMul(dot(mapping, comma), comma); + if (!temperEquaves) { + delta[0] = 0; + } + subInPlace(mapping, delta); + } + } + + if (units === "ratio") { + return mapping.map(Math.exp); + } + return mapping; +} + +/* +function toPrimeMapping(mapping: number[], subgroup: Subgroup) { + // TODO +} +*/ + export class Mapping { vector: number[];