diff --git a/src/components/ScaleBuilder.vue b/src/components/ScaleBuilder.vue index 211f3628..ffdc2565 100644 --- a/src/components/ScaleBuilder.vue +++ b/src/components/ScaleBuilder.vue @@ -21,6 +21,7 @@ import LatticeModal from "@/components/modals/generation/SpanLattice.vue"; import EulerGenusModal from "@/components/modals/generation/EulerGenus.vue"; import DwarfModal from "@/components/modals/generation/DwarfScale.vue"; import RankTwoModal from "@/components/modals/generation/RankTwo.vue"; +import HistoricalModal from "@/components/modals/generation/HistoricalScale.vue"; import RotateModal from "@/components/modals/modification/RotateScale.vue"; import SubsetModal from "@/components/modals/modification/TakeSubset.vue"; import StretchModal from "@/components/modals/modification/StretchScale.vue"; @@ -163,6 +164,7 @@ const showEulerGenusModal = ref(false); const showDwarfModal = ref(false); const showRankTwoModal = ref(false); const showCrossPolytopeModal = ref(false); +const showHistoricalModal = ref(false); const showRotateModal = ref(false); const showSubsetModal = ref(false); @@ -221,8 +223,8 @@ function copyToClipboard() {
  • Equal temperament
  • -
  • Rank-2 temperament
  • Historical temperament
  • Harmonic series segment
  • Euler-Fokker genus
  • +
  • Rank-2 temperament
  • Dwarf scale
  • Cross-polytope
  • + + +import { ExtendedMonzo, Interval, parseLine, Scale } from "scale-workshop-core"; +import Modal from "@/components/ModalDialog.vue"; +import { computed, ref } from "vue"; +import { DEFAULT_NUMBER_OF_COMPONENTS } from "@/constants"; +import ScaleLineInput from "@/components/ScaleLineInput.vue"; +import { mmod } from "xen-dev-utils"; + +const props = defineProps<{ + centsFractionDigits: number; +}>(); + +const emit = defineEmits([ + "update:scaleName", + "update:scale", + "update:keyColors", + "cancel", +]); + +const name = ref(""); + +const OCTAVE = parseLine("2/1", DEFAULT_NUMBER_OF_COMPONENTS); + +// TODO: Implement tempering to target interval as a new radio tab. +const method = ref<"simple" | "target">("simple"); + +const generator = ref(parseLine("3/2", DEFAULT_NUMBER_OF_COMPONENTS)); +const generatorString = ref("3/2"); + +const size = ref(12); +const down = ref(4); +const format = ref<"cents" | "default">("cents"); + +const pureGenerator = ref(parseLine("3/2", DEFAULT_NUMBER_OF_COMPONENTS)); +const pureGeneratorString = ref("3/2"); +const target = ref(parseLine("7/4", DEFAULT_NUMBER_OF_COMPONENTS)); +const targetString = ref("7/4"); +const searchRange = ref(11); +const period = ref(OCTAVE); +const periodString = ref("2/1"); +const pureExponent = ref(10); +const temperingStrength = ref(1); + +type Candidate = { + exponent: number; + tempering: number; +}; + +const candidates = computed(() => { + const pureCents = pureGenerator.value.totalCents(); + const targetCents = target.value.totalCents(); + const periodCents = period.value.totalCents(); + const halfPeriod = 0.5 * periodCents; + const result: Candidate[] = []; + for (let i = -searchRange.value; i <= searchRange.value; ++i) { + if (i === 0 || i === 1 || i === -1) { + continue; + } + const offset = + mmod(targetCents - pureCents * i + halfPeriod, periodCents) - halfPeriod; + result.push({ + exponent: i, + tempering: offset / i, + }); + } + result.sort((a, b) => Math.abs(a.tempering) - Math.abs(b.tempering)); + return result; +}); + +// This way makes the selection behave slightly nicer when other values change +const tempering = computed(() => { + for (const candidate of candidates.value) { + if (candidate.exponent === pureExponent.value) { + return candidate.tempering; + } + } + return 0; +}); + +const strengthSlider = computed({ + get: () => temperingStrength.value, + set(newValue: number) { + // There's something wrong with how input ranges are handled. + if (typeof newValue !== "number") { + newValue = parseFloat(newValue); + } + if (!isNaN(newValue)) { + temperingStrength.value = newValue; + } + }, +}); + +function equalizeBeating() { + // This is a naïve, simplified and linearized model of beating + const generatorMaxBeats = Math.abs( + generator.value.monzo.valueOf() * (1 - Math.pow(2, tempering.value / 1200)) + ); + const targetMaxBeats = Math.abs( + target.value.monzo.valueOf() * + (1 - Math.pow(2, (tempering.value * pureExponent.value) / 1200)) + ); + + // Solve the linearized model: + // generatorBeats = generatorMaxBeats * strength = targetMaxBeats * (1 - strength) = targetBeats + temperingStrength.value = + targetMaxBeats / (generatorMaxBeats + targetMaxBeats); +} + +function preset(line: string) { + generator.value = parseLine(line, DEFAULT_NUMBER_OF_COMPONENTS); + generatorString.value = line; +} + +function presetPythagorean() { + name.value = "Pythagorean tuning"; + preset("3/2"); +} + +function preset12TET() { + name.value = "12-tone equal temperament"; + preset("7\\12"); +} + +function presetEleventhCommaMeantone() { + name.value = "1/11-comma Meantone"; + preset("3/2 - 1\\11<81/80>"); +} + +function presetSixthCommaMeantone() { + name.value = "⅙-comma Meantone"; + preset("3/2 - 1\\6<81/80>"); +} + +function presetFifthCommaMeantone() { + name.value = "⅕-comma Meantone"; + preset("3/2 - 1\\5<81/80>"); +} + +function presetQuarterCommaMeantone() { + name.value = "¼-comma Meantone"; + preset("3/2 - 1\\4<81/80>"); +} + +function presetTwoSeventhsCommaMeantone() { + name.value = "2/7-comma Meantone"; + preset("3/2 - 2\\7<81/80>"); +} + +function presetThirdCommaMeantone() { + name.value = "⅓-comma Meantone"; + preset("3/2 - 1\\3<81/80>"); +} + +function presetHalfCommaMeantone() { + name.value = "½-comma Meantone"; + preset("3/2 - 1\\2<81/80>"); +} + +function generate() { + const lineOptions = { centsFractionDigits: props.centsFractionDigits }; + + if (method.value === "simple") { + const scale = Scale.fromRank2( + generator.value.mergeOptions(lineOptions), + OCTAVE.mergeOptions(lineOptions), + size.value, + down.value + ); + + if (name.value === "") { + name.value = `Rank 2 temperament (${generatorString.value})`; + } + + emit("update:scaleName", name.value); + if (format.value === "cents") { + emit("update:scale", scale.asType("cents")); + } else { + emit("update:scale", scale); + } + } else { + const g = pureGenerator.value + .mergeOptions(lineOptions) + .add( + new Interval( + ExtendedMonzo.fromCents( + tempering.value * temperingStrength.value, + DEFAULT_NUMBER_OF_COMPONENTS + ), + "cents", + undefined, + lineOptions + ) + ); + const scale = Scale.fromRank2( + g, + period.value.mergeOptions(lineOptions), + size.value, + down.value + ); + + emit( + "update:scaleName", + `Rank 2 temperament (${g.toString()}, ${periodString.value})` + ); + if (format.value === "cents") { + emit("update:scale", scale.asType("cents")); + } else { + emit("update:scale", scale); + } + } +} + +