From 246f7cea924fa0ed17d6c3a28603533ab5783478 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Mon, 11 Mar 2024 09:20:17 +0200 Subject: [PATCH] The SW3 commit This commit swaps out the core engine from scale-workshop-core to sonic-weave. Too many breaking changes have been introduced to maintain an incremental commit history at this juncture. --- CHANGELOG.md | 16 + LICENSE | 2 +- README.md | 5 + cypress/e2e/basic.cy.ts | 12 +- cypress/e2e/compatibility.cy.ts | 17 + index.html | 6 +- package-lock.json | 510 ++------ package.json | 26 +- public/favicon.png | Bin 17358 -> 22811 bytes src/App.vue | 293 ++--- src/__tests__/analysis.spec.ts | 21 +- src/__tests__/midi.spec.ts | 10 +- src/__tests__/scale-workshop-one.spec.ts | 49 +- src/__tests__/tempering.spec.ts | 215 +--- src/__tests__/url-encode.spec.ts | 10 +- src/__tests__/util.spec.ts | 29 +- src/analysis.ts | 345 +++++- src/assets/base.css | 292 +---- src/assets/img/spoob.png | Bin 0 -> 26901 bytes src/assets/logo.svg | 2 +- src/assets/main.css | 215 ++++ src/character-palette.json | 33 + src/components/ChordWheel.vue | 2 +- src/components/DropdownGroup.vue | 70 ++ src/components/ExporterButtons.vue | 203 ++++ src/components/GridLattice.vue | 155 +++ src/components/JustIntonationLattice.vue | 201 ++++ src/components/ModalDialog.vue | 17 +- src/components/ModifyScale.vue | 222 ++++ src/components/NewScale.vue | 260 ++++ src/components/PeriodCircle.vue | 15 +- src/components/ScaleBuilder.vue | 886 -------------- src/components/ScaleControls.vue | 157 +++ src/components/ScaleLattice.vue | 282 ----- src/components/ScaleLineInput.vue | 10 +- src/components/ScaleRule.vue | 9 +- src/components/TuningTable.vue | 69 +- src/components/TuningTableRow.vue | 21 +- src/components/VirtualKeyboard.vue | 2 +- src/components/VirtualPiano.vue | 284 ++++- src/components/VirtualTypingKeyboard.vue | 35 +- src/components/modals/export/KorgExport.vue | 11 +- .../modals/export/MtsSysexExport.vue | 9 +- src/components/modals/export/ReaperExport.vue | 18 +- .../generation/CombinationProductSet.vue | 25 +- .../modals/generation/ConcordanceShell.vue | 140 +++ .../modals/generation/CrossPolytope.vue | 77 -- .../modals/generation/DwarfScale.vue | 42 - .../modals/generation/EnumerateChord.vue | 30 +- .../modals/generation/EqualTemperament.vue | 68 +- .../modals/generation/EulerGenus.vue | 49 +- .../modals/generation/GeneratorSequence.vue | 195 +++ .../modals/generation/HarmonicSeries.vue | 25 +- .../modals/generation/HistoricalScale.vue | 148 +-- .../modals/generation/LoadPreset.vue | 57 + src/components/modals/generation/MosScale.vue | 41 +- src/components/modals/generation/RankTwo.vue | 133 +- .../modals/generation/SpanLattice.vue | 104 +- .../modals/generation/StackSteps.vue | 61 + .../modals/generation/SubharmonicSeries.vue | 35 +- .../modals/generation/SummonOctaplex.vue | 33 +- .../modification/ApproximateByHarmonics.vue | 47 +- .../modification/ApproximateByRatios.vue | 57 +- .../ApproximateBySubharmonics.vue | 39 +- .../modification/CoalesceDuplicates.vue | 106 ++ .../modals/modification/ConvertType.vue | 156 ++- .../modals/modification/EnumerateScale.vue | 86 ++ .../modals/modification/EqualizeScale.vue | 67 +- .../modals/modification/ExpandScale.vue | 44 + .../modals/modification/MergeOffset.vue | 93 -- .../modals/modification/MergeOffsets.vue | 76 ++ .../modals/modification/RandomVariance.vue | 40 +- .../modals/modification/RotateScale.vue | 33 +- .../modals/modification/StretchScale.vue | 37 +- .../modals/modification/TakeSubset.vue | 42 +- .../modals/modification/TemperScale.vue | 182 +-- src/constants.ts | 50 +- src/exporters/__tests__/ableton.spec.ts | 13 +- src/exporters/__tests__/anamark.v1.tun | 490 ++++---- src/exporters/__tests__/anamark.v2.tun | 501 ++++---- src/exporters/__tests__/deflemask.txt | 105 +- src/exporters/__tests__/image-line.spec.ts | 5 +- src/exporters/__tests__/kontakt.txt | 264 ++-- src/exporters/__tests__/korg.spec.ts | 68 +- src/exporters/__tests__/max-msp.txt | 246 ++-- src/exporters/__tests__/mts.spec.ts | 10 +- src/exporters/__tests__/pure-data.spec.ts | 5 +- src/exporters/__tests__/pure-data.txt | 128 ++ src/exporters/__tests__/reaper.spec.ts | 6 +- src/exporters/__tests__/reaper.txt | 244 ++-- src/exporters/__tests__/scala.spec.ts | 14 +- src/exporters/__tests__/soniccouture.nka | 136 +-- src/exporters/__tests__/test-data.ts | 79 +- src/exporters/ableton.ts | 31 +- src/exporters/anamark.ts | 30 +- src/exporters/base.ts | 29 +- src/exporters/deflemask.ts | 4 +- src/exporters/image-line.ts | 7 +- src/exporters/index.ts | 18 +- src/exporters/kontakt.ts | 10 +- src/exporters/korg.ts | 26 +- src/exporters/max-msp.ts | 11 +- src/exporters/mts-sysex.ts | 12 +- src/exporters/pure-data.ts | 14 +- src/exporters/reaper.ts | 101 +- src/exporters/scala.ts | 40 +- src/exporters/soniccouture.ts | 12 +- src/importers/__tests__/anamark.spec.ts | 12 +- src/importers/__tests__/scala.spec.ts | 25 +- src/importers/anamark.ts | 16 +- src/importers/base.ts | 11 +- src/importers/scala.ts | 20 +- src/main.ts | 3 - src/midi.ts | 9 +- src/presets.json | 1069 ++--------------- src/presets.ts | 13 +- src/scale-workshop-one.ts | 11 +- src/scale.ts | 111 ++ src/stores/audio.ts | 222 +++- src/stores/grid.ts | 248 ++++ src/stores/historical.ts | 80 +- src/stores/ji-lattice.ts | 161 +++ src/stores/modal.ts | 247 +++- src/stores/scale.ts | 452 +++++++ src/stores/state.ts | 177 +-- src/stores/tempering.ts | 85 +- src/synth.ts | 597 ++++----- src/tempering.ts | 167 +-- src/utils.ts | 139 ++- src/views/AboutView.vue | 52 +- src/views/AnalysisView.vue | 234 +++- src/views/LatticeView.vue | 348 +++++- src/views/MidiView.vue | 10 +- src/views/NotFoundView.vue | 60 +- src/views/PreferencesView.vue | 29 +- src/views/ScaleView.vue | 137 ++- src/views/SynthView.vue | 154 ++- src/views/VirtualKeyboardView.vue | 26 +- src/views/VirtualQwerty.vue | 25 +- 139 files changed, 8599 insertions(+), 6474 deletions(-) create mode 100644 cypress/e2e/compatibility.cy.ts create mode 100644 src/assets/img/spoob.png create mode 100644 src/assets/main.css create mode 100644 src/character-palette.json create mode 100644 src/components/DropdownGroup.vue create mode 100644 src/components/ExporterButtons.vue create mode 100644 src/components/GridLattice.vue create mode 100644 src/components/JustIntonationLattice.vue create mode 100644 src/components/ModifyScale.vue create mode 100644 src/components/NewScale.vue delete mode 100644 src/components/ScaleBuilder.vue create mode 100644 src/components/ScaleControls.vue delete mode 100644 src/components/ScaleLattice.vue create mode 100644 src/components/modals/generation/ConcordanceShell.vue delete mode 100644 src/components/modals/generation/CrossPolytope.vue delete mode 100644 src/components/modals/generation/DwarfScale.vue create mode 100644 src/components/modals/generation/GeneratorSequence.vue create mode 100644 src/components/modals/generation/LoadPreset.vue create mode 100644 src/components/modals/generation/StackSteps.vue create mode 100644 src/components/modals/modification/CoalesceDuplicates.vue create mode 100644 src/components/modals/modification/EnumerateScale.vue create mode 100644 src/components/modals/modification/ExpandScale.vue delete mode 100644 src/components/modals/modification/MergeOffset.vue create mode 100644 src/components/modals/modification/MergeOffsets.vue create mode 100644 src/exporters/__tests__/pure-data.txt create mode 100644 src/scale.ts create mode 100644 src/stores/grid.ts create mode 100644 src/stores/ji-lattice.ts create mode 100644 src/stores/scale.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0225e10a..1680f1a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change log +## 3.0.0-beta + * Feature: Core language switched to from [scale-workshop-core](https://github.com/xenharmonic-devs/scale-workshop-core) to [sonic-weave](https://github.com/xenharmonic-devs/sonic-weave) + * Feature: Virtual piano now supports up to 4 layers of colors + * Feature: Character palette with tooltips for syntax beyond ASCII [#533](https://github.com/xenharmonic-devs/scale-workshop/issues/533) + * Feature: Interval matrix simplified by default [#536](https://github.com/xenharmonic-devs/scale-workshop/issues/536) + * Feature: Convert scale to enumeration [#538](https://github.com/xenharmonic-devs/scale-workshop/issues/538) + * Feature: More MOS coloring options [#554](https://github.com/xenharmonic-devs/scale-workshop/issues/554) + * Feature: Variety and brightness signatures show in the interval matrix [#568](https://github.com/xenharmonic-devs/scale-workshop/issues/568) + * Feature: Periodic equally tempered grids supported on the lattice tab + * Feature: Lattice label sizes customizable [#581](https://github.com/xenharmonic-devs/scale-workshop/issues/581) + * Feature: Lattice colors inverted and scale colors incorporated [#586](https://github.com/xenharmonic-devs/scale-workshop/issues/586) + * Feature: Scott Dakota's prime rings on the lattice tab [#551](https://github.com/xenharmonic-devs/scale-workshop/issues/551) + * Feature: Tonnetz prime ellipse coordinates on the lattice tab [#588](https://github.com/xenharmonic-devs/scale-workshop/issues/588) + * Feature: New `latticeView()` command for displaying the order of intervals (prior to sorting) [#597](https://github.com/xenharmonic-devs/scale-workshop/issues/597) + * Alpha cycle issues: [#574](https://github.com/xenharmonic-devs/scale-workshop/issues/574), [#579](https://github.com/xenharmonic-devs/scale-workshop/issues/579) + ## 2.4.1 * Bug fix: Unison is no longer affected by random variance [#613](https://github.com/xenharmonic-devs/scale-workshop/issues/613) diff --git a/LICENSE b/LICENSE index 62ccc0e4..e36d9b60 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Xenharmonic Developers +Copyright (c) 2022-2024 Xenharmonic Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9850164e..12880aab 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ ![Scale Workshop screenshot](https://raw.githubusercontent.com/xenharmonic-devs/scale-workshop/main/src/assets/img/opengraph-image.png) +## Beta Warning! +This release is in beta. Not everything has been implemented and documentation has not been synchronized with the new features. + +Some of the new features are documented over at [sonic-weave](https://github.com/xenharmonic-devs/sonic-weave). + ## Description [Scale Workshop](https://scaleworkshop.plainsound.org/) allows you to design microtonal scales and play them in your web browser. Export your scales for use with VST instruments. Convert Scala files to various tuning formats. diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts index a0736e16..df334961 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.ts @@ -8,14 +8,16 @@ describe("Basic test", () => { it("preserves the base frequency when changing tabs", () => { cy.visit("/"); - cy.get(".real-valued").clear() - cy.get(".real-valued").type("432") - cy.get(".real-valued").trigger("change") - cy.get(".real-valued").should("have.value", "432"); + cy.get("#auto-frequency").click() + cy.get("#base-frequency").clear() + cy.get("#base-frequency").type("432") + cy.get("#base-frequency").trigger("change") + cy.get("#base-frequency").should("have.value", "432"); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(400); // Wait for debounce to expire. cy.get("a").contains("Synth").click(); - cy.url().should("contain", "f="); + cy.get("a").contains("Build Scale").click(); + cy.get("#base-frequency").should("have.value", "432"); }); }); diff --git a/cypress/e2e/compatibility.cy.ts b/cypress/e2e/compatibility.cy.ts new file mode 100644 index 00000000..d2bb8ff5 --- /dev/null +++ b/cypress/e2e/compatibility.cy.ts @@ -0,0 +1,17 @@ +// https://on.cypress.io/api + +describe("Scale Workshop 2 compatibility", () => { + it("ignores invalid scale lines", () => { + cy.visit("/?l=1_7nF74_5F4_gtFe8_bF8_2c1F1kw_pFg_1jFw_3dF1s_2F1&version=2.4.0"); + cy.contains("h2", "Scale data"); + cy.get("#scale-data").should('have.value', '// 1\n275/256 black\n5/4 white\n605/512 white\n11/8 black\n3025/2048 white\n25/16 black\n55/32 white\n121/64 white\n2/1 black'); + }); +}); + +describe("Scale Workshop 1 compatibility", () => { + it("supports all line types", () => { + cy.visit("?name=Test%20scale&data=1%2C23%0A1%5C3%0A3%2F2%0A1001.2%0A2%2F1&freq=420&midi=42&vert=5&horiz=1&colors=white%20black%20white%20black%20white&waveform=triangle&env=perc-medium"); + cy.contains("h2", "Scale data"); + cy.get("#scale-data").should('have.value', '1.23e black\n1\\3 white\n3/2 black\n1001.2 white\n2/1 white'); + }); +}); diff --git a/index.html b/index.html index a21980a9..f57c7db4 100644 --- a/index.html +++ b/index.html @@ -4,11 +4,11 @@ - Scale Workshop + Scale Workshop 3 - + - + diff --git a/package-lock.json b/package-lock.json index 78111f99..ed881e13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,21 @@ { "name": "scale-workshop", - "version": "2.4.1", + "version": "3.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "scale-workshop", - "version": "2.4.1", + "version": "3.0.0-beta.1", "dependencies": { "isomorphic-qwerty": "^0.0.2", + "ji-lattice": "^0.0.2", "jszip": "^3.10.1", - "moment-of-symmetry": "^0.4.1", + "moment-of-symmetry": "^0.4.2", "pinia": "^2.1.7", - "qs": "^6.11.2", - "scale-workshop-core": "github:xenharmonic-devs/scale-workshop-core#v0.1.5", + "qs": "^6.12.0", + "sonic-weave": "github:xenharmonic-devs/sonic-weave#v0.0.4", + "sw-synth": "^0.1.0", "temperaments": "^0.5.3", "vue": "^3.3.4", "vue-router": "^4.3.0", @@ -22,26 +24,26 @@ "xen-midi": "^0.1.2" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.7.2", - "@tsconfig/node18": "^18.2.2", + "@rushstack/eslint-patch": "^1.8.0", + "@tsconfig/node18": "^18.2.3", "@types/jsdom": "^21.1.6", - "@types/node": "^18.19.21", - "@types/qs": "^6.9.12", + "@types/node": "^18.19.26", + "@types/qs": "^6.9.14", "@vitejs/plugin-vue": "^4.6.2", "@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-typescript": "^12.0.0", - "@vue/test-utils": "^2.4.4", + "@vue/test-utils": "^2.4.5", "@vue/tsconfig": "^0.4.0", - "cypress": "^13.6.6", + "cypress": "^13.7.1", "eslint": "^8.57.0", "eslint-plugin-cypress": "^2.15.1", - "eslint-plugin-vue": "^9.22.0", + "eslint-plugin-vue": "^9.23.0", "jsdom": "^22.1.0", "npm-run-all2": "^6.1.2", "prettier": "^3.2.5", "start-server-and-test": "^2.0.3", "typescript": "~5.2.0", - "vite": "^4.5.2", + "vite": "^4.5.3", "vitest": "^0.34.6", "vue-tsc": "^1.8.27" } @@ -150,246 +152,6 @@ "ms": "^2.1.1" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", @@ -406,102 +168,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -798,9 +464,9 @@ } }, "node_modules/@rushstack/eslint-patch": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", - "integrity": "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.8.0.tgz", + "integrity": "sha512-0HejFckBN2W+ucM6cUOlwsByTKt9/+0tWhqUffNIcHqCXkthY/mZ7AuYPK/2IIaGWhdl0h+tICDO0ssLMd6XMQ==", "dev": true }, "node_modules/@sideway/address": { @@ -840,9 +506,9 @@ } }, "node_modules/@tsconfig/node18": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz", - "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.3.tgz", + "integrity": "sha512-5GKTU9bfn4L37G9IdK8wcHfvyMijzw1uKNCd2Rs75V7fZK/l2OjGJ8Aa2myqNnESjekm/udpCnFH9qR9yPCtmw==", "dev": true }, "node_modules/@types/chai": { @@ -878,18 +544,18 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.19.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.21.tgz", - "integrity": "sha512-2Q2NeB6BmiTFQi4DHBzncSoq/cJMLDdhPaAoJFnFCyD9a8VPZRf7a1GAwp1Edb7ROaZc5Jz/tnZyL6EsWMRaqw==", + "version": "18.19.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.26.tgz", + "integrity": "sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/qs": { - "version": "6.9.12", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz", - "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==", + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", + "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==", "dev": true }, "node_modules/@types/semver": { @@ -1423,22 +1089,13 @@ "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" }, "node_modules/@vue/test-utils": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.4.tgz", - "integrity": "sha512-8jkRxz8pNhClAf4Co4ZrpAoFISdvT3nuSkUlY6Ys6rmTpw3DMWG/X3mw3gQ7QJzgCZO9f+zuE2kW57fi09MW7Q==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.5.tgz", + "integrity": "sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==", "dev": true, "dependencies": { "js-beautify": "^1.14.9", - "vue-component-type-helpers": "^1.8.21" - }, - "peerDependencies": { - "@vue/server-renderer": "^3.0.1", - "vue": "^3.0.1" - }, - "peerDependenciesMeta": { - "@vue/server-renderer": { - "optional": true - } + "vue-component-type-helpers": "^2.0.0" } }, "node_modules/@vue/tsconfig": { @@ -1594,6 +1251,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aperiodic-oscillator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/aperiodic-oscillator/-/aperiodic-oscillator-0.1.0.tgz", + "integrity": "sha512-i85h6R8nI03sRkNOIDcc/f1c1V5r+UcSYy3/stxhVUnBuwLAtCkA3FB9ZoIu8Z5rMkaNoEZuTCDvN2t+1ijYhw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/frostburn" + } + }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -2173,9 +1839,9 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cypress": { - "version": "13.6.6", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.6.tgz", - "integrity": "sha512-S+2S9S94611hXimH9a3EAYt81QM913ZVA03pUmGDfLTFa5gyp85NJ8dJGSlEAEmyRsYkioS1TtnWtbv/Fzt11A==", + "version": "13.7.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.1.tgz", + "integrity": "sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2672,12 +2338,13 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.22.0.tgz", - "integrity": "sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.24.0.tgz", + "integrity": "sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", @@ -3748,6 +3415,18 @@ "node": ">=10.0.0" } }, + "node_modules/ji-lattice": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/ji-lattice/-/ji-lattice-0.0.2.tgz", + "integrity": "sha512-nLCvHXi0zIhtYVBHaipnsFdMyZG6//xsTcnHidXtv3+r/q2IyesSQ9OSHS+F2cB7lGmaceiEEcplXTnMGY/j6w==", + "dependencies": { + "xen-dev-utils": "^0.2.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/frostburn" + } + }, "node_modules/joi": { "version": "17.12.2", "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", @@ -4321,11 +4000,11 @@ } }, "node_modules/moment-of-symmetry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/moment-of-symmetry/-/moment-of-symmetry-0.4.1.tgz", - "integrity": "sha512-uT4K/nUca6e8z/LruWbZROqi0efB+P3zZ5U4ffGWigKqkvGkUGI12pGWbssLjMobzoJHaluFgnxmD2N3fb6uGA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/moment-of-symmetry/-/moment-of-symmetry-0.4.2.tgz", + "integrity": "sha512-XBzU01kDgQfN0JacGSwMOxJOk5EhwcaojbDQDDdZF/k16ze/VP9X8Cz1Gy8Hmhg+WMu0axfTVP4ufzhoFt0C4g==", "dependencies": { - "xen-dev-utils": "^0.2.7" + "xen-dev-utils": "^0.2.8" }, "funding": { "type": "github", @@ -4961,11 +4640,11 @@ } }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -5245,18 +4924,6 @@ "node": ">=v12.22.7" } }, - "node_modules/scale-workshop-core": { - "version": "0.1.5", - "resolved": "git+ssh://git@github.com/xenharmonic-devs/scale-workshop-core.git#77422327f9258e22635a3857994c373ee1e80eff", - "license": "MIT", - "dependencies": { - "xen-dev-utils": "0.2.8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/frostburn" - } - }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", @@ -5392,6 +5059,25 @@ "node": ">=8" } }, + "node_modules/sonic-weave": { + "version": "0.0.4", + "resolved": "git+ssh://git@github.com/xenharmonic-devs/sonic-weave.git#9643f6c7958143289ad05f016287ff4ed8025ccf", + "license": "MIT", + "dependencies": { + "moment-of-symmetry": "^0.4.2", + "xen-dev-utils": "^0.2.8" + }, + "bin": { + "sonic-weave": "bin/sonic-weave.js" + }, + "engines": { + "node": ">=10.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/frostburn" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -5641,6 +5327,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/sw-synth": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sw-synth/-/sw-synth-0.1.0.tgz", + "integrity": "sha512-JUUdHz5jNLlr3Q+crEY6kM6cS4Dx0KGHV7eiCU3qTJLTCKI9XUc4T1SL4kJblcf4f2cxcN/f6k0rCXX1kzfIcA==", + "dependencies": { + "aperiodic-oscillator": "^0.1.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/frostburn" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -5988,9 +5686,9 @@ "dev": true }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -6163,9 +5861,9 @@ } }, "node_modules/vue-component-type-helpers": { - "version": "1.8.27", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.27.tgz", - "integrity": "sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.7.tgz", + "integrity": "sha512-7e12Evdll7JcTIocojgnCgwocX4WzIYStGClBQ+QuWPinZo/vQolv2EMq4a3lg16TKfwWafLimG77bxb56UauA==", "dev": true }, "node_modules/vue-eslint-parser": { diff --git a/package.json b/package.json index 4c3d9f98..48d7c4cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scale-workshop", - "version": "2.4.1", + "version": "3.0.0-beta.1", "scripts": { "dev": "vite", "build": "run-p type-check \"build-only {@}\" --", @@ -16,11 +16,13 @@ }, "dependencies": { "isomorphic-qwerty": "^0.0.2", + "ji-lattice": "^0.0.2", "jszip": "^3.10.1", - "moment-of-symmetry": "^0.4.1", + "moment-of-symmetry": "^0.4.2", "pinia": "^2.1.7", - "qs": "^6.11.2", - "scale-workshop-core": "github:xenharmonic-devs/scale-workshop-core#v0.1.5", + "qs": "^6.12.0", + "sonic-weave": "github:xenharmonic-devs/sonic-weave#v0.0.4", + "sw-synth": "^0.1.0", "temperaments": "^0.5.3", "vue": "^3.3.4", "vue-router": "^4.3.0", @@ -29,26 +31,26 @@ "xen-midi": "^0.1.2" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.7.2", - "@tsconfig/node18": "^18.2.2", + "@rushstack/eslint-patch": "^1.8.0", + "@tsconfig/node18": "^18.2.3", "@types/jsdom": "^21.1.6", - "@types/node": "^18.19.21", - "@types/qs": "^6.9.12", + "@types/node": "^18.19.26", + "@types/qs": "^6.9.14", "@vitejs/plugin-vue": "^4.6.2", "@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-typescript": "^12.0.0", - "@vue/test-utils": "^2.4.4", + "@vue/test-utils": "^2.4.5", "@vue/tsconfig": "^0.4.0", - "cypress": "^13.6.6", + "cypress": "^13.7.1", "eslint": "^8.57.0", "eslint-plugin-cypress": "^2.15.1", - "eslint-plugin-vue": "^9.22.0", + "eslint-plugin-vue": "^9.23.0", "jsdom": "^22.1.0", "npm-run-all2": "^6.1.2", "prettier": "^3.2.5", "start-server-and-test": "^2.0.3", "typescript": "~5.2.0", - "vite": "^4.5.2", + "vite": "^4.5.3", "vitest": "^0.34.6", "vue-tsc": "^1.8.27" } diff --git a/public/favicon.png b/public/favicon.png index b39986908c2f3a7345f282a2e03a5d4705da83f1..f1e427b8a7f232e70f3528af30290a6c2f176e55 100644 GIT binary patch literal 22811 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 literal 17358 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 import { computed, onMounted, onUnmounted, reactive, watch } from 'vue' -import { RouterLink, RouterView, useRouter, type LocationQuery } from 'vue-router' -import { NEWLINE_TEST } from '@/constants' +import { RouterLink, RouterView, useRouter } from 'vue-router' +import { DEFAULT_NUMBER_OF_COMPONENTS } from '@/constants' import { ScaleWorkshopOneData } from '@/scale-workshop-one' import type { Input, Output } from 'webmidi' import { MidiIn, midiKeyInfo, MidiOut } from 'xen-midi' import { Keyboard, type CoordinateKeyboardEvent } from 'isomorphic-qwerty' -import { decodeQuery, encodeQuery, type DecodedState } from '@/url-encode' -import { debounce } from '@/utils' +import { decodeQuery } from '@/url-encode' +import { annotateColors } from '@/utils' import { version } from '../package.json' import { useAudioStore } from '@/stores/audio' import { useStateStore } from './stores/state' import { useMidiStore } from './stores/midi' +import { useScaleStore } from './stores/scale' +import { clamp } from 'xen-dev-utils' +import { parseScaleWorkshop2Line, setNumberOfComponents } from 'sonic-weave' // === Pinia-managed state === -const audio = useAudioStore() const state = useStateStore() +const scale = useScaleStore() const midi = useMidiStore() +const audio = useAudioStore() // == URL path handling == /** @@ -26,159 +30,14 @@ function getPath(url: URL) { return url.pathname.slice(import.meta.env.BASE_URL.length) } -// == State encoding == const router = useRouter() -// Flags to prevent infinite decode - watch - encode loops -let justEncodedUrl = false -let justDecodedUrl = false - -// Debounced to stagger navigation loops if the flags fail -const encodeState = debounce(() => { - // Navigation loop prevention - if (justDecodedUrl) { - justDecodedUrl = false - return - } - justEncodedUrl = true - - const decodedState: DecodedState = { - scaleName: state.scaleName, - scaleLines: state.scaleLines, - baseFrequency: state.scale.baseFrequency, - baseMidiNote: state.baseMidiNote, - keyColors: state.keyColors, - isomorphicHorizontal: state.isomorphicHorizontal, - isomorphicVertical: state.isomorphicVertical, - keyboardMode: state.keyboardMode, - pianoMode: state.pianoMode, - equaveShift: state.equaveShift, - degreeShift: state.degreeShift, - waveform: audio.waveform, - attackTime: audio.attackTime, - decayTime: audio.decayTime, - sustainLevel: audio.sustainLevel, - releaseTime: audio.releaseTime, - pingPongDelayTime: audio.pingPongDelayTime, - pingPongFeedback: audio.pingPongFeedback, - pingPongSeparation: audio.pingPongSeparation, - pingPongGain: audio.pingPongGain - } - - const query = encodeQuery(decodedState) as LocationQuery - query.version = version - - // XXX: There are some sporadic issues with useRoute().fullPath - // so we use native URL.pathname. - const url = new URL(window.location.href) - - router.push({ path: getPath(url), query }) -}, 200) - -watch( - () => [ - state.scaleName, - state.scaleLines, - state.scale.baseFrequency, - state.baseMidiNote, - state.keyColors, - state.isomorphicHorizontal, - state.isomorphicVertical, - state.keyboardMode, - state.pianoMode, - state.equaveShift, - state.degreeShift, - audio.waveform, - audio.attackTime, - audio.decayTime, - audio.sustainLevel, - audio.releaseTime, - audio.pingPongDelayTime, - audio.pingPongFeedback, - audio.pingPongSeparation, - audio.pingPongGain - ], - encodeState -) - -// == State decoding == -router.afterEach((to, from) => { - if (to.fullPath === from.fullPath) { - return - } - // Navigation loop prevention - if (justEncodedUrl) { - justEncodedUrl = false - return - } - - // XXX: There are some sporadic issues with useRoute().fullPath - // so we use native URL.searchParams. - const url = new URL(window.location.href) - const query = url.searchParams - if (query.has('version')) { - try { - const decodedState = decodeQuery(query) - justDecodedUrl = true - - state.scaleName = decodedState.scaleName - state.scale.baseFrequency = decodedState.baseFrequency - state.baseMidiNote = decodedState.baseMidiNote - state.keyColors = decodedState.keyColors - state.isomorphicHorizontal = decodedState.isomorphicHorizontal - state.isomorphicVertical = decodedState.isomorphicVertical - state.keyboardMode = decodedState.keyboardMode - state.pianoMode = decodedState.pianoMode - state.scaleLines = decodedState.scaleLines - state.equaveShift = decodedState.equaveShift - state.degreeShift = decodedState.degreeShift - audio.waveform = decodedState.waveform - audio.attackTime = decodedState.attackTime - audio.decayTime = decodedState.decayTime - audio.sustainLevel = decodedState.sustainLevel - audio.releaseTime = decodedState.releaseTime - audio.pingPongDelayTime = decodedState.pingPongDelayTime - audio.pingPongFeedback = decodedState.pingPongFeedback - audio.pingPongSeparation = decodedState.pingPongSeparation - audio.pingPongGain = decodedState.pingPongGain - } catch (error) { - console.error(`Error parsing version ${query.get('version')} URL`, error) - } - } -}) - // === Tuning table highlighting === -// We use hacks to bypass Vue state management for real-time gains function tuningTableKeyOn(index: number) { - if (index >= 0 && index < 128) { - let tuningTableRow = (window as any).TUNING_TABLE_ROWS[index] - if (tuningTableRow === undefined) { - tuningTableRow = { heldKeys: 0, element: null } - } - tuningTableRow.heldKeys++ - if (tuningTableRow.element?._rawValue) { - tuningTableRow.element._rawValue.classList.add('active') - } - ;(window as any).TUNING_TABLE_ROWS[index] = tuningTableRow - } - // Virtual keyboard state is too complex so we take the performance hit. state.heldNotes.set(index, (state.heldNotes.get(index) ?? 0) + 1) } function tuningTableKeyOff(index: number) { - if (index >= 0 && index < 128) { - let tuningTableRow = (window as any).TUNING_TABLE_ROWS[index] - if (tuningTableRow === undefined) { - tuningTableRow = { heldKeys: 0, element: null } - } - tuningTableRow.heldKeys-- - if (tuningTableRow.element?._rawValue) { - if (!tuningTableRow.heldKeys) { - tuningTableRow.element._rawValue.classList.remove('active') - } - ;(window as any).TUNING_TABLE_ROWS[index] = tuningTableRow - } - } state.heldNotes.set(index, Math.max(0, (state.heldNotes.get(index) ?? 0) - 1)) } @@ -187,6 +46,7 @@ function tuningTableKeyOff(index: number) { const midiOut = computed(() => new MidiOut(midi.output as Output, midi.outputChannels)) function sendNoteOn(frequency: number, rawAttack: number) { + frequency = clamp(-24000, 24000, frequency) const midiOff = midiOut.value.sendNoteOn(frequency, rawAttack) if (audio.synth === null || audio.virtualSynth === null) { @@ -212,7 +72,7 @@ function midiNoteOn(index: number, rawAttack?: number) { if (rawAttack === undefined) { rawAttack = 80 } - let frequency = state.frequencies[index] + let frequency = scale.frequencies[index] if (!midi.velocityOn) { rawAttack = 80 } @@ -220,7 +80,7 @@ function midiNoteOn(index: number, rawAttack?: number) { // Store state to ensure consistent note off. const info = midiKeyInfo(index) const whiteMode = midi.whiteMode - const indices = state.whiteIndices + const indices = scale.whiteIndices if (whiteMode === 'off') { tuningTableKeyOn(index) @@ -228,20 +88,20 @@ function midiNoteOn(index: number, rawAttack?: number) { if (info.whiteNumber === undefined) { frequency = NaN } else { - info.whiteNumber += state.whiteModeOffset - frequency = state.getFrequency(info.whiteNumber) + info.whiteNumber += scale.whiteModeOffset + frequency = scale.getFrequency(info.whiteNumber) tuningTableKeyOn(info.whiteNumber) } } else if (whiteMode === 'blackAverage') { if (info.whiteNumber === undefined) { - info.flatOf += state.whiteModeOffset - info.sharpOf += state.whiteModeOffset - frequency = Math.sqrt(state.getFrequency(info.flatOf) * state.getFrequency(info.sharpOf)) + info.flatOf += scale.whiteModeOffset + info.sharpOf += scale.whiteModeOffset + frequency = Math.sqrt(scale.getFrequency(info.flatOf) * scale.getFrequency(info.sharpOf)) tuningTableKeyOn(info.flatOf) tuningTableKeyOn(info.sharpOf) } else { - info.whiteNumber += state.whiteModeOffset - frequency = state.getFrequency(info.whiteNumber) + info.whiteNumber += scale.whiteModeOffset + frequency = scale.getFrequency(info.whiteNumber) tuningTableKeyOn(info.whiteNumber) } } else if (whiteMode === 'keyColors') { @@ -253,12 +113,12 @@ function midiNoteOn(index: number, rawAttack?: number) { if (index === indices[info.sharpOf + 1]) { frequency = NaN } else { - frequency = state.getFrequency(index) + frequency = scale.getFrequency(index) tuningTableKeyOn(index) } } else { index = indices[info.whiteNumber] - frequency = state.getFrequency(index) + frequency = scale.getFrequency(index) tuningTableKeyOn(index) } } else { @@ -335,7 +195,7 @@ watch( // === Virtual and typing keyboard === function keyboardNoteOn(index: number) { tuningTableKeyOn(index) - const noteOff = sendNoteOn(state.getFrequency(index), 80) + const noteOff = sendNoteOn(scale.getFrequency(index), 80) function keyOff() { tuningTableKeyOff(index) return noteOff(80) @@ -346,7 +206,7 @@ function keyboardNoteOn(index: number) { // === Typing keyboard state === function windowKeydownOrUp(event: KeyboardEvent | MouseEvent) { // Audio context must be initialized as a response to user gesture - audio.initialize() + setTimeout(() => audio.initialize(), 1) const target = event.target // Keep typing activated while adjusting sliders @@ -387,21 +247,21 @@ function windowKeydown(event: KeyboardEvent) { // "Octave" keys if (event.code === state.equaveUpCode) { - state.equaveShift++ + scale.equaveShift++ return } if (event.code === state.equaveDownCode) { - state.equaveShift-- + scale.equaveShift-- return } // "Transpose" keys if (event.code === state.degreeUpCode) { - state.degreeShift++ + scale.degreeShift++ return } if (event.code === state.degreeDownCode) { - state.degreeShift-- + scale.degreeShift-- return } @@ -431,13 +291,14 @@ function typingKeydown(event: CoordinateKeyboardEvent) { return emptyKeyup } - let index = state.baseMidiNote + state.scale.size * state.equaveShift + let index = scale.baseMidiNote + scale.scale.size * scale.equaveShift + scale.degreeShift - if (state.keyboardMode === 'isomorphic') { - index += state.degreeShift + x * state.isomorphicHorizontal + (2 - y) * state.isomorphicVertical + if (scale.keyboardMode === 'isomorphic') { + index += x * state.isomorphicHorizontal + (2 - y) * state.isomorphicVertical } else { - if (state.keyboardMapping.has(event.code)) { - index = state.keyboardMapping.get(event.code)! + if (scale.qwertyMapping.has(event.code)) { + // QWERTY mapping incorporates shifts + index = scale.qwertyMapping.get(event.code)! } else { // No user mapping for the key, bail out return emptyKeyup @@ -460,30 +321,33 @@ onMounted(() => { const url = new URL(window.location.href) const query = url.searchParams + // This is overriden when scale data is evaluated, but some corner cases need to be covered. + setNumberOfComponents(DEFAULT_NUMBER_OF_COMPONENTS) + // Special handling for the empty app state so that // the browser's back button can undo to the clean state. if (![...query.keys()].length) { router.push({ path: getPath(url), query: { version } }) - } - // Scale Workshop 1 compatibility - else if (!query.has('version')) { + } else if (!query.has('version')) { + // Scale Workshop 1 compatibility try { const scaleWorkshopOneData = new ScaleWorkshopOneData() - state.scaleName = scaleWorkshopOneData.name - state.scale.baseFrequency = scaleWorkshopOneData.freq - state.baseMidiNote = scaleWorkshopOneData.midi + scale.name = scaleWorkshopOneData.name + scale.baseFrequency = scaleWorkshopOneData.freq + scale.autoFrequency = false + scale.baseMidiNote = scaleWorkshopOneData.midi state.isomorphicHorizontal = scaleWorkshopOneData.horizontal state.isomorphicVertical = scaleWorkshopOneData.vertical - if (scaleWorkshopOneData.colors !== undefined) { - state.keyColors = scaleWorkshopOneData.colors.split(' ') - } if (scaleWorkshopOneData.data !== undefined) { - // Check that the scale is valid by attempting a parse - scaleWorkshopOneData.parseTuningData() - // Store raw text lines - state.scaleLines = scaleWorkshopOneData.data.split(NEWLINE_TEST) + const colors = scaleWorkshopOneData.colors ?? '' + const intervals = scaleWorkshopOneData.parseTuningData() + // Convert to raw text + const sourceLines = intervals.map((i) => i.toString()) + annotateColors(sourceLines, colors.split(' ')) + scale.sourceText = sourceLines.join('\n') + scale.computeScale() } audio.waveform = scaleWorkshopOneData.waveform || 'semisine' @@ -491,9 +355,67 @@ onMounted(() => { audio.decayTime = scaleWorkshopOneData.decayTime audio.sustainLevel = scaleWorkshopOneData.sustainLevel audio.releaseTime = scaleWorkshopOneData.releaseTime + + // Replace query with version 3. + router.push({ path: getPath(url), query: { version } }) } catch (error) { console.error('Error parsing version 1 URL', error) } + } else if (query.get('version')!.startsWith('2.')) { + // Scale Workshop 2 compatibility + try { + const decodedState = decodeQuery(query) + + let pianoMode: 'Asdf' | 'QweZxc' = 'Asdf' + if (decodedState.pianoMode === 'QweZxc0' || decodedState.pianoMode === 'QweZxc1') { + pianoMode = 'QweZxc' + } + + scale.name = decodedState.scaleName + scale.baseFrequency = decodedState.baseFrequency + scale.autoFrequency = false + scale.baseMidiNote = decodedState.baseMidiNote + state.isomorphicHorizontal = decodedState.isomorphicHorizontal + state.isomorphicVertical = decodedState.isomorphicVertical + scale.keyboardMode = decodedState.keyboardMode + scale.pianoMode = pianoMode + scale.equaveShift = decodedState.equaveShift + scale.degreeShift = decodedState.degreeShift + audio.waveform = decodedState.waveform + audio.attackTime = decodedState.attackTime + audio.decayTime = decodedState.decayTime + audio.sustainLevel = decodedState.sustainLevel + audio.releaseTime = decodedState.releaseTime + audio.pingPongDelayTime = decodedState.pingPongDelayTime + audio.pingPongFeedback = decodedState.pingPongFeedback + audio.pingPongSeparation = decodedState.pingPongSeparation + audio.pingPongGain = decodedState.pingPongGain + + // The decoder speaks Scale Workshop 2. Translate to SonicWeave. + const sourceLines: string[] = [] + const invalidLines: [string, number][] = [] + for (let i = 0; i < decodedState.scaleLines.length; ++i) { + const line = decodedState.scaleLines[i] + try { + const sourceLine = parseScaleWorkshop2Line(line, DEFAULT_NUMBER_OF_COMPONENTS).toString() + sourceLines.push(sourceLine) + } catch { + invalidLines.push([line, i]) + } + } + + annotateColors(sourceLines, decodedState.keyColors) + for (const [line, index] of invalidLines) { + sourceLines.splice(index, 0, '// ' + line) + } + scale.sourceText = sourceLines.join('\n') + scale.computeScale() + + // Replace query with version 3. + router.push({ path: getPath(url), query: { version } }) + } catch (error) { + console.error(`Error parsing version ${query.get('version')} URL`, error) + } } }) @@ -568,6 +490,7 @@ function panic() { diff --git a/src/components/ExporterButtons.vue b/src/components/ExporterButtons.vue new file mode 100644 index 00000000..58868255 --- /dev/null +++ b/src/components/ExporterButtons.vue @@ -0,0 +1,203 @@ + + + + diff --git a/src/components/GridLattice.vue b/src/components/GridLattice.vue new file mode 100644 index 00000000..30ec36e9 --- /dev/null +++ b/src/components/GridLattice.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/src/components/JustIntonationLattice.vue b/src/components/JustIntonationLattice.vue new file mode 100644 index 00000000..ea24e7d9 --- /dev/null +++ b/src/components/JustIntonationLattice.vue @@ -0,0 +1,201 @@ + + + diff --git a/src/components/ModalDialog.vue b/src/components/ModalDialog.vue index 8a44c14f..bf95d913 100644 --- a/src/components/ModalDialog.vue +++ b/src/components/ModalDialog.vue @@ -9,6 +9,10 @@ const props = defineProps({ extraStyle: { default: '', type: String + }, + right: { + default: false, + type: Boolean } }) @@ -73,7 +77,7 @@ watch( + diff --git a/src/components/modals/generation/EulerGenus.vue b/src/components/modals/generation/EulerGenus.vue index 2a33a0f8..1a753fb7 100644 --- a/src/components/modals/generation/EulerGenus.vue +++ b/src/components/modals/generation/EulerGenus.vue @@ -1,23 +1,36 @@ @@ -39,11 +52,29 @@ function generate() { v-model="modal.guideTone" /> +
+ + +
+ diff --git a/src/components/modals/generation/GeneratorSequence.vue b/src/components/modals/generation/GeneratorSequence.vue new file mode 100644 index 00000000..8b800799 --- /dev/null +++ b/src/components/modals/generation/GeneratorSequence.vue @@ -0,0 +1,195 @@ + + + diff --git a/src/components/modals/generation/HarmonicSeries.vue b/src/components/modals/generation/HarmonicSeries.vue index ec4c191d..7ba6da1d 100644 --- a/src/components/modals/generation/HarmonicSeries.vue +++ b/src/components/modals/generation/HarmonicSeries.vue @@ -1,28 +1,26 @@ @@ -55,5 +53,12 @@ function generate() { + diff --git a/src/components/modals/generation/HistoricalScale.vue b/src/components/modals/generation/HistoricalScale.vue index 5b320b23..7f9b2a3c 100644 --- a/src/components/modals/generation/HistoricalScale.vue +++ b/src/components/modals/generation/HistoricalScale.vue @@ -1,33 +1,31 @@ + diff --git a/src/views/AnalysisView.vue b/src/views/AnalysisView.vue index c6a7b541..80e5c3c4 100644 --- a/src/views/AnalysisView.vue +++ b/src/views/AnalysisView.vue @@ -1,21 +1,38 @@ @@ -236,9 +361,29 @@ main { border-collapse: collapse; text-align: center; } +.variety { + border-top: 2px solid; +} +.brightness { + border-left: 2px solid !important; +} +.violator { + color: red; +} +.highlight { + text-decoration: underline; +} +.violator.highlight { + background-color: rgba(255, 255, 100, 0.8); +} +.held { + background-color: var(--color-accent); + color: var(--color-accent-text); +} /* Content layout (medium) */ -div.columns-container { +div.columns-container, +div.bicolumns-container { column-count: 2; column-gap: 1rem; overflow: hidden; @@ -263,4 +408,9 @@ div.column { max-width: 100%; height: auto; } + +/* Equally tempered chord */ +.chord-data { + font-size: 1.2em; +} diff --git a/src/views/LatticeView.vue b/src/views/LatticeView.vue index d15c470c..f3dba428 100644 --- a/src/views/LatticeView.vue +++ b/src/views/LatticeView.vue @@ -1,35 +1,363 @@ - diff --git a/src/views/MidiView.vue b/src/views/MidiView.vue index 593a1578..9e15dabe 100644 --- a/src/views/MidiView.vue +++ b/src/views/MidiView.vue @@ -2,14 +2,14 @@ import { onMounted, onUnmounted, reactive, ref } from 'vue' import { Input, Output, WebMidi, type NoteMessageEvent, type MessageEvent } from 'webmidi' import MidiPiano from '@/components/MidiPiano.vue' -import { useStateStore } from '@/stores/state' import { useMidiStore } from '@/stores/midi' +import { useScaleStore } from '@/stores/scale' const props = defineProps<{ midiInputChannels: Set }>() -const state = useStateStore() +const scale = useScaleStore() const midi = useMidiStore() const inputs = reactive([]) @@ -204,10 +204,10 @@ onUnmounted(() => {
diff --git a/src/views/NotFoundView.vue b/src/views/NotFoundView.vue index 1a784be3..e58ea5a3 100644 --- a/src/views/NotFoundView.vue +++ b/src/views/NotFoundView.vue @@ -4,58 +4,20 @@ */ import OctaplexPortal from '@/components/modals/generation/SummonOctaplex.vue' -import { encodeQuery } from '@/url-encode' -import type { Scale } from 'scale-workshop-core' -import { nextTick, ref } from 'vue' -import { useRouter, type LocationQuery } from 'vue-router' -import { version } from '../../package.json' -import { useAudioStore } from '@/stores/audio' -import { useStateStore } from '@/stores/state' +import { useRouter } from 'vue-router' +import { useScaleStore } from '@/stores/scale' +import { ref } from 'vue' const ritualInProgress = ref(false) const router = useRouter() +const scale = useScaleStore() -const state = useStateStore() - -const audio = useAudioStore() - -function openTheGates(scale: Scale) { - state.scale = scale - - // Unfortunately we need to encode the state here. - // Simply navigating to "/" triggers decoding of the default empty state. - nextTick(() => { - const encodedState = { - scaleName: state.scaleName, - scaleLines: state.scaleLines, - baseFrequency: state.scale.baseFrequency, - baseMidiNote: state.baseMidiNote, - keyColors: state.keyColors, - isomorphicHorizontal: state.isomorphicHorizontal, - isomorphicVertical: state.isomorphicVertical, - keyboardMode: state.keyboardMode, - pianoMode: state.pianoMode, - equaveShift: state.equaveShift, - degreeShift: state.degreeShift, - - waveform: audio.waveform, - attackTime: audio.attackTime, - decayTime: audio.decayTime, - sustainLevel: audio.sustainLevel, - releaseTime: audio.releaseTime, - - pingPongDelayTime: audio.pingPongDelayTime, - pingPongFeedback: audio.pingPongFeedback, - pingPongSeparation: audio.pingPongSeparation, - pingPongGain: audio.pingPongGain - } - - const query = encodeQuery(encodedState) as LocationQuery - query.version = version - - router.push({ path: '/', query }) - }) +function openTheGates(source: string) { + scale.sourceText = source + scale.computeScale() + ritualInProgress.value = false + router.push({ path: '/' }) } @@ -71,8 +33,8 @@ function openTheGates(scale: Scale) { diff --git a/src/views/PreferencesView.vue b/src/views/PreferencesView.vue index d8f2ec0f..4210dcf2 100644 --- a/src/views/PreferencesView.vue +++ b/src/views/PreferencesView.vue @@ -2,8 +2,10 @@ import { UNIX_NEWLINE, WINDOWS_NEWLINE } from '@/constants' import { useStateStore } from '@/stores/state' +import { useScaleStore } from '@/stores/scale' const state = useStateStore() +const scale = useScaleStore()