Skip to content

Commit

Permalink
Merge pull request #398 from xenharmonic-devs/lightweight-tempering
Browse files Browse the repository at this point in the history
Implement lightweight tempering algorithms for large subgroups
  • Loading branch information
frostburn authored Nov 26, 2023
2 parents 7728aa0 + 7741182 commit d1dd036
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 19 deletions.
33 changes: 32 additions & 1 deletion src/__tests__/tempering.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,40 @@ import {
makeRank2FromVals,
mosPatternsRank2FromCommas,
mosPatternsRank2FromVals,
toPrimeMapping,
} from "../tempering";
import { arraysEqual, Fraction, valueToCents } from "xen-dev-utils";
import {
arraysEqual,
Fraction,
PRIME_CENTS,
valueToCents,
} from "xen-dev-utils";
import { ExtendedMonzo, Interval, Scale } from "scale-workshop-core";
import { Subgroup } from "temperaments";
import { DEFAULT_NUMBER_OF_COMPONENTS } from "../constants";

describe("Prime map converter", () => {
it("does (almost) nothing in 5-limit JI", () => {
const fiveLimit = PRIME_CENTS.slice(0, 3);
const mapping = toPrimeMapping(fiveLimit, new Subgroup(5));
expect(mapping[0]).toBeCloseTo(fiveLimit[0]);
expect(mapping[1]).toBeCloseTo(fiveLimit[1]);
expect(mapping[2]).toBeCloseTo(fiveLimit[2]);
expect(mapping.length).toBe(DEFAULT_NUMBER_OF_COMPONENTS);
expect(mapping[3]).toBeCloseTo(PRIME_CENTS[3]);
});

it("converts a mapping in 2.3.13/5 to 2.3.5.7.11.13...", () => {
const original = [1200, 1901, 1654];
const mapping = toPrimeMapping(original, new Subgroup("2.3.13/5"));
expect(mapping[0]).toBeCloseTo(1200);
expect(mapping[1]).toBeCloseTo(1901);
expect(mapping[3]).toBeCloseTo(PRIME_CENTS[3]);
expect(mapping[4]).toBeCloseTo(PRIME_CENTS[4]);
expect(mapping[5] - mapping[2]).toBeCloseTo(1654);
expect(mapping[6]).toBeCloseTo(PRIME_CENTS[6]);
});
});

