From 4de719b22e6d9961c139968a235b99ca0ff269ba Mon Sep 17 00:00:00 2001 From: inthar-raven <36112167+inthar-raven@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:08:31 -0500 Subject: [PATCH 1/7] Update AnalysisView.vue Added an interval matrix indexing option (default zero-indexing). --- src/views/AnalysisView.vue | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/views/AnalysisView.vue b/src/views/AnalysisView.vue index d756d9c3..97ded728 100644 --- a/src/views/AnalysisView.vue +++ b/src/views/AnalysisView.vue @@ -100,9 +100,9 @@ const matrix = computed(() => { - {{ i }} + {{ i - (indexing.value === "indexing-one" ? 0 : 1) }} - ({{ scale.size + 1 }}) + ({{ scale.size + (indexing.value === "indexing-one" ? 1 : 0) }}) {{ formatMatrixCell(scale.getInterval(i)) }} @@ -143,6 +143,28 @@ const matrix = computed(() => { +
+ + + + + + + + + + +
From 5b786e85afd263d6ac925efd3d6e9967e77d5d6b Mon Sep 17 00:00:00 2001 From: inthar-raven <36112167+inthar-raven@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:11:38 -0500 Subject: [PATCH 2/7] Update AnalysisView.vue --- src/views/AnalysisView.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/AnalysisView.vue b/src/views/AnalysisView.vue index 97ded728..8fd03e7e 100644 --- a/src/views/AnalysisView.vue +++ b/src/views/AnalysisView.vue @@ -100,9 +100,9 @@ const matrix = computed(() => { - {{ i - (indexing.value === "indexing-one" ? 0 : 1) }} + {{ i - (indexing.value === "one" ? 0 : 1) }} - ({{ scale.size + (indexing.value === "indexing-one" ? 1 : 0) }}) + ({{ scale.size + (indexing.value === "one" ? 1 : 0) }}) {{ formatMatrixCell(scale.getInterval(i)) }} @@ -149,7 +149,7 @@ const matrix = computed(() => { @@ -159,7 +159,7 @@ const matrix = computed(() => { From a968ee73479ada275b79172f536bd0c3cc9634a5 Mon Sep 17 00:00:00 2001 From: inthar-raven <36112167+inthar-raven@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:21:21 -0500 Subject: [PATCH 3/7] Update AnalysisView.vue --- src/views/AnalysisView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/AnalysisView.vue b/src/views/AnalysisView.vue index 8fd03e7e..26fd8ffb 100644 --- a/src/views/AnalysisView.vue +++ b/src/views/AnalysisView.vue @@ -144,7 +144,7 @@ const matrix = computed(() => {
- + Date: Mon, 27 Nov 2023 11:41:21 -0500 Subject: [PATCH 4/7] Update AnalysisView.vue `label for=` should refer to the id's. --- src/views/AnalysisView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/AnalysisView.vue b/src/views/AnalysisView.vue index 26fd8ffb..51bbf763 100644 --- a/src/views/AnalysisView.vue +++ b/src/views/AnalysisView.vue @@ -152,7 +152,7 @@ const matrix = computed(() => { value="zero" v-model="indexing" /> - + @@ -162,7 +162,7 @@ const matrix = computed(() => { value="one" v-model="indexing" /> - +
From 447a043e7b78b6b411a1bed67c913641e1810143 Mon Sep 17 00:00:00 2001 From: inthar-raven <36112167+inthar-raven@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:47:07 -0500 Subject: [PATCH 5/7] Update AnalysisView.vue --- src/views/AnalysisView.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/views/AnalysisView.vue b/src/views/AnalysisView.vue index 51bbf763..31ec7072 100644 --- a/src/views/AnalysisView.vue +++ b/src/views/AnalysisView.vue @@ -19,6 +19,7 @@ const props = defineProps<{ }>(); const cellFormat = ref<"best" | "cents" | "decimal">("best"); +const indexing = ref(0); const trailLongevity = ref(70); const maxOtonalRoot = ref(16); const maxUtonalRoot = ref(23); @@ -100,9 +101,9 @@ const matrix = computed(() => { - {{ i - (indexing.value === "one" ? 0 : 1) }} + {{ i - 1 + indexing.value }} - ({{ scale.size + (indexing.value === "one" ? 1 : 0) }}) + ({{ scale.size + indexing.value }}) {{ formatMatrixCell(scale.getInterval(i)) }} @@ -149,7 +150,7 @@ const matrix = computed(() => { @@ -159,7 +160,7 @@ const matrix = computed(() => { From 7379af0c568ab12305d02efa84f8dcd9c0d463fc Mon Sep 17 00:00:00 2001 From: inthar-raven <36112167+inthar-raven@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:58:53 -0500 Subject: [PATCH 6/7] Update AnalysisView.vue "indexing.value" -> "indexing" --- src/views/AnalysisView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/AnalysisView.vue b/src/views/AnalysisView.vue index 31ec7072..bb6907be 100644 --- a/src/views/AnalysisView.vue +++ b/src/views/AnalysisView.vue @@ -101,9 +101,9 @@ const matrix = computed(() => { - {{ i - 1 + indexing.value }} + {{ i - 1 + indexing }} - ({{ scale.size + indexing.value }}) + ({{ scale.size + indexing }}) {{ formatMatrixCell(scale.getInterval(i)) }} From 89067a2df000437bfa7047ef23932ee38a06c6b4 Mon Sep 17 00:00:00 2001 From: inthar-raven <36112167+inthar-raven@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:28:18 -0500 Subject: [PATCH 7/7] Add files via upload Added reactivity for Issue #411. --- AnalysisView.vue | 311 +++++++++++++++++++++++++++++++++++++++++++++++ analysis.ts | 114 +++++++++++++++++ 2 files changed, 425 insertions(+) create mode 100644 AnalysisView.vue create mode 100644 analysis.ts diff --git a/AnalysisView.vue b/AnalysisView.vue new file mode 100644 index 00000000..65c4914d --- /dev/null +++ b/AnalysisView.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/analysis.ts b/analysis.ts new file mode 100644 index 00000000..d43de5bb --- /dev/null +++ b/analysis.ts @@ -0,0 +1,114 @@ +import type { Scale } from "scale-workshop-core"; + +const EPSILON = 1e-6; + +// Absolute logarithmic error from the nearest harmonic measured in nats +function harmonicError(ratio: number) { + return Math.abs(Math.log(ratio) - Math.log(Math.round(ratio))); +} + +// Calculate a small correction that removes the bias +// in the movement of an otonal chord wheel. +function otonalCorrection(ratios: number[]) { + let correction = 1; + ratios.forEach((ratio) => { + correction *= ratio / Math.round(ratio); + }); + + return Math.pow(correction, -1 / ratios.length); +} + +// Calculate the best multiplier for a set of ratios that +// miminizes the perceived motion of an otonal chord wheel. +function otonalMultiplier(ratios: number[], maxMultiplier = 16) { + let leastError = Infinity; + let result = 1; + for (let multiplier = 1; multiplier <= maxMultiplier; ++multiplier) { + const candidate = + multiplier * otonalCorrection(ratios.map((ratio) => ratio * multiplier)); + const balanced = ratios.map((ratio) => ratio * candidate); + const error = balanced.map(harmonicError).reduce((a, b) => a + b); + if (error + EPSILON < leastError) { + leastError = error; + result = candidate; + } + } + return result; +} + +/** + * Calculate the fundamental frequency implied by an array of frequencies + * interpreted as an enumerated otonal chord. + * @param frequencies Array of frequencies in the chord with the root first. + * @param maxMultiplier Maximum interpretation of the root as an integer in an enumerated chord. + * @returns The fundamental frequency below the root that minimizes the perceived motion of an otonal chord wheel. + */ +export function otonalFundamental(frequencies: number[], maxMultiplier = 16) { + if (!frequencies.length) { + return NaN; + } + const ratios = frequencies.map((f) => f / frequencies[0]); + const multiplier = otonalMultiplier(ratios, maxMultiplier); + return frequencies[0] / multiplier; +} + +// Absolute logarithmic error from the nearest subharmonic measured in nats +function subharmonicError(ratio: number) { + return Math.abs(Math.log(ratio) + Math.log(Math.round(1 / ratio))); +} + +// Calculate a small correction that removes the bias +// in the movement of an utonal chord wheel. +function utonalCorrection(ratios: number[]) { + let correction = 1; + ratios.forEach((ratio) => { + correction *= ratio * Math.round(1 / ratio); + }); + + return Math.pow(correction, 1 / ratios.length); +} + +// Calculate the best divisor for a set of ratios that +// miminizes the perceived motion of an utonal chord wheel. +export function utonalDivisor(ratios: number[], maxDivisor = 23) { + let leastError = Infinity; + let result = 1; + for (let divisor = 1; divisor <= maxDivisor; ++divisor) { + const candidate = + divisor * utonalCorrection(ratios.map((ratio) => ratio / divisor)); + const balanced = ratios.map((ratio) => ratio / candidate); + const error = balanced.map(subharmonicError).reduce((a, b) => a + b); + if (error + EPSILON < leastError) { + leastError = error; + result = candidate; + } + } + return result; +} + +/** + * Calculate the (high) fundamental frequency implied by an array of frequencies + * interpreted as an enumerated utonal (inverted) chord. + * @param frequencies Array of frequencies in the chord with the root first. + * @param maxDivisor Maximum interpretation of the root as an integer in an enumerated chord. + * @returns The fundamental frequency above the root that minimizes the perceived motion of an utonal chord wheel. + */ +export function utonalFundamental(frequencies: number[], maxDivisor = 23) { + if (!frequencies.length) { + return NaN; + } + const ratios = frequencies.map((f) => f / frequencies[0]); + const divisor = utonalDivisor(ratios, maxDivisor); + return divisor * frequencies[0]; +} + +// Interval matrix a.k.a the modes of a scale +export function intervalMatrix(scale: Scale, startIndex: number) { + const result = []; + const degrees = [...Array(scale.size + 1 - startIndex).keys()]; + for (let i = 0; i < scale.size; ++i) { + const mode = scale.rotate(i); + result.push(degrees.map((j) => mode.getInterval(j + startIndex))); + } + return result; +}