From 7e0af00a8592b09f237cdb9e48112d2c123413ad Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Fri, 31 May 2024 13:15:18 +0300 Subject: [PATCH] Add harmonic entropy to analysis tab --- package-lock.json | 39 +- package.json | 3 +- src/App.vue | 6 +- src/assets/harmonic-entropy.ydata.raw | Bin 0 -> 48004 bytes src/components/HarmonicEntropyPlot.vue | 213 +++++++++ src/harmonic-entropy-worker.ts | 13 + src/stores/harmonic-entropy.ts | 115 +++++ src/views/AnalysisView.vue | 613 ++++++++++++++++--------- 8 files changed, 789 insertions(+), 213 deletions(-) create mode 100644 src/assets/harmonic-entropy.ydata.raw create mode 100644 src/components/HarmonicEntropyPlot.vue create mode 100644 src/harmonic-entropy-worker.ts create mode 100644 src/stores/harmonic-entropy.ts diff --git a/package-lock.json b/package-lock.json index be077069..74bb268b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { "name": "scale-workshop", - "version": "3.0.0-beta.36", + "version": "3.0.0-beta.37", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "scale-workshop", - "version": "3.0.0-beta.36", + "version": "3.0.0-beta.37", "dependencies": { + "harmonic-entropy": "^0.2.0", "isomorphic-qwerty": "^0.0.2", "ji-lattice": "^0.0.3", "jszip": "^3.10.1", @@ -3161,6 +3162,15 @@ "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", "dev": true }, + "node_modules/frost-fft": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/frost-fft/-/frost-fft-0.2.0.tgz", + "integrity": "sha512-o4V+U5CtMv+UVJPlBf6G6+4Gm0+8ttUYpXoIot0maV6FtP9IhmRNX69zfOuhj6G1G5Ip+q+y/G1wc9B0fmFqvQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/frostburn" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -3357,6 +3367,31 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/harmonic-entropy": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/harmonic-entropy/-/harmonic-entropy-0.2.0.tgz", + "integrity": "sha512-XuFwQqyEbDiIWSlZzJcLTh8L9PIoinodKZj7IuP/5x+hmssS4XAeGkwnIsWNYDgBBCeJda/0JlYfb6QKbhM+gA==", + "dependencies": { + "frost-fft": "^0.2.0", + "xen-dev-utils": "^0.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/frostburn" + } + }, + "node_modules/harmonic-entropy/node_modules/xen-dev-utils": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/xen-dev-utils/-/xen-dev-utils-0.7.0.tgz", + "integrity": "sha512-KECBCnnHD9sh4lY0Q6+KcgKVwMBWUOGjEJ/7S8sER2L3BKciy9kLhPFB+LR3z1oQiNS/OI7lv3L4rtPEX5SPUQ==", + "engines": { + "node": ">=10.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/frostburn" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/package.json b/package.json index c464cd94..a3d4735a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scale-workshop", - "version": "3.0.0-beta.36", + "version": "3.0.0-beta.37", "scripts": { "dev": "vite", "build": "run-p type-check \"build-only {@}\" --", @@ -15,6 +15,7 @@ "format": "prettier --write src/" }, "dependencies": { + "harmonic-entropy": "^0.2.0", "isomorphic-qwerty": "^0.0.2", "ji-lattice": "^0.0.3", "jszip": "^3.10.1", diff --git a/src/App.vue b/src/App.vue index a0ac7174..34791b50 100644 --- a/src/App.vue +++ b/src/App.vue @@ -13,6 +13,7 @@ import { useAudioStore } from '@/stores/audio' import { useStateStore } from './stores/state' import { useMidiStore } from './stores/midi' import { useScaleStore } from './stores/scale' +import { useHarmonicEntropyStore } from '@/stores/harmonic-entropy' import { clamp, mmod } from 'xen-dev-utils' import { parseScaleWorkshop2Line, setNumberOfComponents } from 'sonic-weave' @@ -21,6 +22,7 @@ const state = useStateStore() const scale = useScaleStore() const midi = useMidiStore() const audio = useAudioStore() +const entropy = useHarmonicEntropyStore() // == URL path handling == /** @@ -328,7 +330,7 @@ function typingKeydown(event: CoordinateKeyboardEvent) { } // === Lifecycle === -onMounted(() => { +onMounted(async () => { window.addEventListener('keyup', windowKeyup) window.addEventListener('keydown', windowKeydownOrUp) window.addEventListener('keyup', windowKeydownOrUp) @@ -436,6 +438,7 @@ onMounted(() => { console.error(`Error parsing version ${query.get('version')} URL`, error) } } + await entropy.fetchTable() }) onUnmounted(() => { @@ -530,6 +533,7 @@ nav#app-navigation { display: flex; } +#app > #view, #app > main { flex: 1 1 auto; overflow-y: hidden; diff --git a/src/assets/harmonic-entropy.ydata.raw b/src/assets/harmonic-entropy.ydata.raw new file mode 100644 index 0000000000000000000000000000000000000000..d0a5a94fac0af0fd179781444c9299c8d009cece GIT binary patch literal 48004 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 Xw6<+c$*-Y&ZJgY^F^`NHb;kbz(Rrv7 literal 0 HcmV?d00001 diff --git a/src/components/HarmonicEntropyPlot.vue b/src/components/HarmonicEntropyPlot.vue new file mode 100644 index 00000000..2288cafb --- /dev/null +++ b/src/components/HarmonicEntropyPlot.vue @@ -0,0 +1,213 @@ + + + + diff --git a/src/harmonic-entropy-worker.ts b/src/harmonic-entropy-worker.ts new file mode 100644 index 00000000..84951b5a --- /dev/null +++ b/src/harmonic-entropy-worker.ts @@ -0,0 +1,13 @@ +import { EntropyCalculator, type HarmonicEntropyOptions } from 'harmonic-entropy' + +let entropy: EntropyCalculator | undefined + +onmessage = (e) => { + const options: HarmonicEntropyOptions = e.data.options + if (!entropy) { + entropy = new EntropyCalculator(options) + } else { + entropy.options = options + } + postMessage({ json: entropy.toJSON(), jobId: e.data.jobId }) +} diff --git a/src/stores/harmonic-entropy.ts b/src/stores/harmonic-entropy.ts new file mode 100644 index 00000000..7322ffd9 --- /dev/null +++ b/src/stores/harmonic-entropy.ts @@ -0,0 +1,115 @@ +import HE_DATA_URL from '@/assets/harmonic-entropy.ydata.raw?url' +import EntropyWorker from '@/harmonic-entropy-worker?worker' +import { computed, reactive, ref, watch } from 'vue' +import { debounce } from '@/utils' +import { defineStore } from 'pinia' +import { type HarmonicEntropyOptions } from 'harmonic-entropy' + +// The app freezes if we try to recalculate entropy in the main thread. +const worker = new EntropyWorker() +// Debounce doesn't block workers so we need extra guards to hide changes that would be overwritten. +let jobId = 0 + +// Constant options for harmonic-entropy package +const MIN_CENTS = 0 +const MAX_CENTS = 6000 +const RES = 0.5 +const SERIES = 'tenney' +const NORMALIZE = true + +export const useHarmonicEntropyStore = defineStore('harmonic-entropy', () => { + const table = reactive<[number, number][]>([]) + + // The fetched N is much larger, but we use a smaller value for the UI. + const N = ref(10000) + const a = ref(1) + const s = ref(0.01) + + const minY = computed(() => Math.min(...table.map((xy) => xy[1]))) + const maxY = computed(() => Math.max(...table.map((xy) => xy[1]))) + + async function fetchTable(force = false) { + if (table.length && !force) { + return + } + const response = await fetch(HE_DATA_URL) + const buffer = await response.arrayBuffer() + const tableY = Array.from(new Float32Array(buffer)) + + table.length = 0 + + let i = 0 + for (let x = 0; x <= MAX_CENTS; x += RES) { + table.push([x, tableY[i++]]) + } + } + + worker.onmessage = (e) => { + if (e.data.jobId === jobId) { + const tableY = e.data.json.tableY + + table.length = 0 + let i = 0 + for (let x = 0; x <= MAX_CENTS; x += RES) { + table.push([x, tableY[i++]]) + } + } + } + + // Pinia fails to serialize EntropyCalculator so we recreate its functionality here. + function entropyPercentage(cents: number) { + if (!table.length) { + return 0 + } + cents = Math.abs(cents) + if (cents >= MAX_CENTS) { + return (table[table.length - 1][1] - minY.value) / (maxY.value - minY.value) + } + + let mu = cents / RES + const index = Math.floor(mu) + mu -= index + + const y = table[index][1] * (1 - mu) + table[index + 1][1] * mu + return (y - minY.value) / (maxY.value - minY.value) + } + + watch( + N, + debounce((newValue) => { + const opts = { ...options.value } + opts.N = newValue + worker.postMessage({ options: opts, jobId: ++jobId }) + }) + ) + + watch( + a, + debounce((newValue) => { + const opts = { ...options.value } + opts.a = newValue + worker.postMessage({ options: opts, jobId: ++jobId }) + }) + ) + + watch( + s, + debounce((newValue) => { + const opts = { ...options.value } + opts.s = newValue + worker.postMessage({ options: opts, jobId: ++jobId }) + }) + ) + + const options = computed(() => ({ + N: N.value, + a: a.value, + s: s.value, + minCents: MIN_CENTS, + maxCents: MAX_CENTS, + res: RES, + series: SERIES, + normalize: NORMALIZE + })) + return { table, N, a, s, minY, maxY, options, fetchTable, entropyPercentage } +}) diff --git a/src/views/AnalysisView.vue b/src/views/AnalysisView.vue index 43e2b097..b112659d 100644 --- a/src/views/AnalysisView.vue +++ b/src/views/AnalysisView.vue @@ -9,6 +9,7 @@ import { varietySignature } from '@/analysis' import ChordWheel from '@/components/ChordWheel.vue' +import HarmonicEntropyPlot from '@/components/HarmonicEntropyPlot.vue' import ScaleLineInput from '@/components/ScaleLineInput.vue' import { computed, reactive, ref } from 'vue' import { useAudioStore } from '@/stores/audio' @@ -17,11 +18,16 @@ import { literalToString, type Interval } from 'sonic-weave' import { useScaleStore } from '@/stores/scale' import { Fraction, mmod } from 'xen-dev-utils' import { OCTAVE, UNISON } from '@/constants' +import { useHarmonicEntropyStore } from '@/stores/harmonic-entropy' + +const EPSILON = 1e-6 const audio = useAudioStore() const state = useStateStore() const scale = useScaleStore() +const entropy = useHarmonicEntropyStore() +const subtab = ref<'matrix' | 'wheels' | 'entropy'>('matrix') const cellFormat = ref<'best' | 'fraction' | 'cents' | 'decimal'>('best') const simplifyTolerance = ref(3.5) const showOptions = ref(false) @@ -203,241 +209,409 @@ function highlight(y?: number, x?: number) { } } } + +// === Harmonic entropy === +const heMode = ref(0) + +const centss = computed(() => { + const result: number[] = [] + let index = scale.baseMidiNote + heMode.value + const baseCents = scale.scale.getCents(index) + while (index < 10000) { + const cents = scale.scale.getCents(index++) - baseCents + if (cents > 6000 + EPSILON) { + break + } + result.push(cents) + } + return result +}) + +const labels = computed(() => + centss.value.map((_, i) => scale.labelForIndex(scale.baseMidiNote + heMode.value + i)) +) + +const colors = computed(() => + centss.value.map((_, i) => scale.colorForIndex(scale.baseMidiNote + heMode.value + i)) +) + +// These really should be direct v-models, but there's +// something wrong with how input ranges are handled. +const aSlider = computed({ + get: () => entropy.a, + set(newValue: number) { + if (typeof newValue !== 'number') { + newValue = parseFloat(newValue) + } + if (!isNaN(newValue)) { + entropy.a = newValue + } + } +}) + +const sSlider = computed({ + get: () => entropy.s, + set(newValue: number) { + if (typeof newValue !== 'number') { + newValue = parseFloat(newValue) + } + if (!isNaN(newValue)) { + entropy.s = newValue + } + } +})