describe("Temperament Mapping", () => {
it("calculates POTE meantone", () => {
Expand Down
9 changes: 6 additions & 3 deletions src/components/modals/generation/RankTwo.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<script setup lang="ts">
import { DEFAULT_NUMBER_OF_COMPONENTS } from "@/constants";
import {
DEFAULT_NUMBER_OF_COMPONENTS,
MAX_INTERACTIVE_SUBGROUP_SIZE,
} from "@/constants";
import {
makeRank2FromVals,
makeRank2FromCommas,
Expand Down Expand Up @@ -105,7 +108,7 @@ const [mosPatterns, mosPatternsError] = computedAndError(() => {
return [];
}
// Huge subgroups get too expensive to evaluate interactively
if (subgroup.value.basis.length > 6) {
if (subgroup.value.basis.length > MAX_INTERACTIVE_SUBGROUP_SIZE) {
return expensiveMosPatterns.value;
}
return mosPatternsRank2FromVals(
Expand All @@ -121,7 +124,7 @@ const [mosPatterns, mosPatternsError] = computedAndError(() => {
return [];
}
// Huge subgroups get too expensive to evaluate interactively
if (subgroup.value.basis.length > 6) {
if (subgroup.value.basis.length > MAX_INTERACTIVE_SUBGROUP_SIZE) {
return expensiveMosPatterns.value;
}
return mosPatternsRank2FromCommas(
Expand Down
77 changes: 62 additions & 15 deletions src/components/modals/modification/TemperScale.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<script setup lang="ts">
import { DEFAULT_NUMBER_OF_COMPONENTS } from "@/constants";
import { Mapping, stretchToEdo } from "@/tempering";
import {
DEFAULT_NUMBER_OF_COMPONENTS,
MAX_GEO_SUBGROUP_SIZE,
} from "@/constants";
import { Mapping, stretchToEdo, toPrimeMapping } from "@/tempering";
import { computed, ref, watch } from "vue";
import Modal from "@/components/ModalDialog.vue";
import { makeState, splitText } from "@/components/modals/tempering-state";
import { add, Fraction, PRIME_CENTS } from "xen-dev-utils";
import { mapByVal, resolveMonzo } from "temperaments";
import { mapByVal, resolveMonzo, tenneyVals, vanishCommas } from "temperaments";
import {
ExtendedMonzo,
Interval,
Expand Down Expand Up @@ -48,6 +51,10 @@ const subgroup = state.subgroup;
const options = state.options;
const edoUnavailable = computed(() => vals.value.length !== 1);
const constraintsDisabled = computed(
() => subgroup.value.basis.length > MAX_GEO_SUBGROUP_SIZE
);
watch(subgroupError, (newValue) =>
subgroupInput.value!.setCustomValidity(newValue)
);
Expand Down Expand Up @@ -128,19 +135,57 @@ function modify() {
}
mapping = new Mapping(vector.slice(0, DEFAULT_NUMBER_OF_COMPONENTS));
} else if (method.value === "vals") {
mapping = Mapping.fromVals(
vals.value,
DEFAULT_NUMBER_OF_COMPONENTS,
subgroup.value,
options.value
);
if (constraintsDisabled.value) {
// Subgroup is too large to use geometric methods. Use O(n²) projection instead.
const weights = options.value.weights;
// True constraints are not supported so CTE is interpreted as POTE.
const temperEquaves =
options.value.temperEquaves && tempering.value !== "CTE";
const jip = subgroup.value.jip();
const valVectors = vals.value.map((val) =>
subgroup.value.fromWarts(val)
);
let mappingVector = tenneyVals(valVectors, jip, weights);
if (!temperEquaves) {
mappingVector = mappingVector.map(
(m) => (jip[0] * m) / mappingVector[0]
);
}
mapping = new Mapping(toPrimeMapping(mappingVector, subgroup.value));
} else {
mapping = Mapping.fromVals(
vals.value,
DEFAULT_NUMBER_OF_COMPONENTS,
subgroup.value,
options.value
);
}
} else {
mapping = Mapping.fromCommas(
commas.value,
DEFAULT_NUMBER_OF_COMPONENTS,
subgroup.value,
options.value
);
if (constraintsDisabled.value) {
// Subgroup is too large to use geometric methods. Use O(n) gradient descent instead.
// True constraints are not supported so CTE is interpreted as pure equaves.
const temperEquaves =
options.value.temperEquaves && tempering.value !== "CTE";
const weights = options.value.weights;
const jip = subgroup.value.jip();
const commaMonzos = commas.value.map(
(comma) => subgroup.value.toMonzoAndResidual(comma)[0]
);
const mappingVector = vanishCommas(
commaMonzos,
jip,
weights,
temperEquaves
);
mapping = new Mapping(toPrimeMapping(mappingVector, subgroup.value));
} else {
mapping = Mapping.fromCommas(
commas.value,
DEFAULT_NUMBER_OF_COMPONENTS,
subgroup.value,
options.value
);
}
}
emit(
"update:scale",
Expand Down Expand Up @@ -300,6 +345,7 @@ function modify() {
id="tempering-CTE"
value="CTE"
@focus="error = ''"
:disabled="constraintsDisabled"
v-model="tempering"
/>
<label for="tempering-CTE"> CTE </label>
Expand All @@ -311,6 +357,7 @@ function modify() {
<textarea
id="constraints"
@focus="error = ''"
:disabled="constraintsDisabled"
v-model="constraintsString"
></textarea>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ export const LEFT_MOUSE_BTN = 0;

// Offset such that default base MIDI note doesn't move in "simple" white mode.
export const WHITE_MODE_OFFSET = 69 - 40;

// === Sanity limits for tempering ===

// Anything larger than this isn't evaluated interactively
export const MAX_INTERACTIVE_SUBGROUP_SIZE = 6;
// Anything larger than this uses O(n²) methods (if available) instead of O(exp(n))
export const MAX_GEO_SUBGROUP_SIZE = 9;
12 changes: 12 additions & 0 deletions src/tempering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ import {
} from "xen-dev-utils";
import { Interval, Scale } from "scale-workshop-core";

export function toPrimeMapping(mapping: number[], subgroup: Subgroup) {
const result = subgroup.toPrimeMapping(mapping);

while (result.length > DEFAULT_NUMBER_OF_COMPONENTS) {
result.pop();
}
while (result.length < DEFAULT_NUMBER_OF_COMPONENTS) {
result.push(PRIME_CENTS[result.length]);
}
return result as number[];
}

export class Mapping {
vector: number[];

Expand Down

0 comments on commit d1dd036

Please sign in to comment.