create analyzer and analyzer web test

This commit is contained in:
Djkato 2023-05-24 15:23:58 +02:00
parent 5d672c273e
commit 5dc02bf2e3
22 changed files with 1033 additions and 3 deletions

View file

@ -10,10 +10,12 @@ All programs go like this: `npx nx {package script} {package}`
## Test this workspace ## Test this workspace
Run `npx nx server player-web-test` to see the music player in a minimal demo. 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 ## Build
Run `npx nx build player` to build the player. Run `npx nx build player` to build the player.
Run `npx nx build visualizer` to build the player.
## Publish ## Publish

245
package-lock.json generated
View file

@ -1,11 +1,11 @@
{ {
"name": "@euterpe/source", "name": "@euterpe.js/source",
"version": "0.0.0", "version": "0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@euterpe/source", "name": "@euterpe.js/source",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -19,6 +19,7 @@
"@nx/vite": "^16.2.1", "@nx/vite": "^16.2.1",
"@nx/web": "^16.2.1", "@nx/web": "^16.2.1",
"@nx/workspace": "16.2.1", "@nx/workspace": "16.2.1",
"@swc/core": "~1.3.51",
"@types/node": "^20.2.1", "@types/node": "^20.2.1",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0", "@typescript-eslint/parser": "^5.58.0",
@ -31,6 +32,7 @@
"jsdom": "~20.0.3", "jsdom": "~20.0.3",
"nx": "16.2.1", "nx": "16.2.1",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"swc-loader": "0.1.15",
"typescript": "~5.0.2", "typescript": "~5.0.2",
"vite": "^4.3.4", "vite": "^4.3.4",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
@ -3010,6 +3012,200 @@
"node": ">= 8.0.0" "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": { "node_modules/@swc/helpers": {
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
@ -3989,6 +4185,15 @@
"tweetnacl": "^0.14.3" "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": { "node_modules/binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -4947,6 +5152,15 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true "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": { "node_modules/end-of-stream": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "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": { "node_modules/local-pkg": {
"version": "0.4.3", "version": "0.4.3",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz",
@ -8276,6 +8504,19 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/symbol-tree": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",

View file

@ -3,7 +3,8 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"scripts": { "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, "private": false,
"devDependencies": { "devDependencies": {
@ -14,6 +15,7 @@
"@nx/vite": "^16.2.1", "@nx/vite": "^16.2.1",
"@nx/web": "^16.2.1", "@nx/web": "^16.2.1",
"@nx/workspace": "16.2.1", "@nx/workspace": "16.2.1",
"@swc/core": "~1.3.51",
"@types/node": "^20.2.1", "@types/node": "^20.2.1",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0", "@typescript-eslint/parser": "^5.58.0",
@ -26,6 +28,7 @@
"jsdom": "~20.0.3", "jsdom": "~20.0.3",
"nx": "16.2.1", "nx": "16.2.1",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"swc-loader": "0.1.15",
"typescript": "~5.0.2", "typescript": "~5.0.2",
"vite": "^4.3.4", "vite": "^4.3.4",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",

View file

@ -0,0 +1,33 @@
{
"extends": [
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>VisualizerWebTest</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div class="viz-wrapper">
<svg id="trapnation-canvas" viewBox="0 0 500 500" preserveAspectRatio="none"></svg>
<svg id="bar-canvas" viewBox="0 0 500 500" preserveAspectRatio="none"></svg>
</div>
<div class="player-wrapper">
<audio src="" id="audio"></audio>
<button id="play">Play</button>
<button id="pause">Pause</button>
<button id="toggle-play">Toggle Pause/Play</button>
<p id="current">-:--</p>
<input type="range" min="0" max="10" value="0" id="seek" step="0.01">
<p id="duration">-:--</p>
<span>
<input type="range" min="0" max="1" value="1" id="volume" step="0.01">
</span>
<button id="mute">Mute</button>
<button id="unmute">Unmute</button>
<button id="toggle-mute">Toggle Mute</button>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -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"
]
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -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))

View file

@ -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%;
}

View file

@ -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"
]
}

View file

@ -0,0 +1,33 @@
/// <reference types="vitest" />
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: '../../',
// }),
// ],
// },
})

View file

@ -0,0 +1,33 @@
{
"extends": [
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View file

@ -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.

View file

@ -0,0 +1,5 @@
{
"name": "@euterpe/visualizer",
"version": "0.0.1",
"type": "commonjs"
}

View file

@ -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": []
}

View file

@ -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 `<path width="100%" height="100%" d="${path}"/>`
}
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
}
}

View file

@ -0,0 +1,3 @@
export function visualizer(): string {
return "visualizer"
}

View file

@ -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"
}
]
}

View file

@ -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"]
}

View file

@ -20,6 +20,9 @@
"paths": { "paths": {
"@euterpe/player": [ "@euterpe/player": [
"packages/player/src/index.ts" "packages/player/src/index.ts"
],
"@euterpe/visualizer": [
"packages/visualizer/src/index.ts"
] ]
} }
}, },