Init monorepo n move player to it, add test
This commit is contained in:
parent
b3c176946c
commit
5de339ff47
30 changed files with 5929 additions and 30 deletions
|
@ -3,8 +3,8 @@ root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = tab
|
||||||
indent_size = 2
|
indent_size = 4
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
|
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
56
.eslintrc.json
Normal file
56
.eslintrc.json
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@nx"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx",
|
||||||
|
"*.js",
|
||||||
|
"*.jsx"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@nx/enforce-module-boundaries": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"enforceBuildableLibDependency": true,
|
||||||
|
"allow": [],
|
||||||
|
"depConstraints": [
|
||||||
|
{
|
||||||
|
"sourceTag": "*",
|
||||||
|
"onlyDependOnLibsWithTags": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@nx/typescript"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.js",
|
||||||
|
"*.jsx"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@nx/javascript"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,7 +7,7 @@ tmp
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
package-lock.json
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
/.idea
|
||||||
.project
|
.project
|
||||||
|
|
13
.prettierrc
13
.prettierrc
|
@ -1,3 +1,12 @@
|
||||||
{
|
{
|
||||||
"singleQuote": true
|
"parser": "typescript",
|
||||||
}
|
"trailingComma": "none",
|
||||||
|
"useTabs": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,3 +1,7 @@
|
||||||
{
|
{
|
||||||
"recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode"]
|
"recommendations": [
|
||||||
|
"nrwl.angular-console",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
19
README.md
19
README.md
|
@ -1,17 +1,20 @@
|
||||||
# Euterpe
|
# Euterpe
|
||||||
|
|
||||||
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
|
The sole mono-repo for all things Euterpe.
|
||||||
|
|
||||||
✨ **This workspace has been generated by [Nx, a Smart, fast and extensible build system.](https://nx.dev)** ✨
|
|
||||||
|
|
||||||
## Understand this workspace
|
## Understand this workspace
|
||||||
|
|
||||||
Run `nx graph` to see a diagram of the dependencies of the projects.
|
Run `npx nx graph` to see a diagram of the dependencies of the projects.
|
||||||
|
All programs go like this: `npx nx {package script} {package}`
|
||||||
|
|
||||||
## Remote caching
|
## Test this workspace
|
||||||
|
|
||||||
Run `npx nx connect-to-nx-cloud` to enable [remote caching](https://nx.app) and make CI faster.
|
Run `npx nx server player-web-test` to see the music player in a minimal demo.
|
||||||
|
|
||||||
## Further help
|
## Build
|
||||||
|
|
||||||
Visit the [Nx Documentation](https://nx.dev) to learn more.
|
Run `npx nx build player` to build the player.
|
||||||
|
|
||||||
|
## Publish
|
||||||
|
|
||||||
|
First build, then `npm publish --access=public`
|
5
babel.config.json
Normal file
5
babel.config.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"babelrcRoots": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
59
nx.json
59
nx.json
|
@ -7,23 +7,70 @@
|
||||||
"default": {
|
"default": {
|
||||||
"runner": "nx/tasks-runners/default",
|
"runner": "nx/tasks-runners/default",
|
||||||
"options": {
|
"options": {
|
||||||
"cacheableOperations": ["build", "lint", "test", "e2e"]
|
"cacheableOperations": [
|
||||||
|
"build",
|
||||||
|
"lint",
|
||||||
|
"test",
|
||||||
|
"e2e"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"targetDefaults": {
|
"targetDefaults": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": [
|
||||||
"inputs": ["production", "^production"]
|
"^build"
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"production",
|
||||||
|
"^production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"{workspaceRoot}/.eslintrc.json",
|
||||||
|
"{workspaceRoot}/.eslintignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"namedInputs": {
|
"namedInputs": {
|
||||||
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
"default": [
|
||||||
"production": ["default"],
|
"{projectRoot}/**/*",
|
||||||
"sharedGlobals": []
|
"sharedGlobals"
|
||||||
|
],
|
||||||
|
"production": [
|
||||||
|
"default",
|
||||||
|
"!{projectRoot}/.eslintrc.json",
|
||||||
|
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
||||||
|
"!{projectRoot}/tsconfig.spec.json"
|
||||||
|
],
|
||||||
|
"sharedGlobals": [
|
||||||
|
"{workspaceRoot}/babel.config.json"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"workspaceLayout": {
|
"workspaceLayout": {
|
||||||
"appsDir": "packages",
|
"appsDir": "packages",
|
||||||
"libsDir": "packages"
|
"libsDir": "packages"
|
||||||
|
},
|
||||||
|
"generators": {
|
||||||
|
"@nx/web:application": {
|
||||||
|
"style": "css",
|
||||||
|
"linter": "eslint",
|
||||||
|
"unitTestRunner": "vitest",
|
||||||
|
"e2eTestRunner": "cypress"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4764
package-lock.json
generated
4764
package-lock.json
generated
File diff suppressed because it is too large
Load diff
31
package.json
31
package.json
|
@ -1,15 +1,36 @@
|
||||||
{
|
{
|
||||||
"name": "@euterpe/source",
|
"name": "@euterpe.js/source",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"private": true,
|
"private": false,
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@nx/cypress": "16.2.1",
|
||||||
|
"@nx/eslint-plugin": "16.2.1",
|
||||||
"@nx/js": "16.2.1",
|
"@nx/js": "16.2.1",
|
||||||
|
"@nx/linter": "16.2.1",
|
||||||
|
"@nx/vite": "^16.2.1",
|
||||||
|
"@nx/web": "^16.2.1",
|
||||||
"@nx/workspace": "16.2.1",
|
"@nx/workspace": "16.2.1",
|
||||||
|
"@types/node": "^20.2.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
||||||
|
"@typescript-eslint/parser": "^5.58.0",
|
||||||
|
"@vitest/coverage-c8": "^0.31.0",
|
||||||
|
"@vitest/ui": "^0.31.0",
|
||||||
|
"cypress": "^12.11.0",
|
||||||
|
"eslint": "~8.15.0",
|
||||||
|
"eslint-config-prettier": "8.1.0",
|
||||||
|
"eslint-plugin-cypress": "^2.10.3",
|
||||||
|
"jsdom": "~20.0.3",
|
||||||
"nx": "16.2.1",
|
"nx": "16.2.1",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"typescript": "~5.0.2"
|
"typescript": "~5.0.2",
|
||||||
|
"vite": "^4.3.4",
|
||||||
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-tsconfig-paths": "^4.0.2",
|
||||||
|
"vitest": "^0.31.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
33
packages/player-web-test/.eslintrc.json
Normal file
33
packages/player-web-test/.eslintrc.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"../../.eslintrc.json"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"!**/*"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx",
|
||||||
|
"*.js",
|
||||||
|
"*.jsx"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.js",
|
||||||
|
"*.jsx"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
35
packages/player-web-test/index.html
Normal file
35
packages/player-web-test/index.html
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>PlayerWebTest</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="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>
|
59
packages/player-web-test/project.json
Normal file
59
packages/player-web-test/project.json
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"name": "player-web-test",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"sourceRoot": "packages/player-web-test/src",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nx/vite:build",
|
||||||
|
"outputs": [
|
||||||
|
"{options.outputPath}"
|
||||||
|
],
|
||||||
|
"defaultConfiguration": "production",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/packages/player-web-test"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development": {
|
||||||
|
"mode": "development"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"mode": "production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"executor": "@nx/vite:dev-server",
|
||||||
|
"defaultConfiguration": "development",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "player-web-test:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "player-web-test:build:development",
|
||||||
|
"hmr": true
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "player-web-test:build:production",
|
||||||
|
"hmr": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"executor": "@nx/vite:preview-server",
|
||||||
|
"defaultConfiguration": "development",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "player-web-test:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "player-web-test:build:development"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "player-web-test:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
packages/player-web-test/public/favicon.ico
Normal file
BIN
packages/player-web-test/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
0
packages/player-web-test/src/assets/.gitkeep
Normal file
0
packages/player-web-test/src/assets/.gitkeep
Normal file
56
packages/player-web-test/src/main.ts
Normal file
56
packages/player-web-test/src/main.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { MusicPlayerBuilder } from "@euterpe/player";
|
||||||
|
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
||||||
|
const music_player_builder = MusicPlayerBuilder(audio_el)
|
||||||
|
music_player_builder.start()
|
||||||
|
const music_player = music_player_builder.build()
|
||||||
|
music_player.change_volume(1)
|
||||||
|
|
||||||
|
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", () => {
|
||||||
|
//const analyser_node = music_player_builder.add_analyser()
|
||||||
|
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))
|
17
packages/player-web-test/src/styles.css
Normal file
17
packages/player-web-test/src/styles.css
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
#volume{
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
width: 100vw;
|
||||||
|
height:100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.wrapper{
|
||||||
|
width:50vw;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
30
packages/player-web-test/tsconfig.json
Normal file
30
packages/player-web-test/tsconfig.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
}
|
42
packages/player-web-test/vite.config.ts
Normal file
42
packages/player-web-test/vite.config.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/// <reference types="vitest" />
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
|
import viteTsConfigPaths from "vite-tsconfig-paths"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
cacheDir: "../../node_modules/.vite/player-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: '../../',
|
||||||
|
// }),
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
cache: {
|
||||||
|
dir: "../../node_modules/.vitest"
|
||||||
|
},
|
||||||
|
environment: "jsdom",
|
||||||
|
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"]
|
||||||
|
}
|
||||||
|
})
|
33
packages/player/.eslintrc.json
Normal file
33
packages/player/.eslintrc.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"../../.eslintrc.json"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"!**/*"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx",
|
||||||
|
"*.js",
|
||||||
|
"*.jsx"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.js",
|
||||||
|
"*.jsx"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
packages/player/package.json
Normal file
5
packages/player/package.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "@euterpe.js/player",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
41
packages/player/project.json
Normal file
41
packages/player/project.json
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "player",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "packages/player/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nx/js:tsc",
|
||||||
|
"outputs": [
|
||||||
|
"{options.outputPath}"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/packages/player",
|
||||||
|
"main": "packages/player/src/index.ts",
|
||||||
|
"tsConfig": "packages/player/tsconfig.lib.json",
|
||||||
|
"assets": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publish": {
|
||||||
|
"command": "node tools/scripts/publish.mjs player {args.ver} {args.tag}",
|
||||||
|
"dependsOn": [
|
||||||
|
"build"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/linter:eslint",
|
||||||
|
"outputs": [
|
||||||
|
"{options.outputFile}"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": [
|
||||||
|
"packages/player/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"'audioContext",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
527
packages/player/src/index.ts
Normal file
527
packages/player/src/index.ts
Normal file
|
@ -0,0 +1,527 @@
|
||||||
|
export enum SubscribeEvents {
|
||||||
|
CurrentTimeTick,
|
||||||
|
FormattedDurationTick,
|
||||||
|
FormattedCurrentTimeTick,
|
||||||
|
}
|
||||||
|
const PubSub = () => {
|
||||||
|
//el = event listener
|
||||||
|
const el_current_time_tick: Array<(data: any) => void> = []
|
||||||
|
const el_formatted_duration_tick: Array<(data: any) => void> = []
|
||||||
|
const el_formatted_current_time_tick: Array<(data: any) => void> = []
|
||||||
|
|
||||||
|
function subscribe(event_name: SubscribeEvents, func: (data: any) => void) {
|
||||||
|
switch (event_name) {
|
||||||
|
case SubscribeEvents.CurrentTimeTick: {
|
||||||
|
el_current_time_tick.push(func)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case SubscribeEvents.FormattedDurationTick: {
|
||||||
|
el_formatted_duration_tick.push(func)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case SubscribeEvents.FormattedCurrentTimeTick: {
|
||||||
|
el_formatted_current_time_tick.push(func)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function unsubscribe(event_name: SubscribeEvents, func: (data: any) => void) {
|
||||||
|
switch (event_name) {
|
||||||
|
case SubscribeEvents.CurrentTimeTick: {
|
||||||
|
if (el_current_time_tick.includes(func)) {
|
||||||
|
el_current_time_tick.splice(el_current_time_tick.indexOf(func), 1)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case SubscribeEvents.FormattedDurationTick: {
|
||||||
|
if (el_formatted_duration_tick.includes(func)) {
|
||||||
|
el_formatted_duration_tick.splice(el_formatted_duration_tick.indexOf(func), 1)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case SubscribeEvents.FormattedCurrentTimeTick: {
|
||||||
|
if (el_formatted_duration_tick.includes(func)) {
|
||||||
|
el_formatted_duration_tick.splice(el_formatted_duration_tick.indexOf(func), 1)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function emit(event_name: SubscribeEvents, data: any) {
|
||||||
|
switch (event_name) {
|
||||||
|
case SubscribeEvents.CurrentTimeTick: {
|
||||||
|
el_current_time_tick.forEach((func) => {
|
||||||
|
func(data)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case SubscribeEvents.FormattedDurationTick: {
|
||||||
|
el_formatted_duration_tick.forEach((func) => {
|
||||||
|
func(data)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case SubscribeEvents.FormattedCurrentTimeTick: {
|
||||||
|
el_formatted_current_time_tick.forEach((func) => {
|
||||||
|
func(data)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
el_current_time_tick,
|
||||||
|
el_formatted_duration_tick,
|
||||||
|
el_formatted_current_time_tick,
|
||||||
|
subscribe,
|
||||||
|
unsubscribe,
|
||||||
|
emit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For old browsers */
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
webkitAudioContext: typeof AudioContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTMLAudioElement, track_i: MediaElementAudioSourceNode, gain_i: GainNode, volume_i: number, current_song_path_i?: string) => {
|
||||||
|
const audio_element: HTMLAudioElement = audio_element_i
|
||||||
|
const audio_context: AudioContext = audio_context_i
|
||||||
|
const track: MediaElementAudioSourceNode = track_i
|
||||||
|
const gain: GainNode = gain_i
|
||||||
|
let current_song_path: string | undefined = current_song_path_i
|
||||||
|
let current_song_duration: number
|
||||||
|
let volume_cache: number = volume_i
|
||||||
|
let volume: number = volume_i
|
||||||
|
let is_playing = false
|
||||||
|
let time = 0
|
||||||
|
const pub_sub = PubSub()
|
||||||
|
|
||||||
|
function mute_toggle() {
|
||||||
|
if (gain.gain.value == 0) {
|
||||||
|
unmute()
|
||||||
|
} else {
|
||||||
|
mute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function mute() {
|
||||||
|
volume_cache = gain.gain.value
|
||||||
|
/* Gentler mute, doesn't pop
|
||||||
|
gain.gain.linearRampToValueAtTime(
|
||||||
|
0,
|
||||||
|
audio_context.currentTime + 0.1
|
||||||
|
);*/
|
||||||
|
volume = gain.gain.value = 0
|
||||||
|
}
|
||||||
|
function unmute() {
|
||||||
|
volume = gain.gain.value = volume_cache
|
||||||
|
}
|
||||||
|
function change_volume(volume_i: number) {
|
||||||
|
volume = gain.gain.value = volume_i
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Safer seek_async. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed
|
||||||
|
*/
|
||||||
|
function try_seek_async(new_time: number) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (track.context.state == "closed" || track.context.state == "suspended") {
|
||||||
|
is_playing = false
|
||||||
|
reject("Can't seek - track not playing")
|
||||||
|
}
|
||||||
|
audio_element.currentTime = new_time
|
||||||
|
resolve(null)
|
||||||
|
/*audio_element.play().then((s) => resolve(s), (r) => {
|
||||||
|
is_playing = false
|
||||||
|
reject(r)
|
||||||
|
})*/
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Can try to seek even if the audio context was suspended or closed. Best to use try_seek_async()
|
||||||
|
*/
|
||||||
|
function seek_async(new_time: number) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
audio_element.currentTime = new_time
|
||||||
|
resolve(null)
|
||||||
|
/* audio_element.play().then((s) => resolve(s), (r) => {
|
||||||
|
is_playing = false
|
||||||
|
reject(r)
|
||||||
|
})*/
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Unsafe, throws error if failed. Use try_seek_async or seek_async unless you don't care about the result.
|
||||||
|
*/
|
||||||
|
function seek(new_time: number) {
|
||||||
|
audio_element.currentTime = new_time
|
||||||
|
audio_element.play().catch((e) => { throw e })
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Safer play_toggle_async. Normal play_toggle will try to start the player even if the track hasn't started yet, or was previously suspended/closed
|
||||||
|
*/
|
||||||
|
function try_play_toggle_async() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||||
|
reject("Context closed or suspended")
|
||||||
|
}
|
||||||
|
if (audio_element.paused) {
|
||||||
|
audio_element.play().then((s) => {
|
||||||
|
is_playing = true
|
||||||
|
resolve(s)
|
||||||
|
}, (r) => {
|
||||||
|
is_playing = false
|
||||||
|
reject(r)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
audio_element.pause()
|
||||||
|
is_playing = false
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Can try to play even if the audio context was suspended or closed. Best to use try_play_toggle_async()
|
||||||
|
*/
|
||||||
|
function play_toggle_async() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||||
|
audio_context.resume()
|
||||||
|
}
|
||||||
|
if (audio_element.paused) {
|
||||||
|
audio_element.play().then((s) => {
|
||||||
|
is_playing = true
|
||||||
|
resolve(s)
|
||||||
|
}, (r) => {
|
||||||
|
is_playing = false
|
||||||
|
reject(r)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
audio_element.pause()
|
||||||
|
is_playing = false
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Unsafe, throws error if failed. Use play_toggle_async or try_play_toggle_async unless you don't care about the result.
|
||||||
|
*/
|
||||||
|
function play_toggle() {
|
||||||
|
if (audio_element.paused) {
|
||||||
|
is_playing = true
|
||||||
|
audio_element.play().catch((r) => {
|
||||||
|
is_playing = false
|
||||||
|
throw r
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
is_playing = false
|
||||||
|
audio_element.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Safer play_async. Normal play will try to start the player even if the track hasn't started yet, or was previously suspended/closed
|
||||||
|
*/
|
||||||
|
function try_play_async() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (is_playing) reject(Error("Already playing"))
|
||||||
|
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||||
|
reject("Context closed or suspended")
|
||||||
|
}
|
||||||
|
audio_element.play().then((s) => {
|
||||||
|
is_playing = true
|
||||||
|
resolve(s)
|
||||||
|
}, (r) => {
|
||||||
|
is_playing = false
|
||||||
|
reject(r)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Will try to play even if the audio context was suspended or closed. Best to use try_play_async()
|
||||||
|
*/
|
||||||
|
function play_async() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (is_playing) resolve(null)
|
||||||
|
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||||
|
audio_context.resume()
|
||||||
|
}
|
||||||
|
audio_element.play().then((s) => {
|
||||||
|
is_playing = true
|
||||||
|
resolve(s)
|
||||||
|
}, (r) => {
|
||||||
|
is_playing = false
|
||||||
|
reject(r)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Unsafe, throws error if failed. Use play_async or try_play_async unless you don't care about the result.
|
||||||
|
*/
|
||||||
|
function play() {
|
||||||
|
if (is_playing) return
|
||||||
|
audio_element.play().catch((r) => {
|
||||||
|
is_playing = false
|
||||||
|
throw r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Safe technically. Even if audioContext is suspended or closed it will pretend that it paused.
|
||||||
|
*/
|
||||||
|
function pause() {
|
||||||
|
audio_element.pause()
|
||||||
|
is_playing = false
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Will only load metadata of the upcoming song. Need to call try_play_async() afterwards to start the playback
|
||||||
|
*/
|
||||||
|
function try_new_song_async(path: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
audio_element.src = current_song_path = path
|
||||||
|
//Found out today about this. Such a nice new way to mass remove event listeners!
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
audio_element.addEventListener("canplaythrough", function canplay_listener(s) {
|
||||||
|
//current_song_duration = audio_element.duration
|
||||||
|
controller.abort()
|
||||||
|
resolve(s)
|
||||||
|
}, { signal: controller.signal })
|
||||||
|
|
||||||
|
audio_element.addEventListener("error", function error_listener(e) {
|
||||||
|
controller.abort()
|
||||||
|
reject(e)
|
||||||
|
}, { signal: controller.signal })
|
||||||
|
|
||||||
|
audio_element.addEventListener("abort", function abort_listener(e) {
|
||||||
|
controller.abort()
|
||||||
|
reject(e)
|
||||||
|
}, { signal: controller.signal })
|
||||||
|
|
||||||
|
audio_element.addEventListener("stalled", function stalled_listener(e) {
|
||||||
|
controller.abort()
|
||||||
|
reject(e)
|
||||||
|
}, { signal: controller.signal })
|
||||||
|
is_playing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Won't tell if you if the song actually got loaded or if it failed. For a safer version use try_new_song_async() unless you don't care about the result
|
||||||
|
*/
|
||||||
|
function new_song(path: string) {
|
||||||
|
audio_element.src = current_song_path = path
|
||||||
|
//current_song_duration = audio_element.duration
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Will parse the duration of the song to make it easy to display in UI
|
||||||
|
* If somethings undefined it returns "0:00"
|
||||||
|
*/
|
||||||
|
function get_formatted_duration() {
|
||||||
|
const dur = audio_element.duration
|
||||||
|
current_song_duration = audio_element.duration
|
||||||
|
|
||||||
|
if (dur == 0 || !dur) return "0:00"
|
||||||
|
|
||||||
|
// ~ is Bitwise NOT, equivalent to Math.floor()
|
||||||
|
const hrs = ~~(dur / 3600);
|
||||||
|
const mins = ~~((dur % 3600) / 60);
|
||||||
|
const secs = ~~dur % 60;
|
||||||
|
|
||||||
|
let ret = ""
|
||||||
|
if (hrs > 0) {
|
||||||
|
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
|
||||||
|
ret += "" + secs;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Will parse the current time of the song to make it easy to display in UI
|
||||||
|
* If somethings undefined it returns "0:00"
|
||||||
|
*/
|
||||||
|
function get_formatted_current_time() {
|
||||||
|
const curr = audio_element.currentTime
|
||||||
|
|
||||||
|
if (curr == 0 || !curr) return "0:00"
|
||||||
|
// ~~ is Bitwise OR, equivalent to Math.floor()
|
||||||
|
const hrs = ~~(curr / 3600);
|
||||||
|
const mins = ~~((curr % 3600) / 60);
|
||||||
|
const secs = ~~curr % 60;
|
||||||
|
|
||||||
|
let ret = ""
|
||||||
|
if (hrs > 0) {
|
||||||
|
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
|
||||||
|
ret += "" + secs;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Will give current time every animation frame
|
||||||
|
*/
|
||||||
|
function subscribe_to_time_tick(callback: (data: any) => void) {
|
||||||
|
pub_sub.subscribe(SubscribeEvents.CurrentTimeTick, callback)
|
||||||
|
emit_current_time()
|
||||||
|
}
|
||||||
|
function emit_current_time() {
|
||||||
|
const request_id = requestAnimationFrame(emit_current_time.bind(MusicPlayer))
|
||||||
|
if (audio_element.ended) is_playing = false
|
||||||
|
if (audio_element.paused) is_playing == false
|
||||||
|
// if use reactively changes volume directly
|
||||||
|
gain.gain.value = volume
|
||||||
|
|
||||||
|
time = audio_element.currentTime
|
||||||
|
if (pub_sub.el_current_time_tick.length == 0) cancelAnimationFrame(request_id)
|
||||||
|
pub_sub.emit(SubscribeEvents.CurrentTimeTick, time)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Will give formatted current time via get_formatted_current_time() every animation frame
|
||||||
|
*/
|
||||||
|
function subscribe_to_formatted_current_time_tick(callback: (data: any) => void) {
|
||||||
|
pub_sub.subscribe(SubscribeEvents.FormattedCurrentTimeTick, callback)
|
||||||
|
emit_formatted_current_time()
|
||||||
|
}
|
||||||
|
function emit_formatted_current_time() {
|
||||||
|
const request_id = requestAnimationFrame(emit_formatted_current_time.bind(MusicPlayer))
|
||||||
|
const time = get_formatted_current_time()
|
||||||
|
//if (pub_sub.el_formatted_current_time_tick.length == 0) cancelAnimationFrame(request_id)
|
||||||
|
pub_sub.emit(SubscribeEvents.FormattedCurrentTimeTick, time)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Will give formatted duration time via get_formatted_duration() every animation frame
|
||||||
|
*/
|
||||||
|
function subscribe_to_formatted_duration_time(callback: (data: any) => void) {
|
||||||
|
pub_sub.subscribe(SubscribeEvents.FormattedDurationTick, callback)
|
||||||
|
emit_formatted_duration_time()
|
||||||
|
}
|
||||||
|
function emit_formatted_duration_time() {
|
||||||
|
const request_id = requestAnimationFrame(emit_formatted_duration_time.bind(MusicPlayer))
|
||||||
|
const time = get_formatted_duration()
|
||||||
|
//if (pub_sub.el_formatted_duration_tick.length == 0) cancelAnimationFrame(request_id)
|
||||||
|
pub_sub.emit(SubscribeEvents.FormattedDurationTick, time)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
track,
|
||||||
|
get_is_playing: () => is_playing,
|
||||||
|
get_current_path: () => current_song_path,
|
||||||
|
get_current_duration: () => current_song_duration,
|
||||||
|
volume,
|
||||||
|
time,
|
||||||
|
mute,
|
||||||
|
unmute,
|
||||||
|
mute_toggle,
|
||||||
|
change_volume,
|
||||||
|
try_seek_async,
|
||||||
|
seek_async,
|
||||||
|
seek,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
play_toggle,
|
||||||
|
play_toggle_async,
|
||||||
|
try_play_toggle_async,
|
||||||
|
try_new_song_async,
|
||||||
|
new_song,
|
||||||
|
get_formatted_duration,
|
||||||
|
get_formatted_current_time,
|
||||||
|
subscribe_to_formatted_current_time_tick,
|
||||||
|
subscribe_to_formatted_duration_time,
|
||||||
|
subscribe_to_time_tick,
|
||||||
|
try_play_async,
|
||||||
|
play_async,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function MusicPlayerBuilder(audio_element: HTMLAudioElement) {
|
||||||
|
let audio_context: AudioContext
|
||||||
|
let gain: GainNode
|
||||||
|
let track: MediaElementAudioSourceNode
|
||||||
|
const volume = 1
|
||||||
|
let prev_node: any;
|
||||||
|
let is_gain_connected = false
|
||||||
|
/**
|
||||||
|
* Creates a context and gain( Gets connected at the end )
|
||||||
|
* will throw if audio_element is undefined (stupid vue setup amirite?)
|
||||||
|
* will throw if user has not interacted with the page yet (Can't initiate AudioContext)
|
||||||
|
*/
|
||||||
|
function start() {
|
||||||
|
if (audio_element === undefined) throw Error("audio_element was undefined")
|
||||||
|
// ↓ For old browsers
|
||||||
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
|
audio_context = new AudioContext()
|
||||||
|
track = audio_context.createMediaElementSource(audio_element)
|
||||||
|
gain = audio_context.createGain()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* For external use, not kept inside player after connection.
|
||||||
|
* @returns {AnalyserNode}
|
||||||
|
*/
|
||||||
|
function add_analyser() {
|
||||||
|
const analyser = audio_context.createAnalyser()
|
||||||
|
!prev_node ? track.connect(analyser) : prev_node.connect(analyser)
|
||||||
|
prev_node = analyser
|
||||||
|
return analyser
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* For external use, not kept inside player after connection.
|
||||||
|
* @returns {StereoPannerNode}
|
||||||
|
*/
|
||||||
|
function add_stereo_panner_node() {
|
||||||
|
const panner = audio_context.createStereoPanner()
|
||||||
|
!prev_node ? track.connect(panner) : prev_node.connect(panner)
|
||||||
|
prev_node = panner
|
||||||
|
return panner
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* For external use, not kept inside player after connection.
|
||||||
|
* @returns {StereoPannerNode}
|
||||||
|
*/
|
||||||
|
function add_wave_shaper_node() {
|
||||||
|
const shaper = audio_context.createWaveShaper()
|
||||||
|
!prev_node ? track.connect(shaper) : prev_node.connect(shaper)
|
||||||
|
prev_node = shaper
|
||||||
|
return shaper
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* For additional trickery, you can connect your own node.
|
||||||
|
*/
|
||||||
|
function connect_custom_node(node: AudioNode) {
|
||||||
|
!prev_node ? track.connect(node) : prev_node.connect(node)
|
||||||
|
prev_node = node
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Only use if you need to connect the gain before another node,
|
||||||
|
* eg. if you want the analyser nodes output to be affected by user gain
|
||||||
|
*/
|
||||||
|
function connect_gain() {
|
||||||
|
!prev_node ? track.connect(gain) : prev_node.connect(gain)
|
||||||
|
prev_node = gain
|
||||||
|
is_gain_connected = true
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Finishes the build
|
||||||
|
* @returns {MusicPlayer: () => void}
|
||||||
|
*/
|
||||||
|
function build() {
|
||||||
|
if (!is_gain_connected) {
|
||||||
|
!prev_node ? track.connect(gain) : prev_node.connect(gain)
|
||||||
|
prev_node = gain
|
||||||
|
}
|
||||||
|
prev_node.connect(audio_context.destination)
|
||||||
|
audio_element.preload = "metadata"
|
||||||
|
return MusicPlayer(audio_context, audio_element, track, gain, volume)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
add_analyser,
|
||||||
|
add_stereo_panner_node,
|
||||||
|
add_wave_shaper_node,
|
||||||
|
connect_gain,
|
||||||
|
connect_custom_node,
|
||||||
|
build
|
||||||
|
}
|
||||||
|
}
|
3
packages/player/src/lib/player.ts
Normal file
3
packages/player/src/lib/player.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function player(): string {
|
||||||
|
return "player"
|
||||||
|
}
|
19
packages/player/tsconfig.json
Normal file
19
packages/player/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
18
packages/player/tsconfig.lib.json
Normal file
18
packages/player/tsconfig.lib.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
65
tools/scripts/publish.mjs
Normal file
65
tools/scripts/publish.mjs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* This is a minimal script to publish your package to "npm".
|
||||||
|
* This is meant to be used as-is or customize as you see fit.
|
||||||
|
*
|
||||||
|
* This script is executed on "dist/path/to/library" as "cwd" by default.
|
||||||
|
*
|
||||||
|
* You might need to authenticate with NPM before running this script.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from "child_process"
|
||||||
|
import { readFileSync, writeFileSync } from "fs"
|
||||||
|
import chalk from "chalk"
|
||||||
|
|
||||||
|
import devkit from "@nx/devkit"
|
||||||
|
const { readCachedProjectGraph } = devkit
|
||||||
|
|
||||||
|
function invariant(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
console.error(chalk.bold.red(message))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag}
|
||||||
|
// Default "tag" to "next" so we won't publish the "latest" tag by accident.
|
||||||
|
const [, , name, version, tag = "next"] = process.argv
|
||||||
|
|
||||||
|
// A simple SemVer validation to validate the version
|
||||||
|
const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/
|
||||||
|
invariant(
|
||||||
|
version && validVersion.test(version),
|
||||||
|
`No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got ${version}.`
|
||||||
|
)
|
||||||
|
|
||||||
|
const graph = readCachedProjectGraph()
|
||||||
|
const project = graph.nodes[name]
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
project,
|
||||||
|
`Could not find project "${name}" in the workspace. Is the project.json configured correctly?`
|
||||||
|
)
|
||||||
|
|
||||||
|
const outputPath = project.data?.targets?.build?.options?.outputPath
|
||||||
|
invariant(
|
||||||
|
outputPath,
|
||||||
|
`Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?`
|
||||||
|
)
|
||||||
|
|
||||||
|
process.chdir(outputPath)
|
||||||
|
|
||||||
|
// Updating the version in "package.json" before publishing
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(readFileSync(`package.json`).toString())
|
||||||
|
json.version = version
|
||||||
|
writeFileSync(`package.json`, JSON.stringify(json, null, 2))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
chalk.bold.red(
|
||||||
|
`Error reading package.json file from library build output.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute "npm publish" to publish
|
||||||
|
execSync(`npm publish --access public --tag ${tag}`)
|
|
@ -10,11 +10,21 @@
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "es2015",
|
"target": "es2015",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"lib": ["es2020", "dom"],
|
"lib": [
|
||||||
|
"es2020",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {}
|
"paths": {
|
||||||
|
"@euterpe/player": [
|
||||||
|
"packages/player/src/index.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "tmp"]
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"tmp"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue