Add euterpe, many fixes.
This commit is contained in:
parent
d269605f55
commit
081cb68b3d
34 changed files with 1347 additions and 276 deletions
|
@ -5,7 +5,8 @@
|
|||
"scripts": {
|
||||
"publish-player": "nx build player && cd dist/packages/player && npm publish --access=public",
|
||||
"publish-visualizer": "nx build visualizer && cd dist/packages/visualizer && npm publish --access=public",
|
||||
"publish-library": "nx build music-library && cd dist/packages/music-library && npm publish --access=public"
|
||||
"publish-library": "nx build music-library && cd dist/packages/music-library && npm publish --access=public",
|
||||
"publish-euterpe": "nx build euterpe && cd dist/packages/euterpe && npm publish --access=public"
|
||||
},
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
|
@ -39,4 +40,4 @@
|
|||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
5
packages/euterpe-web-test/.babelrc
Normal file
5
packages/euterpe-web-test/.babelrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"presets": [
|
||||
"@nx/js/babel"
|
||||
]
|
||||
}
|
33
packages/euterpe-web-test/.eslintrc.json
Normal file
33
packages/euterpe-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": {}
|
||||
}
|
||||
]
|
||||
}
|
46
packages/euterpe-web-test/index.html
Normal file
46
packages/euterpe-web-test/index.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>EuterpeTest</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="library-wrapper">
|
||||
</div>
|
||||
<div class="name-wrapper">
|
||||
<p>playing:
|
||||
<p id="text-playing"> ID - ID</p>
|
||||
</p>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<audio src="" id="audio"></audio>
|
||||
<button id="previous">Previous Song</button>
|
||||
<button id="play">Play</button>
|
||||
<button id="pause">Pause</button>
|
||||
<button id="toggle-play">Toggle Pause/Play</button>
|
||||
<button id="next">Next Song</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>
|
||||
<div class="dev-wrapper">
|
||||
queue:<p id="queue-info"></p>
|
||||
history:<p id="history-info"></p>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
70
packages/euterpe-web-test/project.json
Normal file
70
packages/euterpe-web-test/project.json
Normal file
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"name": "euterpe-web-test",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "packages/euterpe-web-test/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/vite:build",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"outputPath": "dist/packages/euterpe-web-test"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"mode": "development"
|
||||
},
|
||||
"production": {
|
||||
"mode": "production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/vite:dev-server",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "euterpe-web-test:build"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "euterpe-web-test:build:development",
|
||||
"hmr": true
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "euterpe-web-test:build:production",
|
||||
"hmr": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"executor": "@nx/vite:preview-server",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "euterpe-web-test:build"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "euterpe-web-test:build:development"
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "euterpe-web-test:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/linter:eslint",
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
],
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"packages/euterpe-web-test/**/*.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
packages/euterpe-web-test/public/16.サニーボーイ・ラプソディ.ogg
Normal file
BIN
packages/euterpe-web-test/public/16.サニーボーイ・ラプソディ.ogg
Normal file
Binary file not shown.
BIN
packages/euterpe-web-test/public/Jamie xx - Sleep Sound.mp3
Normal file
BIN
packages/euterpe-web-test/public/Jamie xx - Sleep Sound.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
packages/euterpe-web-test/public/favicon.ico
Normal file
BIN
packages/euterpe-web-test/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
packages/euterpe-web-test/public/janz - wish.mp3
Normal file
BIN
packages/euterpe-web-test/public/janz - wish.mp3
Normal file
Binary file not shown.
0
packages/euterpe-web-test/src/assets/.gitkeep
Normal file
0
packages/euterpe-web-test/src/assets/.gitkeep
Normal file
63
packages/euterpe-web-test/src/db.ts
Normal file
63
packages/euterpe-web-test/src/db.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { EuterpeBuilder, Library, Player } from "@euterpe.js/euterpe";
|
||||
//export const euterpe = new
|
||||
export const db = new Library.DB
|
||||
|
||||
db.add([
|
||||
//The IDs are added incrementally & are 0 based., so first artists ID added is 0, next 1 etc...
|
||||
//You can specify the ID manually if you want
|
||||
new Library.Artist({
|
||||
name: "Jamie xx",
|
||||
}),
|
||||
new Library.Artist({
|
||||
name: "janz",
|
||||
}),
|
||||
new Library.Artist({
|
||||
name: "Machinedrum",
|
||||
}),
|
||||
new Library.Artist({
|
||||
name: "Tanerélle",
|
||||
}),
|
||||
new Library.Artist({
|
||||
name: "Mono/Poly",
|
||||
}),
|
||||
new Library.Artist({
|
||||
name: "IMANU",
|
||||
links: [
|
||||
[Library.Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]
|
||||
]
|
||||
}),
|
||||
new Library.Artist({
|
||||
name: "toe",
|
||||
id: 10
|
||||
}),
|
||||
])
|
||||
db.add([
|
||||
new Library.Song({
|
||||
//Refrences are constructed as such. This allows to get to the artist from either collection or song
|
||||
artists: [new Library.Ref(Library.RefTo.Artists, 2), new Library.Ref(Library.RefTo.Artists, 3), new Library.Ref(Library.RefTo.Artists, 4)],
|
||||
duration: 252,
|
||||
name: "Star",
|
||||
remix_artists: [new Library.Ref(Library.RefTo.Artists, 5)],
|
||||
url: new URL("http://127.0.0.1:4200/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3")
|
||||
}),
|
||||
new Library.Song({
|
||||
//If you don't like guessing the IDs, then this is also a way to do it
|
||||
artists: [new Library.Ref(Library.RefTo.Artists, db.artists.find((a) => a.name == "Jamie xx")!.id!)],
|
||||
duration: 331,
|
||||
name: "Sleep Sound",
|
||||
url: new URL("http://127.0.0.1:4200/Jamie xx - Sleep Sound.mp3")
|
||||
}),
|
||||
new Library.Song({
|
||||
artists: [new Library.Ref(Library.RefTo.Artists, 1)],
|
||||
duration: 75,
|
||||
name: "wish",
|
||||
url: new URL("http://127.0.0.1:4200/janz - wish.mp3")
|
||||
}),
|
||||
new Library.Song({
|
||||
artists: [new Library.Ref(Library.RefTo.Artists, 10)],
|
||||
duration: 4 * 60 + 5,
|
||||
name: "サニーボーイ・ラプソディ",
|
||||
url: new URL("http://127.0.0.1:4200/16.サニーボーイ・ラプソディ.ogg")
|
||||
})
|
||||
])
|
||||
|
142
packages/euterpe-web-test/src/main.ts
Normal file
142
packages/euterpe-web-test/src/main.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import { db } from "./db";
|
||||
import { EuterpeBuilder, Euterpe } from "@euterpe.js/euterpe";
|
||||
|
||||
let is_seeking = false
|
||||
// document.addEventListener("click", start, { once: true })
|
||||
let euterpe: Euterpe
|
||||
maybe_start()
|
||||
function maybe_start() {
|
||||
if (euterpe) return
|
||||
|
||||
euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db)
|
||||
.build()
|
||||
add_library_to_dom()
|
||||
euterpe.preload_song_async(0).then(() => {
|
||||
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
|
||||
}, (e) => console.log(e + " Failed to preload"))
|
||||
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
euterpe.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..
|
||||
euterpe.on_duration_formatted((time) => {
|
||||
document.querySelector("#duration")!.innerHTML = time
|
||||
document.querySelector("#seek")!.max = "" + euterpe.current_song_duration
|
||||
})
|
||||
|
||||
euterpe.on_time_tick_formatted((time) => {
|
||||
document.querySelector("#current")!.innerHTML = time
|
||||
})
|
||||
euterpe.on_time_tick((time) => {
|
||||
if (is_seeking) return
|
||||
document.querySelector("#seek")!.value = "" + time
|
||||
dev_queue_update()
|
||||
dev_history_update()
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
document.querySelector("#previous")?.addEventListener("click", () => {
|
||||
maybe_start()
|
||||
euterpe.previous_song_async().then(() => {
|
||||
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
|
||||
}, (e) => alert(e + "Failed to change song"))
|
||||
})
|
||||
document.querySelector("#next")?.addEventListener("click", () => {
|
||||
maybe_start()
|
||||
euterpe.next_song_async().then(() => {
|
||||
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
|
||||
}, (e) => alert(e + "Failed to change song"))
|
||||
})
|
||||
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
maybe_start()
|
||||
euterpe.play_async().catch((e) => alert("Failed to play, " + e))
|
||||
})
|
||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||
maybe_start()
|
||||
euterpe.pause()
|
||||
})
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
maybe_start()
|
||||
euterpe.mute()
|
||||
})
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
maybe_start()
|
||||
euterpe.unmute()
|
||||
})
|
||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||
maybe_start()
|
||||
euterpe.mute_toggle()
|
||||
})
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
maybe_start()
|
||||
euterpe.play_toggle_async().catch((e) => alert("failed to toggle pause/play!" + e))
|
||||
})
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
maybe_start()
|
||||
euterpe.change_volume(e.target?.valueAsNumber)
|
||||
})
|
||||
//disables time updates so the time slider doesn't slip away from user
|
||||
document.querySelector("#seek")?.addEventListener("mousedown", () => {
|
||||
is_seeking = true;
|
||||
})
|
||||
|
||||
function add_library_to_dom() {
|
||||
const lib_dom = document.querySelector(".library-wrapper") as HTMLDivElement
|
||||
for (const song of euterpe.db.songs) {
|
||||
const div = document.createElement("div")
|
||||
const p = document.createElement("p")
|
||||
const button_play = document.createElement("button")
|
||||
const button_queue = document.createElement("button")
|
||||
const span = document.createElement("span")
|
||||
p.innerHTML = `${euterpe.format_current_song(song.id)}`
|
||||
|
||||
button_play.innerHTML = "play"
|
||||
button_play.dataset["id"] = `${song.id}`
|
||||
button_play.onclick = library_play
|
||||
|
||||
button_queue.innerHTML = "queue"
|
||||
button_queue.dataset["id"] = `${song.id}`
|
||||
button_queue.onclick = library_queue
|
||||
|
||||
div.appendChild(p)
|
||||
span.appendChild(button_play)
|
||||
span.appendChild(button_queue)
|
||||
div.appendChild(span)
|
||||
|
||||
lib_dom.appendChild(div)
|
||||
}
|
||||
}
|
||||
function library_play(e: MouseEvent) {
|
||||
const b = e.currentTarget as HTMLButtonElement
|
||||
euterpe.try_specific_song_async(parseInt(b.dataset["id"]!)).then(
|
||||
() => document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song(),
|
||||
(e) => alert(e)
|
||||
)
|
||||
}
|
||||
function library_queue(e: MouseEvent) {
|
||||
const b = e.currentTarget as HTMLButtonElement
|
||||
euterpe.queue_append(parseInt(b.dataset["id"]!))
|
||||
}
|
||||
function dev_queue_update() {
|
||||
const p = document.querySelector("#queue-info") as HTMLParagraphElement
|
||||
const dev_arr = []
|
||||
for (const song of euterpe.queue) {
|
||||
dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`)
|
||||
}
|
||||
p.innerHTML = dev_arr.toString()
|
||||
}
|
||||
function dev_history_update() {
|
||||
const p = document.querySelector("#history-info") as HTMLParagraphElement
|
||||
const dev_arr = []
|
||||
for (const song of euterpe.played_history) {
|
||||
dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`)
|
||||
}
|
||||
p.innerHTML = dev_arr.toString()
|
||||
}
|
38
packages/euterpe-web-test/src/styles.css
Normal file
38
packages/euterpe-web-test/src/styles.css
Normal file
|
@ -0,0 +1,38 @@
|
|||
#volume{
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
body {
|
||||
width: 100vw;
|
||||
height:100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.wrapper{
|
||||
width:80vw;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.name-wrapper{
|
||||
width:80vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.library-wrapper > div {
|
||||
display: flex;
|
||||
margin: 0 5rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.library-wrapper {
|
||||
width:60vw;
|
||||
}
|
||||
.library-wrapper button {
|
||||
height: 100%;
|
||||
}
|
||||
.dev-wrapper{
|
||||
width: 80vw;
|
||||
margin: 6rem 0;
|
||||
}
|
9
packages/euterpe-web-test/tsconfig.app.json
Normal file
9
packages/euterpe-web-test/tsconfig.app.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
27
packages/euterpe-web-test/tsconfig.json
Normal file
27
packages/euterpe-web-test/tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"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"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
33
packages/euterpe-web-test/vite.config.ts
Normal file
33
packages/euterpe-web-test/vite.config.ts
Normal 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/euterpe-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: '../../',
|
||||
// }),
|
||||
// ],
|
||||
// },
|
||||
})
|
33
packages/euterpe/.eslintrc.json
Normal file
33
packages/euterpe/.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": {}
|
||||
}
|
||||
]
|
||||
}
|
22
packages/euterpe/README.md
Normal file
22
packages/euterpe/README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Euterpe
|
||||
|
||||
Fully featured AudioContext music player for the web.
|
||||
|
||||
Features:
|
||||
- "Local" library/Database for songs, collections etc.
|
||||
- Queue
|
||||
- History
|
||||
- Easy way to create Vector based audio visuals
|
||||
- Safe. Provides wrappers for all functions that are either unsafe or don't give a success return. (very Rust inspired, yes.)
|
||||
- Async / Await or simple funcions.
|
||||
|
||||
## How to use:
|
||||
|
||||
#### Simple demo [here](https://github.com/euterpe-js/euterpe-source/tree/master/packages/euterpe-web-test)
|
||||
|
||||
Since this package is just a compilation of our smaller modules, you can read individual modules' tutorials on their respective npm page:
|
||||
- [Euterpe Player](https://www.npmjs.com/package/@euterpe.js/player)
|
||||
- [Euterpe Visualizer](https://www.npmjs.com/package/@euterpe.js/visualizer)
|
||||
- [Euterpe Music Library](https://www.npmjs.com/package/@euterpe.js/music-library)
|
||||
|
||||
This module builds on those, and further adds functions for playing backwards, forwards and managing the queue.
|
33
packages/euterpe/package.json
Normal file
33
packages/euterpe/package.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "@euterpe.js/euterpe",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!",
|
||||
"main": "./src/index.js",
|
||||
"author": {
|
||||
"name": "Djkáťo",
|
||||
"email": "djkatovfx@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/euterpe-js/euterpe-source.git"
|
||||
},
|
||||
"homepage": "https://github.com/euterpe-js/euterpe-source/tree/master/packages/euterpe#readme",
|
||||
"keywords": [
|
||||
"audio",
|
||||
"library",
|
||||
"music-database",
|
||||
"audio-player",
|
||||
"webaudio",
|
||||
"database",
|
||||
"db"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.d.ts",
|
||||
"import": "./src/index.js",
|
||||
"require": "./src/lib/euterpe.js"
|
||||
}
|
||||
}
|
||||
}
|
40
packages/euterpe/project.json
Normal file
40
packages/euterpe/project.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "euterpe",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/euterpe/src",
|
||||
"projectType": "library",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/euterpe",
|
||||
"main": "packages/euterpe/src/index.ts",
|
||||
"tsConfig": "packages/euterpe/tsconfig.lib.json",
|
||||
"assets": [
|
||||
"packages/euterpe/*.md"
|
||||
]
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
"command": "node tools/scripts/publish.mjs euterpe {args.ver} {args.tag}",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/linter:eslint",
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
],
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"packages/euterpe/**/*.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
484
packages/euterpe/src/index.ts
Normal file
484
packages/euterpe/src/index.ts
Normal file
|
@ -0,0 +1,484 @@
|
|||
import * as Player from "@euterpe.js/player"
|
||||
import * as Library from "@euterpe.js/music-library"
|
||||
import * as Visualizer from "@euterpe.js/visualizer"
|
||||
|
||||
export { Player, Library, Visualizer, Euterpe, EuterpeBuilder }
|
||||
/**
|
||||
* Avoid Writing directly to any fields in this class!
|
||||
*/
|
||||
class Euterpe extends Player.MusicPlayer {
|
||||
current_song: Library.Song | undefined
|
||||
current_song_id = 0
|
||||
queue: Library.Song[] = []
|
||||
played_history: Library.Song[] = []
|
||||
constructor(
|
||||
public db: Library.DB,
|
||||
audio_context: AudioContext,
|
||||
audio_element: HTMLAudioElement,
|
||||
track: MediaElementAudioSourceNode,
|
||||
gain: GainNode,
|
||||
volume: number,
|
||||
current_song_path?: string) {
|
||||
|
||||
super(audio_context, audio_element, track, gain, volume, current_song_path,)
|
||||
}
|
||||
/**
|
||||
* Use to load song on page load.
|
||||
*/
|
||||
preload_song_async(id: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const next = this.db.songs.find((song) => song!.id == id)
|
||||
if (!next) reject(new Error(`Song with id ${id} doesn't exist`))
|
||||
else {
|
||||
this.try_new_song_async(next.url.pathname).then((s) => {
|
||||
this.current_song = next
|
||||
resolve(s)
|
||||
}, (e) => reject(e))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Won't loop back to first song if already on the last.
|
||||
* If queue present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
try_next_song_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let new_song: Library.Song
|
||||
if (this.queue.length > 0) {
|
||||
new_song = this.queue.shift()!
|
||||
} else {
|
||||
let id_i = this.db.songs.length;
|
||||
this.db.songs.sort((a, b) => a.id! - b.id!)
|
||||
while (this.db.songs[--id_i].id! > this.current_song_id);
|
||||
const next_id = ++id_i;
|
||||
|
||||
if (next_id == this.db.songs.length) reject(new Error("Won't go past the last song"))
|
||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||
}
|
||||
this.try_new_song_async(new_song.url.href).then(
|
||||
() => {
|
||||
this.try_play_async().then((s) => {
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
resolve(s)
|
||||
}, (e) => reject(e))
|
||||
},
|
||||
(e) => reject(e)
|
||||
)
|
||||
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Will loop back to first song if already on last song,
|
||||
* If queue present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
next_song_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let new_song: Library.Song
|
||||
if (this.queue.length > 0) {
|
||||
new_song = this.queue.shift()!
|
||||
} else {
|
||||
let id_i = this.db.songs.length;
|
||||
this.db.songs.sort((a, b) => a.id! - b.id!)
|
||||
while (this.db.songs[--id_i].id! > this.current_song_id);
|
||||
let next_id = ++id_i
|
||||
|
||||
if (next_id == this.db.songs.length) next_id = this.db.songs[0].id!
|
||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||
}
|
||||
this.try_new_song_async(new_song.url.href).then(
|
||||
() => {
|
||||
this.try_play_async().then((s) => {
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
resolve(s)
|
||||
}, (e) => reject(e))
|
||||
},
|
||||
(e) => reject(e)
|
||||
)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Won't tell you if the playback was successsful & wil loop back if already on last song. Best use try_next_song_async()
|
||||
* If queue present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
next_song() {
|
||||
let new_song: Library.Song
|
||||
if (this.queue.length > 0) {
|
||||
new_song = this.queue.shift()!
|
||||
} else {
|
||||
let id_i = this.db.songs.length;
|
||||
this.db.songs.sort((a, b) => a.id! - b.id!)
|
||||
while (this.db.songs[--id_i].id! > this.current_song_id);
|
||||
let next_id = ++id_i;
|
||||
|
||||
if (next_id == this.db.songs.length) next_id = this.db.songs[0].id!
|
||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||
}
|
||||
this.new_song(new_song.url.href)
|
||||
this.play()
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
}
|
||||
/**
|
||||
* Won't tell you if the playback was successsful, won't loop back if already on last song and will throw error if attempted. Best use next_song_async()
|
||||
* If queue present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
try_next_song() {
|
||||
let new_song: Library.Song
|
||||
if (this.queue.length > 0) {
|
||||
new_song = this.queue.shift()!
|
||||
} else {
|
||||
let id_i = this.db.songs.length;
|
||||
this.db.songs.sort((a, b) => a.id! - b.id!)
|
||||
while (this.db.songs[--id_i].id! > this.current_song_id);
|
||||
const next_id = ++id_i;
|
||||
if (next_id == this.db.songs.length) throw new Error("Won't go past the last song")
|
||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||
}
|
||||
this.new_song(new_song.url.href)
|
||||
this.play()
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
}
|
||||
/**
|
||||
* Uses safer try_play_async. Normal play / play_async will try to start the player even if the track hasn't started yet, or was previously suspended/closed
|
||||
*/
|
||||
try_specific_song_async(new_song_id: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
|
||||
if (!new_song) reject(new Error(`No song with id "${new_song_id}" found`))
|
||||
else {
|
||||
this.try_new_song_async(new_song.url.href).then(
|
||||
() => {
|
||||
this.try_play_async().then((s) => {
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
resolve(s)
|
||||
}, (e) => reject(e))
|
||||
},
|
||||
(e) => reject(e)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* uses play_async. Will try to play even if the audio context was suspended or closed.
|
||||
*/
|
||||
specific_song_async(new_song_id: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
|
||||
if (!new_song) reject(new Error(`No song with id "${new_song_id}" found`))
|
||||
else {
|
||||
this.try_new_song_async(new_song.url.href).then(
|
||||
() => {
|
||||
this.play_async().then((s) => {
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
resolve(s)
|
||||
}, (e) => reject(e))
|
||||
},
|
||||
(e) => reject(e)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Will throw an error if new ID not found. Won't tell you if the play was successful, best use specific_song_async() or try_specific_song_async()
|
||||
*/
|
||||
specific_song(new_song_id: number) {
|
||||
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
|
||||
if (!new_song) throw new Error(`No song with id "${new_song_id}" found`)
|
||||
else {
|
||||
this.new_song(new_song.url.href)
|
||||
this.play()
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Won't loop back to first song if already on the last.
|
||||
* If played_history is present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
try_previous_song_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let new_song: Library.Song
|
||||
if (this.played_history.length > 0) {
|
||||
new_song = this.played_history.pop()!
|
||||
} else {
|
||||
let id_i = 0;
|
||||
this.db.songs.sort((a, b) => a.id! - b.id!)
|
||||
while (this.db.songs[++id_i].id! < this.current_song_id);
|
||||
const next_id = --id_i;
|
||||
|
||||
if (next_id == this.db.songs.length) reject(new Error("Won't roll backwards to last song"))
|
||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||
}
|
||||
this.try_new_song_async(new_song.url.href).then(
|
||||
() => {
|
||||
this.try_play_async().then((s) => {
|
||||
//if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
resolve(s)
|
||||
}, (e) => reject(e))
|
||||
},
|
||||
(e) => reject(e)
|
||||
)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Will loop back to first song if already on the last.
|
||||
* If history present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
previous_song_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let new_song: Library.Song
|
||||
if (this.played_history.length > 0) {
|
||||
new_song = this.played_history.pop()!
|
||||
} else {
|
||||
let id_i = -1;
|
||||
this.db.songs.sort((a, b) => a.id! - b.id!)
|
||||
while (this.db.songs[++id_i].id! < this.current_song_id);
|
||||
let next_id = --id_i;
|
||||
|
||||
if (next_id == -1) next_id = this.db.songs[this.db.songs.length - 1].id!
|
||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||
}
|
||||
this.try_new_song_async(new_song.url.href).then(
|
||||
() => {
|
||||
this.try_play_async().then((s) => {
|
||||
//if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
resolve(s)
|
||||
}, (e) => reject(e))
|
||||
},
|
||||
(e) => reject(e)
|
||||
)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* won't tell you if the play was successful, won't loop back to last song if already on the first and will throw error if attempted.
|
||||
* If history present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
try_previous_song() {
|
||||
let new_song: Library.Song
|
||||
if (this.played_history.length > 0) {
|
||||
new_song = this.played_history.pop()!
|
||||
} else {
|
||||
let id_i = 0;
|
||||
this.db.songs.sort((a, b) => a.id! - b.id!)
|
||||
while (this.db.songs[++id_i].id! < this.current_song_id);
|
||||
const next_id = -id_i;
|
||||
|
||||
if (next_id == this.db.songs.length) throw new Error("Won't go past the last song")
|
||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||
}
|
||||
this.new_song(new_song.url.href)
|
||||
this.play()
|
||||
//if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song_id = new_song.id!
|
||||
this.current_song = new_song
|
||||
}
|
||||
/**
|
||||
* won't tell you if the play was successful & will loop back to last song if already on the first.
|
||||
* If queue present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
previous_song() {
|
||||
let new_song: Library.Song
|
||||
if (this.played_history.length > 0) {
|
||||
new_song = this.played_history.pop()!
|
||||
} else {
|
||||
let id_i = 0;
|
||||
this.db.songs.sort((a, b) => a.id! - b.id!)
|
||||
while (this.db.songs[++id_i].id! < this.current_song_id);
|
||||
let next_id = -id_i;
|
||||
|
||||
if (next_id == this.db.songs.length) next_id = this.db.songs[this.db.songs.length].id!
|
||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||
}
|
||||
this.new_song(new_song.url.href)
|
||||
this.play()
|
||||
//if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song_id = new_song.id!
|
||||
this.current_song = new_song
|
||||
}
|
||||
/**
|
||||
* Takes the song data from current song if no song ID is specified. Will return "ID - ID" if ID and current song doesn't exist
|
||||
* @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix)
|
||||
*/
|
||||
format_current_song(id = this.current_song?.id) {
|
||||
|
||||
const curr_song = this.db.songs.find((song) => song.id == id)
|
||||
if (!curr_song) {
|
||||
return "ID - ID"
|
||||
}
|
||||
let final_text = ""
|
||||
|
||||
for (const artist of curr_song.artists) {
|
||||
const curr_artist = artist.get(this.db) as Library.Artist
|
||||
final_text += curr_artist.name + ", "
|
||||
}
|
||||
|
||||
final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", "
|
||||
final_text += " - " + curr_song.name
|
||||
|
||||
if (curr_song.remix_artists.length > 0) {
|
||||
final_text += " ("
|
||||
|
||||
for (const artist of curr_song.remix_artists) {
|
||||
const curr_artist = artist.get(this.db) as Library.Artist
|
||||
if (curr_artist.links && curr_artist.links.length > 0) {
|
||||
final_text += curr_artist.name
|
||||
} else {
|
||||
final_text += curr_artist.name + ", "
|
||||
}
|
||||
}
|
||||
|
||||
final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", "
|
||||
final_text += " Remix)"
|
||||
}
|
||||
|
||||
return final_text
|
||||
}
|
||||
|
||||
/**
|
||||
* Will add to queue, if ID is undefined nothing will happen. If ID already is in queue, nothing will happen. For more control use `try_queue_add()`
|
||||
*/
|
||||
queue_add(id: number) {
|
||||
const curr_song = this.db.songs.find((song) => song.id == id)
|
||||
if (!curr_song) return
|
||||
if (this.queue.find((song) => song.id == id)) return
|
||||
this.queue.push(curr_song)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will add to queue. If ID is undefined throws error. if ID is already in queue, throws error.
|
||||
*/
|
||||
try_queue_add(id: number) {
|
||||
const curr_song = this.db.songs.find((song) => song.id == id)
|
||||
if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`)
|
||||
if (this.queue.find((song) => song.id == id)) throw new Error(`Song of id "${id}" already queued`)
|
||||
this.queue.push(curr_song)
|
||||
}
|
||||
/**
|
||||
* Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. Throws error if ID doesn't exist.
|
||||
*/
|
||||
try_queue_append(id: number) {
|
||||
const curr_song = this.db.songs.find((song) => song.id == id)
|
||||
if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`)
|
||||
const i = this.queue.findIndex((song) => song.id == id)
|
||||
if (i != -1) this.queue.push(this.queue.splice(i, 1)[0])
|
||||
else this.queue.push(curr_song)
|
||||
}
|
||||
/**
|
||||
* Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. If ID Doesn't exist, does nothing. For more control use try_queue_append()
|
||||
*/
|
||||
queue_append(id: number) {
|
||||
const curr_song = this.db.songs.find((song) => song.id == id)
|
||||
if (!curr_song) return
|
||||
const i = this.queue.findIndex((song) => song.id == id)
|
||||
if (i != -1) this.queue.push(this.queue.splice(i, 1)[0])
|
||||
else this.queue.push(curr_song)
|
||||
}
|
||||
/**
|
||||
* Removes song of ID from queue and returns it. Does and returns nothing if song already not found.
|
||||
*/
|
||||
queue_remove(id: number) {
|
||||
const i = this.queue.findIndex((song) => song.id == id)
|
||||
if (i == -1) return
|
||||
return this.queue.splice(i, 1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EuterpeBuilder {
|
||||
#audio_context: AudioContext
|
||||
#gain: GainNode
|
||||
#track: MediaElementAudioSourceNode
|
||||
#volume = 1
|
||||
#prev_node: any;
|
||||
#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)
|
||||
*/
|
||||
constructor(private audio_element: HTMLAudioElement, private db: Library.DB) {
|
||||
if (audio_element === undefined) throw Error("audio_element was undefined")
|
||||
// ↓ For old browsers
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
this.#audio_context = new AudioContext()
|
||||
this.#track = this.#audio_context.createMediaElementSource(audio_element)
|
||||
this.#gain = this.#audio_context.createGain()
|
||||
}
|
||||
/**
|
||||
* For external use, not kept inside player after connection.
|
||||
* @returns {AnalyserNode}
|
||||
*/
|
||||
add_analyser() {
|
||||
const analyser = this.#audio_context.createAnalyser()
|
||||
!this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser)
|
||||
this.#prev_node = analyser
|
||||
return analyser
|
||||
}
|
||||
/**
|
||||
* For external use, not kept inside player after connection.
|
||||
* @returns {StereoPannerNode}
|
||||
*/
|
||||
add_stereo_panner_node() {
|
||||
const panner = this.#audio_context.createStereoPanner()
|
||||
!this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner)
|
||||
this.#prev_node = panner
|
||||
return panner
|
||||
}
|
||||
/**
|
||||
* For external use, not kept inside player after connection.
|
||||
* @returns {StereoPannerNode}
|
||||
*/
|
||||
add_wave_shaper_node() {
|
||||
const shaper = this.#audio_context.createWaveShaper()
|
||||
!this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper)
|
||||
this.#prev_node = shaper
|
||||
return shaper
|
||||
}
|
||||
/**
|
||||
* For additional trickery, you can connect your own node.
|
||||
*/
|
||||
connect_custom_node(node: AudioNode) {
|
||||
!this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node)
|
||||
this.#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
|
||||
*/
|
||||
connect_gain() {
|
||||
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain)
|
||||
this.#prev_node = this.#gain
|
||||
this.#is_gain_connected = true
|
||||
}
|
||||
/**
|
||||
* Finishes the build
|
||||
* @returns {Euterpe}
|
||||
*/
|
||||
build() {
|
||||
if (!this.#is_gain_connected) {
|
||||
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain)
|
||||
this.#prev_node = this.#gain
|
||||
}
|
||||
this.#prev_node.connect(this.#audio_context.destination)
|
||||
this.audio_element.preload = "metadata"
|
||||
return new Euterpe(this.db, this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume)
|
||||
}
|
||||
}
|
3
packages/euterpe/src/lib/euterpe.ts
Normal file
3
packages/euterpe/src/lib/euterpe.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function euterpe(): string {
|
||||
return "euterpe"
|
||||
}
|
19
packages/euterpe/tsconfig.json
Normal file
19
packages/euterpe/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"
|
||||
}
|
||||
]
|
||||
}
|
10
packages/euterpe/tsconfig.lib.json
Normal file
10
packages/euterpe/tsconfig.lib.json
Normal 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"]
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { DB, Artist, Song, RefTo, Ref, Platforms } from "@euterpe/music-library";
|
||||
import { DB, Artist, Song, RefTo, Ref, Platforms } from "@euterpe.js/music-library";
|
||||
export const db = new DB
|
||||
|
||||
db.add([
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { MusicPlayerBuilder } from "@euterpe/player";
|
||||
import { MusicPlayerBuilder } from "@euterpe.js/player";
|
||||
import { db } from "./db";
|
||||
import { Artist } from "@euterpe/music-library";
|
||||
import { DB, Platforms } from "@euterpe/music-library";
|
||||
import { Artist } from "@euterpe.js/music-library";
|
||||
import { DB, Platforms } from "@euterpe.js/music-library";
|
||||
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
||||
const music_player_builder = MusicPlayerBuilder(audio_el)
|
||||
music_player_builder.start()
|
||||
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
||||
const music_player = music_player_builder.build()
|
||||
music_player.change_volume(1)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "ESNext",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
|
@ -16,4 +16,4 @@
|
|||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { MusicPlayerBuilder } from "@euterpe/player";
|
||||
import { MusicPlayerBuilder } from "@euterpe.js/player";
|
||||
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
||||
const music_player_builder = MusicPlayerBuilder(audio_el)
|
||||
music_player_builder.start()
|
||||
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
||||
const music_player = music_player_builder.build()
|
||||
music_player.change_volume(1)
|
||||
|
||||
|
|
|
@ -55,19 +55,19 @@ document.querySelector("#volume")?.addEventListener("input", (e) => {
|
|||
Euterpe Player also provides functions to easily track the status of playback. It does this via Subscription/Publisher pattern which publishes every frame ( Using `requestAnimationFrame()`). This allows for always up todate values reflecting on the UI.
|
||||
```js
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.subscribe_to_formatted_duration_time((time) => {
|
||||
music_player.on_duration_formatted((time) => {
|
||||
//time == "4:53, "15:59", "1756:15:59"...
|
||||
document.querySelector("#duration-text").innerHTML = time
|
||||
//duration but in "0","1.2", "1223.21668181"... format
|
||||
document.querySelector("#input-seek-range").max = "" + music_player.get_current_duration()
|
||||
})
|
||||
//Keep the current time uptodate but formatted.
|
||||
music_player.subscribe_to_formatted_current_time_tick((time) => {
|
||||
music_player.on_time_tick_formatted((time) => {
|
||||
//time == "2:52", "10:59:59"...
|
||||
document.querySelector("#current-text").innerHTML = time
|
||||
})
|
||||
//Keep <input type="range"..> slider uptodate
|
||||
music_player.subscribe_to_time_tick((time) => {
|
||||
music_player.on_time_tick((time) => {
|
||||
//time == "0","1.2", "1223.21668181"...
|
||||
document.querySelector("#input-seek-range").value = "" + time
|
||||
})
|
||||
|
|
|
@ -3,80 +3,72 @@ export enum SubscribeEvents {
|
|||
FormattedDurationTick,
|
||||
FormattedCurrentTimeTick,
|
||||
}
|
||||
const PubSub = () => {
|
||||
class 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> = []
|
||||
el_current_time_tick: Array<(data: any) => void> = []
|
||||
el_formatted_duration_tick: Array<(data: any) => void> = []
|
||||
el_formatted_current_time_tick: Array<(data: any) => void> = []
|
||||
|
||||
function subscribe(event_name: SubscribeEvents, func: (data: any) => void) {
|
||||
subscribe(event_name: SubscribeEvents, func: (data: any) => void) {
|
||||
switch (event_name) {
|
||||
case SubscribeEvents.CurrentTimeTick: {
|
||||
el_current_time_tick.push(func)
|
||||
this.el_current_time_tick.push(func)
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedDurationTick: {
|
||||
el_formatted_duration_tick.push(func)
|
||||
this.el_formatted_duration_tick.push(func)
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedCurrentTimeTick: {
|
||||
el_formatted_current_time_tick.push(func)
|
||||
this.el_formatted_current_time_tick.push(func)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
function unsubscribe(event_name: SubscribeEvents, func: (data: any) => void) {
|
||||
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)
|
||||
if (this.el_current_time_tick.includes(func)) {
|
||||
this.el_current_time_tick.splice(this.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)
|
||||
if (this.el_formatted_duration_tick.includes(func)) {
|
||||
this.el_formatted_duration_tick.splice(this.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)
|
||||
if (this.el_formatted_duration_tick.includes(func)) {
|
||||
this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
function emit(event_name: SubscribeEvents, data: any) {
|
||||
emit(event_name: SubscribeEvents, data: any) {
|
||||
switch (event_name) {
|
||||
case SubscribeEvents.CurrentTimeTick: {
|
||||
el_current_time_tick.forEach((func) => {
|
||||
this.el_current_time_tick.forEach((func) => {
|
||||
func(data)
|
||||
})
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedDurationTick: {
|
||||
el_formatted_duration_tick.forEach((func) => {
|
||||
this.el_formatted_duration_tick.forEach((func) => {
|
||||
func(data)
|
||||
})
|
||||
break
|
||||
}
|
||||
case SubscribeEvents.FormattedCurrentTimeTick: {
|
||||
el_formatted_current_time_tick.forEach((func) => {
|
||||
this.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 */
|
||||
|
@ -87,51 +79,54 @@ declare global {
|
|||
}
|
||||
|
||||
|
||||
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()
|
||||
export class MusicPlayer {
|
||||
current_song_duration = 0
|
||||
#volume_cache: number
|
||||
is_playing = false
|
||||
time = 0
|
||||
#pub_sub = new PubSub
|
||||
constructor(
|
||||
private audio_context: AudioContext,
|
||||
private audio_element: HTMLAudioElement,
|
||||
public track: MediaElementAudioSourceNode,
|
||||
private gain: GainNode,
|
||||
public volume: number,
|
||||
private current_song_path?: string) {
|
||||
this.#volume_cache = volume
|
||||
}
|
||||
|
||||
function mute_toggle() {
|
||||
if (gain.gain.value == 0) {
|
||||
unmute()
|
||||
mute_toggle() {
|
||||
if (this.gain.gain.value == 0) {
|
||||
this.unmute()
|
||||
} else {
|
||||
mute()
|
||||
this.mute()
|
||||
}
|
||||
}
|
||||
function mute() {
|
||||
volume_cache = gain.gain.value
|
||||
mute() {
|
||||
this.#volume_cache = this.gain.gain.value
|
||||
/* Gentler mute, doesn't pop
|
||||
gain.gain.linearRampToValueAtTime(
|
||||
0,
|
||||
audio_context.currentTime + 0.1
|
||||
);*/
|
||||
volume = gain.gain.value = 0
|
||||
this.volume = this.gain.gain.value = 0
|
||||
}
|
||||
function unmute() {
|
||||
volume = gain.gain.value = volume_cache
|
||||
unmute() {
|
||||
this.volume = this.gain.gain.value = this.#volume_cache
|
||||
}
|
||||
function change_volume(volume_i: number) {
|
||||
volume = gain.gain.value = volume_i
|
||||
change_volume(volume_i: number) {
|
||||
this.volume = this.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) {
|
||||
try_seek_async(new_time: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (track.context.state == "closed" || track.context.state == "suspended") {
|
||||
is_playing = false
|
||||
if (this.track.context.state == "closed" || this.track.context.state == "suspended") {
|
||||
this.is_playing = false
|
||||
reject("Can't seek - track not playing")
|
||||
}
|
||||
audio_element.currentTime = new_time
|
||||
this.audio_element.currentTime = new_time
|
||||
resolve(null)
|
||||
/*audio_element.play().then((s) => resolve(s), (r) => {
|
||||
is_playing = false
|
||||
|
@ -139,45 +134,47 @@ export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTML
|
|||
})*/
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 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 })
|
||||
// THIS MIGHT BE UNNECESSARY? CUZ SEEKING DOESN'T REQUIRE PLAY
|
||||
// /**
|
||||
// * Can try to seek even if the audio context was suspended or closed. Best to use try_seek_async()
|
||||
// */
|
||||
// seek_async(new_time: number) {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// this.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.
|
||||
// */
|
||||
|
||||
seek(new_time: number) {
|
||||
this.audio_element.currentTime = new_time
|
||||
//this.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() {
|
||||
try_play_toggle_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||
if (this.audio_context.state === "suspended" || this.audio_context.state === "closed") {
|
||||
reject("Context closed or suspended")
|
||||
}
|
||||
if (audio_element.paused) {
|
||||
audio_element.play().then((s) => {
|
||||
is_playing = true
|
||||
if (this.audio_element.paused) {
|
||||
this.audio_element.play().then((s) => {
|
||||
this.is_playing = true
|
||||
resolve(s)
|
||||
}, (r) => {
|
||||
is_playing = false
|
||||
this.is_playing = false
|
||||
reject(r)
|
||||
})
|
||||
} else {
|
||||
audio_element.pause()
|
||||
is_playing = false
|
||||
this.audio_element.pause()
|
||||
this.is_playing = false
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
|
@ -185,22 +182,22 @@ export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTML
|
|||
/**
|
||||
* Can try to play even if the audio context was suspended or closed. Best to use try_play_toggle_async()
|
||||
*/
|
||||
function play_toggle_async() {
|
||||
play_toggle_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||
audio_context.resume()
|
||||
if (this.audio_context.state === "suspended" || this.audio_context.state === "closed") {
|
||||
this.audio_context.resume()
|
||||
}
|
||||
if (audio_element.paused) {
|
||||
audio_element.play().then((s) => {
|
||||
is_playing = true
|
||||
if (this.audio_element.paused) {
|
||||
this.audio_element.play().then((s) => {
|
||||
this.is_playing = true
|
||||
resolve(s)
|
||||
}, (r) => {
|
||||
is_playing = false
|
||||
this.is_playing = false
|
||||
reject(r)
|
||||
})
|
||||
} else {
|
||||
audio_element.pause()
|
||||
is_playing = false
|
||||
this.audio_element.pause()
|
||||
this.is_playing = false
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
|
@ -208,32 +205,32 @@ export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTML
|
|||
/**
|
||||
* 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
|
||||
play_toggle() {
|
||||
if (this.audio_element.paused) {
|
||||
this.is_playing = true
|
||||
this.audio_element.play().catch((r) => {
|
||||
this.is_playing = false
|
||||
throw r
|
||||
})
|
||||
} else {
|
||||
is_playing = false
|
||||
audio_element.pause()
|
||||
this.is_playing = false
|
||||
this.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() {
|
||||
try_play_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (is_playing) reject(Error("Already playing"))
|
||||
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||
if (this.is_playing) reject(Error("Already playing"))
|
||||
if (this.audio_context.state === "suspended" || this.audio_context.state === "closed") {
|
||||
reject("Context closed or suspended")
|
||||
}
|
||||
audio_element.play().then((s) => {
|
||||
is_playing = true
|
||||
this.audio_element.play().then((s) => {
|
||||
this.is_playing = true
|
||||
resolve(s)
|
||||
}, (r) => {
|
||||
is_playing = false
|
||||
this.is_playing = false
|
||||
reject(r)
|
||||
})
|
||||
})
|
||||
|
@ -241,17 +238,17 @@ export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTML
|
|||
/**
|
||||
* Will try to play even if the audio context was suspended or closed. Best to use try_play_async()
|
||||
*/
|
||||
function play_async() {
|
||||
play_async() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (is_playing) resolve(null)
|
||||
if (audio_context.state === "suspended" || audio_context.state === "closed") {
|
||||
audio_context.resume()
|
||||
if (this.is_playing) resolve(null)
|
||||
if (this.audio_context.state === "suspended" || this.audio_context.state === "closed") {
|
||||
this.audio_context.resume()
|
||||
}
|
||||
audio_element.play().then((s) => {
|
||||
is_playing = true
|
||||
this.audio_element.play().then((s) => {
|
||||
this.is_playing = true
|
||||
resolve(s)
|
||||
}, (r) => {
|
||||
is_playing = false
|
||||
this.is_playing = false
|
||||
reject(r)
|
||||
})
|
||||
})
|
||||
|
@ -259,67 +256,61 @@ export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTML
|
|||
/**
|
||||
* 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
|
||||
play() {
|
||||
if (this.is_playing) return
|
||||
this.audio_element.play().catch((r) => {
|
||||
this.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
|
||||
pause() {
|
||||
this.audio_element.pause()
|
||||
this.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) {
|
||||
try_new_song_async(path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
audio_element.src = current_song_path = path
|
||||
this.audio_element.src = this.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
|
||||
this.audio_element.addEventListener("canplaythrough", function canplay_listener(s) {
|
||||
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) {
|
||||
this.audio_element.addEventListener("error", function error_listener(e) {
|
||||
controller.abort()
|
||||
reject(e)
|
||||
}, { signal: controller.signal })
|
||||
|
||||
audio_element.addEventListener("stalled", function stalled_listener(e) {
|
||||
this.audio_element.addEventListener("stalled", function stalled_listener(e) {
|
||||
controller.abort()
|
||||
reject(e)
|
||||
}, { signal: controller.signal })
|
||||
*/
|
||||
is_playing = false
|
||||
|
||||
this.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
|
||||
new_song(path: string) {
|
||||
this.audio_element.src = this.current_song_path = path
|
||||
this.current_song_duration = this.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
|
||||
get_formatted_duration() {
|
||||
const dur = this.audio_element.duration
|
||||
this.current_song_duration = this.audio_element.duration
|
||||
|
||||
if (dur == 0 || !dur) return "0:00"
|
||||
|
||||
|
@ -341,8 +332,8 @@ export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTML
|
|||
* 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
|
||||
get_formatted_current_time() {
|
||||
const curr = this.audio_element.currentTime
|
||||
|
||||
if (curr == 0 || !curr) return "0:00"
|
||||
// ~~ is Bitwise OR, equivalent to Math.floor()
|
||||
|
@ -359,170 +350,133 @@ export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTML
|
|||
ret += "" + secs;
|
||||
return ret;
|
||||
}
|
||||
#emit_time() {
|
||||
const request_id = requestAnimationFrame(this.#emit_time.bind(this))
|
||||
if (this.audio_element.ended) this.is_playing = false
|
||||
if (this.audio_element.paused) this.is_playing == false
|
||||
// if use reactively changes volume directly
|
||||
this.gain.gain.value = this.volume
|
||||
|
||||
this.time = this.audio_element.currentTime
|
||||
if (this.#pub_sub.el_current_time_tick.length == 0) cancelAnimationFrame(request_id)
|
||||
this.#pub_sub.emit(SubscribeEvents.CurrentTimeTick, this.time)
|
||||
}
|
||||
#emit_duration_fmt() {
|
||||
const request_id = requestAnimationFrame(this.#emit_duration_fmt.bind(this))
|
||||
const time = this.get_formatted_duration()
|
||||
if (this.#pub_sub.el_formatted_duration_tick.length == 0) cancelAnimationFrame(request_id)
|
||||
this.#pub_sub.emit(SubscribeEvents.FormattedDurationTick, time)
|
||||
}
|
||||
#emit_time_fmt() {
|
||||
const request_id = requestAnimationFrame(this.#emit_time_fmt.bind(this))
|
||||
const time = this.get_formatted_current_time()
|
||||
if (this.#pub_sub.el_formatted_current_time_tick.length == 0) cancelAnimationFrame(request_id)
|
||||
this.#pub_sub.emit(SubscribeEvents.FormattedCurrentTimeTick, time)
|
||||
}
|
||||
/**
|
||||
* 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()
|
||||
on_time_tick(callback: (data: any) => void) {
|
||||
this.#pub_sub.subscribe(SubscribeEvents.CurrentTimeTick, callback)
|
||||
this.#emit_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)
|
||||
on_time_tick_formatted(callback: (data: any) => void) {
|
||||
this.#pub_sub.subscribe(SubscribeEvents.FormattedCurrentTimeTick, callback)
|
||||
this.#emit_time_fmt()
|
||||
}
|
||||
/**
|
||||
* 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,
|
||||
on_duration_formatted(callback: (data: any) => void) {
|
||||
this.#pub_sub.subscribe(SubscribeEvents.FormattedDurationTick, callback)
|
||||
this.#emit_duration_fmt()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
export class MusicPlayerBuilder {
|
||||
#audio_context: AudioContext
|
||||
#gain: GainNode
|
||||
#track: MediaElementAudioSourceNode
|
||||
#volume = 1
|
||||
#prev_node: any;
|
||||
#is_gain_connected = false
|
||||
/**
|
||||
* Creates a context and gain( Gets connected at the end )
|
||||
* 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() {
|
||||
constructor(private audio_element: HTMLAudioElement) {
|
||||
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()
|
||||
this.#audio_context = new AudioContext()
|
||||
this.#track = this.#audio_context.createMediaElementSource(audio_element)
|
||||
this.#gain = this.#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
|
||||
add_analyser() {
|
||||
const analyser = this.#audio_context.createAnalyser()
|
||||
!this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser)
|
||||
this.#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
|
||||
add_stereo_panner_node() {
|
||||
const panner = this.#audio_context.createStereoPanner()
|
||||
!this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner)
|
||||
this.#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
|
||||
add_wave_shaper_node() {
|
||||
const shaper = this.#audio_context.createWaveShaper()
|
||||
!this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper)
|
||||
this.#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
|
||||
connect_custom_node(node: AudioNode) {
|
||||
!this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node)
|
||||
this.#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
|
||||
* 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
|
||||
connect_gain() {
|
||||
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain)
|
||||
this.#prev_node = this.#gain
|
||||
this.#is_gain_connected = true
|
||||
}
|
||||
/**
|
||||
* Finishes the build
|
||||
* @returns {MusicPlayer: () => void}
|
||||
* @returns {Euterpe}
|
||||
*/
|
||||
function build() {
|
||||
if (!is_gain_connected) {
|
||||
!prev_node ? track.connect(gain) : prev_node.connect(gain)
|
||||
prev_node = gain
|
||||
build() {
|
||||
if (!this.#is_gain_connected) {
|
||||
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain)
|
||||
this.#prev_node = this.#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
|
||||
this.#prev_node.connect(this.#audio_context.destination)
|
||||
this.audio_element.preload = "metadata"
|
||||
return new MusicPlayer(this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume)
|
||||
}
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
import { MusicPlayerBuilder } from "@euterpe/player";
|
||||
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer"
|
||||
import { MusicPlayerBuilder } from "@euterpe.js/player";
|
||||
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe.js/visualizer"
|
||||
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
||||
const music_player_builder = MusicPlayerBuilder(audio_el)
|
||||
music_player_builder.start()
|
||||
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
||||
const trapnation_analyser_node = music_player_builder.add_analyser()
|
||||
const bar_analyser_node = music_player_builder.add_analyser()
|
||||
const music_player = music_player_builder.build()
|
||||
|
@ -70,14 +69,14 @@ music_player.try_new_song_async(encodeURI("http://127.0.0.1:4200/nuphory - NVISI
|
|||
is_seeking = false
|
||||
})
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.subscribe_to_formatted_duration_time((time) => {
|
||||
music_player.on_duration_formatted((time) => {
|
||||
document.querySelector("#duration").innerHTML = time
|
||||
document.querySelector("#seek").max = "" + music_player.get_current_duration()
|
||||
document.querySelector("#seek").max = "" + music_player.current_song_duration
|
||||
})
|
||||
music_player.subscribe_to_formatted_current_time_tick((time) => {
|
||||
music_player.on_time_tick_formatted((time) => {
|
||||
document.querySelector("#current").innerHTML = time
|
||||
})
|
||||
music_player.subscribe_to_time_tick((time) => {
|
||||
music_player.on_time_tick((time) => {
|
||||
if (is_seeking) return
|
||||
document.querySelector("#seek").value = "" + time
|
||||
})
|
||||
|
|
|
@ -37,6 +37,7 @@ export class AudioVisual {
|
|||
#canvas_height
|
||||
#canvas_width
|
||||
#fft_data
|
||||
#subscriber_fns = new Array<(data: Float32Array) => void>()
|
||||
constructor(
|
||||
analyzer_node: AnalyserNode,
|
||||
svg_injecting_element: SVGSVGElement,
|
||||
|
@ -256,9 +257,14 @@ export class AudioVisual {
|
|||
return `<path width="100%" height="100%" d="${path}"/>`
|
||||
}
|
||||
|
||||
on_data(fn: ((data: Float32Array) => void)) {
|
||||
this.#subscriber_fns.push(fn)
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.#analyzer_node.getFloatFrequencyData(this.#fft_data)
|
||||
this.#svg_injecting_element.innerHTML = this.#create_svg_element()
|
||||
this.#subscriber_fns.forEach((fn) => fn(this.#fft_data))
|
||||
requestAnimationFrame(this.draw.bind(this))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,16 @@
|
|||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@euterpe/music-library": [
|
||||
"@euterpe.js/euterpe": [
|
||||
"packages/euterpe/src/index.ts"
|
||||
],
|
||||
"@euterpe.js/music-library": [
|
||||
"packages/music-library/src/index.ts"
|
||||
],
|
||||
"@euterpe/player": [
|
||||
"@euterpe.js/player": [
|
||||
"packages/player/src/index.ts"
|
||||
],
|
||||
"@euterpe/visualizer": [
|
||||
"@euterpe.js/visualizer": [
|
||||
"packages/visualizer/src/index.ts"
|
||||
]
|
||||
}
|
||||
|
@ -33,4 +36,4 @@
|
|||
"node_modules",
|
||||
"tmp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue