diff --git a/README.md b/README.md
index 8d9b10e..80fe6d8 100644
--- a/README.md
+++ b/README.md
@@ -10,10 +10,12 @@ All programs go like this: `npx nx {package script} {package}`
## Test this workspace
Run `npx nx server player-web-test` to see the music player in a minimal demo.
+Run `npx nx server visualizer-web-test` to see the music player in a minimal demo.
## Build
Run `npx nx build player` to build the player.
+Run `npx nx build visualizer` to build the player.
## Publish
diff --git a/package-lock.json b/package-lock.json
index 287b505..66a7a33 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "@euterpe/source",
+ "name": "@euterpe.js/source",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "@euterpe/source",
+ "name": "@euterpe.js/source",
"version": "0.0.0",
"license": "MIT",
"dependencies": {
@@ -19,6 +19,7 @@
"@nx/vite": "^16.2.1",
"@nx/web": "^16.2.1",
"@nx/workspace": "16.2.1",
+ "@swc/core": "~1.3.51",
"@types/node": "^20.2.1",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
@@ -31,6 +32,7 @@
"jsdom": "~20.0.3",
"nx": "16.2.1",
"prettier": "^2.6.2",
+ "swc-loader": "0.1.15",
"typescript": "~5.0.2",
"vite": "^4.3.4",
"vite-plugin-eslint": "^1.8.1",
@@ -3010,6 +3012,200 @@
"node": ">= 8.0.0"
}
},
+ "node_modules/@swc/core": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.59.tgz",
+ "integrity": "sha512-ZBw31zd2E5SXiodwGvjQdx5ZC90b2uyX/i2LeMMs8LKfXD86pfOfQac+JVrnyEKDhASXj9icgsF9NXBhaMr3Kw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.3.59",
+ "@swc/core-darwin-x64": "1.3.59",
+ "@swc/core-linux-arm-gnueabihf": "1.3.59",
+ "@swc/core-linux-arm64-gnu": "1.3.59",
+ "@swc/core-linux-arm64-musl": "1.3.59",
+ "@swc/core-linux-x64-gnu": "1.3.59",
+ "@swc/core-linux-x64-musl": "1.3.59",
+ "@swc/core-win32-arm64-msvc": "1.3.59",
+ "@swc/core-win32-ia32-msvc": "1.3.59",
+ "@swc/core-win32-x64-msvc": "1.3.59"
+ },
+ "peerDependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.59.tgz",
+ "integrity": "sha512-AnqWFBgEKHP0jb4iZqx7eVQT9/rX45+DE4Ox7GpwCahUKxxrsDLyXzKhwLwQuAjUvtu5JcSB77szKpPGDM49fQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.59.tgz",
+ "integrity": "sha512-iqDs+yii9mOsmpJez82SEi4d4prWDRlapHxKnDVJ0x1AqRo41vIq8t3fujrvCHYU5VQgOYGh4ooXQpaP2H3B2A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.59.tgz",
+ "integrity": "sha512-PB0PP+SgkCSd/kYmltnPiGv42cOSaih1OjXCEjxvNwUFEmWqluW6uGdWaNiR1LoYMxhcHZTc336jL2+O3l6p0Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.59.tgz",
+ "integrity": "sha512-Ol/JPszWZ+OZ44FOdJe35TfJ1ckG4pYaisZJ4E7PzfwfVe2ygX85C5WWR4e5L0Y1zFvzpcI7gdyC2wzcXk4Cig==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.59.tgz",
+ "integrity": "sha512-PtTTtGbj9GiY5gJdoSFL2A0vL6BRaS1haAhp6g3hZvLDkTTg+rJURmzwBMMjaQlnGC62x/lLf6MoszHG/05//Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.59.tgz",
+ "integrity": "sha512-XBW9AGi0YsIN76IfesnDSBn/5sjR69J75KUNte8sH6seYlHJ0/kblqUMbUcfr0CiGoJadbzAZeKZZmfN7EsHpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.59.tgz",
+ "integrity": "sha512-Cy5E939SdWPQ34cg6UABNO0RyEe0FuWqzZ/GLKtK11Ir4fjttVlucZiY59uQNyUVUc8T2qE0VBFCyD/zYGuHtg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.59.tgz",
+ "integrity": "sha512-z5ZJxizRvRoSAaevRIi3YjQh74OFWEIhonSDWNdqDL7RbjEivcatYcG7OikH6s+rtPhOcwNm3PbGV2Prcgh/gg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.59.tgz",
+ "integrity": "sha512-vxpsn+hrKAhi5YusQfB/JXUJJVX40rIRE/L49ilBEqdbH8Khkoego6AD+2vWqTdJcUHo1WiAIAEZ0rTsjyorLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.3.59",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.59.tgz",
+ "integrity": "sha512-Ris/cJbURylcLwqz4RZUUBCEGsuaIHOJsvf69W5pGKHKBryVoOTNhBKpo3Km2hoAi5qFQ/ou0trAT4hBsVPZvQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
@@ -3989,6 +4185,15 @@
"tweetnacl": "^0.14.3"
}
},
+ "node_modules/big.js": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -4947,6 +5152,15 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
+ "node_modules/emojis-list": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
+ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -6705,6 +6919,20 @@
}
}
},
+ "node_modules/loader-utils": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+ "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
"node_modules/local-pkg": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz",
@@ -8276,6 +8504,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/swc-loader": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.1.15.tgz",
+ "integrity": "sha512-cn1WPIeQJvXM4bbo3OwdEIapsQ4uUGOfyFj0h2+2+brT0k76DCGnZXDE2KmcqTd2JSQ+b61z2NPMib7eEwMYYw==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@swc/core": "^1.2.52",
+ "webpack": ">=2"
+ }
+ },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
diff --git a/package.json b/package.json
index 2dcedcc..a056b8a 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,8 @@
"version": "0.0.0",
"license": "MIT",
"scripts": {
- "publish-player": "cd dist/packages/player && npm publish --access=public"
+ "publish-player": "cd dist/packages/player && npm publish --access=public",
+ "publish-visualizer": "cd dist/packages/visualizer && npm publish --access=public"
},
"private": false,
"devDependencies": {
@@ -14,6 +15,7 @@
"@nx/vite": "^16.2.1",
"@nx/web": "^16.2.1",
"@nx/workspace": "16.2.1",
+ "@swc/core": "~1.3.51",
"@types/node": "^20.2.1",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
@@ -26,6 +28,7 @@
"jsdom": "~20.0.3",
"nx": "16.2.1",
"prettier": "^2.6.2",
+ "swc-loader": "0.1.15",
"typescript": "~5.0.2",
"vite": "^4.3.4",
"vite-plugin-eslint": "^1.8.1",
diff --git a/packages/visualizer-web-test/.eslintrc.json b/packages/visualizer-web-test/.eslintrc.json
new file mode 100644
index 0000000..5313267
--- /dev/null
+++ b/packages/visualizer-web-test/.eslintrc.json
@@ -0,0 +1,33 @@
+{
+ "extends": [
+ "../../.eslintrc.json"
+ ],
+ "ignorePatterns": [
+ "!**/*"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ "*.js",
+ "*.jsx"
+ ],
+ "rules": {}
+ },
+ {
+ "files": [
+ "*.ts",
+ "*.tsx"
+ ],
+ "rules": {}
+ },
+ {
+ "files": [
+ "*.js",
+ "*.jsx"
+ ],
+ "rules": {}
+ }
+ ]
+}
diff --git a/packages/visualizer-web-test/index.html b/packages/visualizer-web-test/index.html
new file mode 100644
index 0000000..e0241e8
--- /dev/null
+++ b/packages/visualizer-web-test/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+ VisualizerWebTest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-:--
+
+
-:--
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/visualizer-web-test/project.json b/packages/visualizer-web-test/project.json
new file mode 100644
index 0000000..04d7bb6
--- /dev/null
+++ b/packages/visualizer-web-test/project.json
@@ -0,0 +1,70 @@
+{
+ "name": "visualizer-web-test",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "packages/visualizer-web-test/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@nx/vite:build",
+ "outputs": [
+ "{options.outputPath}"
+ ],
+ "defaultConfiguration": "production",
+ "options": {
+ "outputPath": "dist/packages/visualizer-web-test"
+ },
+ "configurations": {
+ "development": {
+ "mode": "development"
+ },
+ "production": {
+ "mode": "production"
+ }
+ }
+ },
+ "serve": {
+ "executor": "@nx/vite:dev-server",
+ "defaultConfiguration": "development",
+ "options": {
+ "buildTarget": "visualizer-web-test:build"
+ },
+ "configurations": {
+ "development": {
+ "buildTarget": "visualizer-web-test:build:development",
+ "hmr": true
+ },
+ "production": {
+ "buildTarget": "visualizer-web-test:build:production",
+ "hmr": false
+ }
+ }
+ },
+ "preview": {
+ "executor": "@nx/vite:preview-server",
+ "defaultConfiguration": "development",
+ "options": {
+ "buildTarget": "visualizer-web-test:build"
+ },
+ "configurations": {
+ "development": {
+ "buildTarget": "visualizer-web-test:build:development"
+ },
+ "production": {
+ "buildTarget": "visualizer-web-test:build:production"
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": [
+ "{options.outputFile}"
+ ],
+ "options": {
+ "lintFilePatterns": [
+ "packages/visualizer-web-test/**/*.ts"
+ ]
+ }
+ }
+ }
+}
diff --git a/packages/visualizer-web-test/public/favicon.ico b/packages/visualizer-web-test/public/favicon.ico
new file mode 100644
index 0000000..317ebcb
Binary files /dev/null and b/packages/visualizer-web-test/public/favicon.ico differ
diff --git a/packages/visualizer-web-test/public/nuphory - NVISION (EXTENDED MIX).ogg b/packages/visualizer-web-test/public/nuphory - NVISION (EXTENDED MIX).ogg
new file mode 100644
index 0000000..c2836c3
Binary files /dev/null and b/packages/visualizer-web-test/public/nuphory - NVISION (EXTENDED MIX).ogg differ
diff --git a/packages/visualizer-web-test/src/assets/.gitkeep b/packages/visualizer-web-test/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/packages/visualizer-web-test/src/main.ts b/packages/visualizer-web-test/src/main.ts
new file mode 100644
index 0000000..fb1014e
--- /dev/null
+++ b/packages/visualizer-web-test/src/main.ts
@@ -0,0 +1,82 @@
+import { MusicPlayerBuilder } from "@euterpe/player";
+import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer";
+const audio_el = document.querySelector("#audio") as HTMLAudioElement
+const music_player_builder = MusicPlayerBuilder(audio_el)
+music_player_builder.start()
+const analyser_node = music_player_builder.add_analyser()
+const music_player = music_player_builder.build()
+music_player.change_volume(1)
+
+
+
+const trapnation_visual_builder = AudioVisualBuilder(analyser_node, document.querySelector("#trapnation-canvas") as SVGSVGElement)
+trapnation_visual_builder.start()
+trapnation_visual_builder.set_fft_size(8192)
+trapnation_visual_builder.set_fft_data_tresholds({ to_fft_range_i: 3, point_count_i: 50, fft_multiplier_i: 1, fft_offset_i: -80 })
+trapnation_visual_builder.set_fft_time_smoothing(0.6)
+//If not using typescript enums, CatmullRom = number 2
+trapnation_visual_builder.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
+const trapnation_visual = trapnation_visual_builder.build(ShapeType.Circle)
+
+const bar_visual_builder = AudioVisualBuilder(analyser_node, document.querySelector("#bar-canvas") as SVGSVGElement)
+bar_visual_builder.start()
+bar_visual_builder.set_fft_data_tresholds({ to_fft_range_i: 10, point_count_i: 10, fft_multiplier_i: 1.5, fft_offset_i: 50 })
+bar_visual_builder.set_fft_time_smoothing(0.8)
+//If not using typescript enums, CatmullRom = number 2
+bar_visual_builder.set_smoothing_algorythm(SmoothingAlgorythm.Linear)
+const bar_visual = trapnation_visual_builder.build(ShapeType.Line)
+
+
+trapnation_visual.draw()
+bar_visual.draw()
+/*
+ * The player part
+ */
+music_player.try_new_song_async(encodeURI("http://127.0.0.1:4200/nuphory - NVISION (EXTENDED MIX).ogg"))
+ .then(() => {
+ let is_seeking = false
+ document.querySelector("#play")?.addEventListener("click", () => {
+ music_player.play_async()
+ .then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
+ })
+ document.querySelector("#pause")?.addEventListener("click", () => {
+ music_player.pause()
+ })
+ document.querySelector("#mute")?.addEventListener("click", () => {
+ music_player.mute()
+ })
+ document.querySelector("#unmute")?.addEventListener("click", () => {
+ music_player.unmute()
+ })
+ document.querySelector("#toggle-mute")?.addEventListener("click", () => {
+ music_player.mute_toggle()
+ })
+ document.querySelector("#toggle-play")?.addEventListener("click", () => {
+ music_player.play_toggle_async().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e))
+ })
+ document.querySelector("#volume")?.addEventListener("input", (e) => {
+ music_player.change_volume(e.target?.valueAsNumber)
+ })
+ document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
+ is_seeking = true;
+ })
+ document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
+ music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => {
+ alert("Failed seeking! " + e)
+ })
+ is_seeking = false
+ })
+ // Subscriptions to AudioContext changes, eg. time..
+ music_player.subscribe_to_formatted_duration_time((time) => {
+ document.querySelector("#duration").innerHTML = time
+ document.querySelector("#seek").max = "" + music_player.get_current_duration()
+ })
+ music_player.subscribe_to_formatted_current_time_tick((time) => {
+ document.querySelector("#current").innerHTML = time
+ })
+ music_player.subscribe_to_time_tick((time) => {
+ if (is_seeking) return
+ document.querySelector("#seek").value = "" + time
+ })
+
+ }, (e) => console.log(e))
\ No newline at end of file
diff --git a/packages/visualizer-web-test/src/styles.css b/packages/visualizer-web-test/src/styles.css
new file mode 100644
index 0000000..302751e
--- /dev/null
+++ b/packages/visualizer-web-test/src/styles.css
@@ -0,0 +1,30 @@
+#volume{
+ transform: rotate(270deg);
+}
+body {
+ width: 100vw;
+ height:100vh;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+.player-wrapper{
+ width:50vw;
+ display: flex;
+ justify-content: space-between;
+}
+.viz-wrapper{
+ width:50vw;
+ display: flex;
+ justify-content: space-between;
+}
+#trapnation-canvas{
+ width:100%;
+ fill: grey;
+ stroke: black;
+}
+.wrapper div{
+ width:100%;
+}
\ No newline at end of file
diff --git a/packages/visualizer-web-test/tsconfig.json b/packages/visualizer-web-test/tsconfig.json
new file mode 100644
index 0000000..85e7800
--- /dev/null
+++ b/packages/visualizer-web-test/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "files": [],
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": [
+ "ESNext",
+ "DOM"
+ ],
+ "moduleResolution": "Node",
+ "strict": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "noEmit": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "skipLibCheck": true,
+ "types": [
+ "vite/client"
+ ]
+ },
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file
diff --git a/packages/visualizer-web-test/vite.config.ts b/packages/visualizer-web-test/vite.config.ts
new file mode 100644
index 0000000..5a37f34
--- /dev/null
+++ b/packages/visualizer-web-test/vite.config.ts
@@ -0,0 +1,33 @@
+///
+import { defineConfig } from "vite"
+
+import viteTsConfigPaths from "vite-tsconfig-paths"
+
+export default defineConfig({
+ cacheDir: "../../node_modules/.vite/visualizer-web-test",
+
+ server: {
+ port: 4200,
+ host: "localhost"
+ },
+
+ preview: {
+ port: 4300,
+ host: "localhost"
+ },
+
+ plugins: [
+ viteTsConfigPaths({
+ root: "../../"
+ })
+ ]
+
+ // Uncomment this if you are using workers.
+ // worker: {
+ // plugins: [
+ // viteTsConfigPaths({
+ // root: '../../',
+ // }),
+ // ],
+ // },
+})
diff --git a/packages/visualizer/.eslintrc.json b/packages/visualizer/.eslintrc.json
new file mode 100644
index 0000000..5313267
--- /dev/null
+++ b/packages/visualizer/.eslintrc.json
@@ -0,0 +1,33 @@
+{
+ "extends": [
+ "../../.eslintrc.json"
+ ],
+ "ignorePatterns": [
+ "!**/*"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ "*.js",
+ "*.jsx"
+ ],
+ "rules": {}
+ },
+ {
+ "files": [
+ "*.ts",
+ "*.tsx"
+ ],
+ "rules": {}
+ },
+ {
+ "files": [
+ "*.js",
+ "*.jsx"
+ ],
+ "rules": {}
+ }
+ ]
+}
diff --git a/packages/visualizer/README.md b/packages/visualizer/README.md
new file mode 100644
index 0000000..0446fae
--- /dev/null
+++ b/packages/visualizer/README.md
@@ -0,0 +1,7 @@
+# Visualizer
+
+Euterpe Visualizer is an extensive Audio visual library for the web. Using SVG elements instead of canvas, it allows to leverage the power of GPU to do the actual drawing, whilst allowing and ease of use as opposed to the canvas counter part.
+
+## Building
+
+Run `nx build visualizer` to build the library.
\ No newline at end of file
diff --git a/packages/visualizer/package.json b/packages/visualizer/package.json
new file mode 100644
index 0000000..6019132
--- /dev/null
+++ b/packages/visualizer/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@euterpe/visualizer",
+ "version": "0.0.1",
+ "type": "commonjs"
+}
diff --git a/packages/visualizer/project.json b/packages/visualizer/project.json
new file mode 100644
index 0000000..0bc0823
--- /dev/null
+++ b/packages/visualizer/project.json
@@ -0,0 +1,40 @@
+{
+ "name": "visualizer",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "packages/visualizer/src",
+ "projectType": "library",
+ "targets": {
+ "build": {
+ "executor": "@nx/js:tsc",
+ "outputs": [
+ "{options.outputPath}"
+ ],
+ "options": {
+ "outputPath": "dist/packages/visualizer",
+ "main": "packages/visualizer/src/index.ts",
+ "tsConfig": "packages/visualizer/tsconfig.lib.json",
+ "assets": [
+ "packages/visualizer/*.md"
+ ]
+ }
+ },
+ "publish": {
+ "command": "node tools/scripts/publish.mjs visualizer {args.ver} {args.tag}",
+ "dependsOn": [
+ "build"
+ ]
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": [
+ "{options.outputFile}"
+ ],
+ "options": {
+ "lintFilePatterns": [
+ "packages/visualizer/**/*.ts"
+ ]
+ }
+ }
+ },
+ "tags": []
+}
diff --git a/packages/visualizer/src/index.ts b/packages/visualizer/src/index.ts
new file mode 100644
index 0000000..1cd297b
--- /dev/null
+++ b/packages/visualizer/src/index.ts
@@ -0,0 +1,348 @@
+export enum SmoothingAlgorythm {
+ Linear,
+ BezierPerpendicular,
+ CatmullRom,
+ BezierWeighted,
+}
+export enum ShapeType {
+ Circle,
+ Line,
+ /*To be Implmeneted
+ Waveform,
+ FullSongWaveForm
+ */
+}
+type Point = {
+ x: number,
+ y: number,
+}
+type Shape = {
+ shape_type: ShapeType,
+ //Algo-rythm, because this is about music. Get it? xd
+ smoothing_algorythm: SmoothingAlgorythm
+ points: Point[]
+}
+
+export function AudioVisual(
+ analyzer_node: AnalyserNode,
+ svg_injecting_element: SVGSVGElement,
+ shape: Shape,
+ buffer_length: number,
+ fft_multiplier: number,
+ fft_offset: number,
+ from_fft_range: number,
+ to_fft_range: number,
+ point_count: number,
+ canvas_height: number,
+ canvas_width: number,
+) {
+ let fft_data = new Float32Array(buffer_length)
+
+ function get_cured_frequency_data() {
+ fft_data = new Float32Array(buffer_length)
+ analyzer_node.getFloatFrequencyData(fft_data)
+ const from = Math.round((point_count / 100) * from_fft_range)
+ const to = Math.round(buffer_length - (buffer_length / 100) * to_fft_range)
+ const squeeze_factor = Math.round((buffer_length - to) / point_count)
+
+ const return_array = new Array(point_count)
+ for (let i = 0; i < point_count + 1; i++) {
+ return_array[i] = fft_data[from + i * squeeze_factor]
+ }
+ return return_array
+ }
+
+ function normalise_perpendicular_anchors(x: number, y: number) {
+ const magnitude = Math.sqrt(x * x + y * y)
+ return [x / magnitude, y / magnitude]
+ }
+
+ function create_perpendicular_anchors(arr: { x: number, y: number }[]) {
+ const anchors = []
+ switch (shape.shape_type) {
+ case ShapeType.Circle: {
+ const pointDistance = 7
+ for (let curPoint = 0; curPoint < arr.length; curPoint++) {
+ const [dx, dy] = normalise_perpendicular_anchors(arr[curPoint].x, arr[curPoint].y)
+ const perpendicular = [-dy, dx]
+ anchors.push({
+ leftAnchor: {
+ x: arr[curPoint].x + pointDistance * perpendicular[0],
+ y: arr[curPoint].y + pointDistance * perpendicular[1],
+ },
+ rightAnchor: {
+ x: arr[curPoint].x - pointDistance * perpendicular[0],
+ y: arr[curPoint].y - pointDistance * perpendicular[1],
+ },
+ })
+ }
+ break
+ }
+ case ShapeType.Line: {
+ const pointDistance = canvas_width / arr.length
+ for (let curPoint = 0; curPoint < arr.length; curPoint++) {
+ anchors.push({
+ leftAnchor: {
+ x: pointDistance * curPoint - pointDistance / 3,
+ y: arr[curPoint].y,
+ },
+ rightAnchor: {
+ x: pointDistance * curPoint + pointDistance / 3,
+ y: arr[curPoint].y,
+ },
+ })
+ }
+ }
+ }
+
+ return anchors
+ }
+
+ function catmull_rom_smooth(arr: { x: number, y: number }[], k: number) {
+ if (k == null) k = 1
+ const last = arr.length - 2
+
+ let path = "M" + [arr[0].x, arr[0].y]
+
+ for (let i = 0; i < arr.length - 1; i++) {
+
+ const x0 = i ? arr[i - 1].x : arr[0].x
+ const y0 = i ? arr[i - 1].y : arr[0].y
+
+ const x1 = arr[i].x
+ const y1 = arr[i].y
+
+ const x2 = arr[i + 1].x
+ const y2 = arr[i + 1].y
+
+ let subx = y2
+ let suby = y2
+ //Makes the last line before Z a bit less jarring
+ if (shape.shape_type == ShapeType.Circle) {
+ subx = arr[0].x
+ suby = arr[0].y
+ }
+ const x3 = i !== last ? arr[i + 2].x : subx
+ const y3 = i !== last ? arr[i + 2].y : suby
+
+ const cp1x = x1 + (x2 - x0) / 6 * k
+ const cp1y = y1 + (y2 - y0) / 6 * k
+
+ const cp2x = x2 - (x3 - x1) / 6 * k
+ const cp2y = y2 - (y3 - y1) / 6 * k
+
+ path += "C" + [cp1x, cp1y, cp2x, cp2y, x2, y2]
+ }
+ path += " Z"
+ return path
+ }
+
+ function mutate_points() {
+ const mutated_points = []
+ const frequency_data = get_cured_frequency_data()
+ const out_range = [0, canvas_height]
+ const in_range = [-165, -30]
+ switch (shape.shape_type) {
+ case ShapeType.Line: {
+ for (let i = 0; i < frequency_data.length - 1; i++) {
+ mutated_points.push({
+ x: shape.points[i].x /** ((Math.max(FFTDataArray[i] + 100)) * 4)*/,
+ y: shape.points[i].y - convert_range(frequency_data[i] * fft_multiplier + fft_offset, in_range, out_range),
+ })
+ }
+ break
+ }
+ case ShapeType.Circle: {
+ for (let i = 0; i < frequency_data.length - 1; i++) {
+ const new_i = i > (frequency_data.length - 1) / 2 ? frequency_data.length - 1 - i : i
+ mutated_points.push({
+ x: shape.points[i].x * Math.max((frequency_data[new_i] * fft_multiplier + fft_offset) / 50, 1) + canvas_width / 2,
+ y: shape.points[i].y * Math.max((frequency_data[new_i] * fft_multiplier + fft_offset) / 50, 1) + canvas_height / 2,
+ })
+ /* TODO: IMPLEMENT SCALING TO BEAT
+ this.injectingHTMLElement.parentElement.style.transform = `scale(${(100 + Math.max((frequencyData[2] * 2 + 130) / 5, 1)) / 100})`
+ */
+ }
+
+ break
+ }
+ }
+ return mutated_points
+ }
+
+ function convert_range(value: number, r1: number[], r2: number[]) {
+ if (!isFinite(value)) return 0
+ return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0]
+ }
+
+ function create_svg_element() {
+ let path
+ const arr = mutate_points()
+ switch (shape.shape_type) {
+ case ShapeType.Line: {
+ path = `M ${0} ${canvas_height} `
+ break
+ }
+ case ShapeType.Circle: {
+ path = `M ${arr[0].x} ${arr[0].y} `
+ }
+ }
+ switch (shape.smoothing_algorythm) {
+ case SmoothingAlgorythm.Linear: {
+ for (let i = 0; i < arr.length; i++) {
+ path += `L ${arr[i].x},${arr[i].y} `
+ }
+ if (shape.shape_type == ShapeType.Line) {
+ path += `L ${this.canvasWidth} ${this.canvasHeight / 2} `
+ //path += `L ${canvas_width} ${canvas_height} `
+ }
+ path += `Z `
+ break
+ }
+
+ case SmoothingAlgorythm.BezierPerpendicular: {
+ const anchors = create_perpendicular_anchors(arr)
+
+ for (let i = 1; i < arr.length; i++) {
+ path += `C ${anchors[i - 1].rightAnchor.x} ${anchors[i - 1].rightAnchor.y} ${anchors[i].leftAnchor.x} ${anchors[i].leftAnchor.y} ${arr[i].x} ${arr[i].y} `
+ }
+ if (shape.shape_type == ShapeType.Line) {
+ //path += `L ${this.canvasWidth} ${this.canvasHeight / 2} `
+ path += `L ${canvas_width} ${canvas_height} `
+ }
+ path += `Z `
+ break
+ }
+
+ case SmoothingAlgorythm.BezierWeighted: {
+ /*THIS IS VERY MUCH BROKEN ATM :(
+ for (let i = 2; i < arr.length; i++) {
+ const end = [arr.x[i], arr.y[i]] // the current point is the end of this segment of the curve
+ path += `C ${startControl[0]} ${startControl[1]} ${endControl[0]} ${endControl[1]} ${end[0]} ${end[1]}`
+ }*/
+ console.error("BezierWeighted not implemented yet...")
+ break
+ }
+ case SmoothingAlgorythm.CatmullRom: {
+ path = catmull_rom_smooth(arr, 1)
+ break
+ }
+ }
+ return ``
+ }
+
+ function draw() {
+ analyzer_node.getFloatFrequencyData(fft_data)
+ svg_injecting_element.innerHTML = create_svg_element()
+ requestAnimationFrame(draw.bind(AudioVisual))
+ }
+
+ return {
+ draw,
+ }
+}
+export function AudioVisualBuilder(analyzer_node: AnalyserNode, svg_injecting_element: SVGSVGElement) {
+ let canvas_height: number
+ let canvas_width: number
+ let buffer_length = analyzer_node.frequencyBinCount
+ let smoothing_algorythm = SmoothingAlgorythm.Linear
+ let fft_time_smoothing = 0.1
+ let fft_size = 4096
+ let fft_multipier = 1.5
+ let fft_offset = -50
+ let from_fft_range = 0
+ let to_fft_range = 100
+ let point_count: number
+ let scale_to_beat = false
+ let shape: Shape
+
+ function start() {
+ canvas_width = svg_injecting_element.viewBox.baseVal.width // viewbox does exist on svg element, ignore error...
+ canvas_height = svg_injecting_element.viewBox.baseVal.height
+ return this
+ }
+
+ function set_fft_time_smoothing(fft_time_smoothing_i: number) {
+ analyzer_node.smoothingTimeConstant = fft_time_smoothing = fft_time_smoothing_i
+ return this
+ }
+ function set_fft_size(fft_size_i: number) {
+ if (!(fft_size && !(fft_size_i & (fft_size_i - 1)))) throw Error("fft_size not power of two")
+ analyzer_node.fftSize = fft_size = fft_size_i
+ buffer_length = analyzer_node.frequencyBinCount
+ return this
+ }
+ function set_fft_data_tresholds({ from_fft_range_i = 0, to_fft_range_i = 100, point_count_i = Math.round((buffer_length / 100) * (from_fft_range_i - to_fft_range_i)), fft_multiplier_i = 2, fft_offset_i = -50 }) {
+ from_fft_range = from_fft_range_i
+ to_fft_range = to_fft_range_i
+ point_count = point_count_i
+ fft_multipier = fft_multiplier_i
+ fft_offset = fft_offset_i
+ return this
+ }
+ function set_smoothing_algorythm(algorythm: SmoothingAlgorythm) {
+ smoothing_algorythm = algorythm
+ return this
+ }
+ function enable_scaling_to_beat(enable = false) {
+ scale_to_beat = enable
+ }
+ function create_shape(shape_type: ShapeType): Shape {
+ const point_amount = get_cured_frequency_data(analyzer_node, buffer_length, from_fft_range, to_fft_range, point_count).length
+ let new_shape: Shape
+ switch (shape_type) {
+ case ShapeType.Line: {
+ const points = []
+ for (let i = 0; i < point_amount; i++) {
+ points.push({
+ x: (canvas_width / point_amount) * i,
+ y: canvas_height / 2 - (0 / point_amount) * i,
+ })
+ }
+ new_shape = { shape_type, points, smoothing_algorythm }
+ break
+ }
+ case ShapeType.Circle: {
+ const points = []
+ const radius = canvas_height > canvas_width ? canvas_height / 5 : canvas_width / 5
+ for (let i = 0; i < point_amount; i++) {
+ points.push({
+ x: Math.cos(((2 * Math.PI) / point_amount) * i - Math.PI / 2) * radius,
+ y: Math.sin(((2 * Math.PI) / point_amount) * i - Math.PI / 2) * radius,
+ })
+ }
+
+ new_shape = { shape_type, points, smoothing_algorythm }
+ break
+ }
+ }
+ shape = new_shape
+ return this
+ }
+ function get_cured_frequency_data(analyzer_node: AnalyserNode, buffer_length: number, from_range: number, to_range: number, point_count: number) {
+ const fft_data_array = new Float32Array(buffer_length)
+ analyzer_node.getFloatFrequencyData(fft_data_array)
+ const from = Math.round((point_count / 100) * from_range)
+ const to = Math.round(buffer_length - (buffer_length / 100) * to_range)
+ const squeezeFactor = Math.round((buffer_length - to) / point_count)
+
+ const return_array = new Array(point_count)
+ for (let i = 0; i < point_count; i++) {
+ return_array[i] = fft_data_array[from + i * squeezeFactor]
+ }
+ return return_array
+ }
+ function build(shape_type: ShapeType) {
+ create_shape(shape_type)
+ return AudioVisual(analyzer_node, svg_injecting_element, shape, buffer_length, fft_multipier, fft_offset, from_fft_range, to_fft_range, point_count, canvas_height, canvas_width)
+ }
+ return {
+ start,
+ set_fft_size,
+ enable_scaling_to_beat,
+ set_fft_time_smoothing,
+ set_fft_data_tresholds,
+ set_smoothing_algorythm,
+ build
+ }
+}
diff --git a/packages/visualizer/src/lib/visualizer.ts b/packages/visualizer/src/lib/visualizer.ts
new file mode 100644
index 0000000..f1a4a45
--- /dev/null
+++ b/packages/visualizer/src/lib/visualizer.ts
@@ -0,0 +1,3 @@
+export function visualizer(): string {
+ return "visualizer"
+}
diff --git a/packages/visualizer/tsconfig.json b/packages/visualizer/tsconfig.json
new file mode 100644
index 0000000..b91a60c
--- /dev/null
+++ b/packages/visualizer/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "commonjs",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noImplicitThis": false,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/visualizer/tsconfig.lib.json b/packages/visualizer/tsconfig.lib.json
new file mode 100644
index 0000000..33eca2c
--- /dev/null
+++ b/packages/visualizer/tsconfig.lib.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "declaration": true,
+ "types": ["node"]
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 08e06dc..b47a2d1 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -20,6 +20,9 @@
"paths": {
"@euterpe/player": [
"packages/player/src/index.ts"
+ ],
+ "@euterpe/visualizer": [
+ "packages/visualizer/src/index.ts"
]
}
},