diff --git a/package.json b/package.json
index 5cfddb6..59cedeb 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/packages/euterpe-web-test/.babelrc b/packages/euterpe-web-test/.babelrc
new file mode 100644
index 0000000..19ebd10
--- /dev/null
+++ b/packages/euterpe-web-test/.babelrc
@@ -0,0 +1,5 @@
+{
+ "presets": [
+ "@nx/js/babel"
+ ]
+}
diff --git a/packages/euterpe-web-test/.eslintrc.json b/packages/euterpe-web-test/.eslintrc.json
new file mode 100644
index 0000000..5313267
--- /dev/null
+++ b/packages/euterpe-web-test/.eslintrc.json
@@ -0,0 +1,33 @@
+{
+ "extends": [
+ "../../.eslintrc.json"
+ ],
+ "ignorePatterns": [
+ "!**/*"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ "*.js",
+ "*.jsx"
+ ],
+ "rules": {}
+ },
+ {
+ "files": [
+ "*.ts",
+ "*.tsx"
+ ],
+ "rules": {}
+ },
+ {
+ "files": [
+ "*.js",
+ "*.jsx"
+ ],
+ "rules": {}
+ }
+ ]
+}
diff --git a/packages/euterpe-web-test/index.html b/packages/euterpe-web-test/index.html
new file mode 100644
index 0000000..d5b8f1a
--- /dev/null
+++ b/packages/euterpe-web-test/index.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ EuterpeTest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-:--
+
+
-:--
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/euterpe-web-test/project.json b/packages/euterpe-web-test/project.json
new file mode 100644
index 0000000..152ae65
--- /dev/null
+++ b/packages/euterpe-web-test/project.json
@@ -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"
+ ]
+ }
+ }
+ }
+}
diff --git a/packages/euterpe-web-test/public/16.サニーボーイ・ラプソディ.ogg b/packages/euterpe-web-test/public/16.サニーボーイ・ラプソディ.ogg
new file mode 100644
index 0000000..54389a4
Binary files /dev/null and b/packages/euterpe-web-test/public/16.サニーボーイ・ラプソディ.ogg differ
diff --git a/packages/euterpe-web-test/public/Jamie xx - Sleep Sound.mp3 b/packages/euterpe-web-test/public/Jamie xx - Sleep Sound.mp3
new file mode 100644
index 0000000..15e7832
Binary files /dev/null and b/packages/euterpe-web-test/public/Jamie xx - Sleep Sound.mp3 differ
diff --git a/packages/euterpe-web-test/public/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3 b/packages/euterpe-web-test/public/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3
new file mode 100644
index 0000000..e4ffc91
Binary files /dev/null and b/packages/euterpe-web-test/public/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3 differ
diff --git a/packages/euterpe-web-test/public/favicon.ico b/packages/euterpe-web-test/public/favicon.ico
new file mode 100644
index 0000000..317ebcb
Binary files /dev/null and b/packages/euterpe-web-test/public/favicon.ico differ
diff --git a/packages/euterpe-web-test/public/janz - wish.mp3 b/packages/euterpe-web-test/public/janz - wish.mp3
new file mode 100644
index 0000000..d98d63c
Binary files /dev/null and b/packages/euterpe-web-test/public/janz - wish.mp3 differ
diff --git a/packages/euterpe-web-test/src/assets/.gitkeep b/packages/euterpe-web-test/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/packages/euterpe-web-test/src/db.ts b/packages/euterpe-web-test/src/db.ts
new file mode 100644
index 0000000..fa0a636
--- /dev/null
+++ b/packages/euterpe-web-test/src/db.ts
@@ -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")
+ })
+])
+
diff --git a/packages/euterpe-web-test/src/main.ts b/packages/euterpe-web-test/src/main.ts
new file mode 100644
index 0000000..634432d
--- /dev/null
+++ b/packages/euterpe-web-test/src/main.ts
@@ -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()
+}
\ No newline at end of file
diff --git a/packages/euterpe-web-test/src/styles.css b/packages/euterpe-web-test/src/styles.css
new file mode 100644
index 0000000..9c81066
--- /dev/null
+++ b/packages/euterpe-web-test/src/styles.css
@@ -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;
+}
\ No newline at end of file
diff --git a/packages/euterpe-web-test/tsconfig.app.json b/packages/euterpe-web-test/tsconfig.app.json
new file mode 100644
index 0000000..3253fc8
--- /dev/null
+++ b/packages/euterpe-web-test/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": ["node"]
+ },
+ "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/euterpe-web-test/tsconfig.json b/packages/euterpe-web-test/tsconfig.json
new file mode 100644
index 0000000..af79c85
--- /dev/null
+++ b/packages/euterpe-web-test/tsconfig.json
@@ -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"
+ }
+ ]
+}
diff --git a/packages/euterpe-web-test/vite.config.ts b/packages/euterpe-web-test/vite.config.ts
new file mode 100644
index 0000000..1eed4fd
--- /dev/null
+++ b/packages/euterpe-web-test/vite.config.ts
@@ -0,0 +1,33 @@
+///
+import { defineConfig } from "vite"
+
+import viteTsConfigPaths from "vite-tsconfig-paths"
+
+export default defineConfig({
+ cacheDir: "../../node_modules/.vite/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: '../../',
+ // }),
+ // ],
+ // },
+})
diff --git a/packages/euterpe/.eslintrc.json b/packages/euterpe/.eslintrc.json
new file mode 100644
index 0000000..5313267
--- /dev/null
+++ b/packages/euterpe/.eslintrc.json
@@ -0,0 +1,33 @@
+{
+ "extends": [
+ "../../.eslintrc.json"
+ ],
+ "ignorePatterns": [
+ "!**/*"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ "*.js",
+ "*.jsx"
+ ],
+ "rules": {}
+ },
+ {
+ "files": [
+ "*.ts",
+ "*.tsx"
+ ],
+ "rules": {}
+ },
+ {
+ "files": [
+ "*.js",
+ "*.jsx"
+ ],
+ "rules": {}
+ }
+ ]
+}
diff --git a/packages/euterpe/README.md b/packages/euterpe/README.md
new file mode 100644
index 0000000..31725b2
--- /dev/null
+++ b/packages/euterpe/README.md
@@ -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.
\ No newline at end of file
diff --git a/packages/euterpe/package.json b/packages/euterpe/package.json
new file mode 100644
index 0000000..9ed7172
--- /dev/null
+++ b/packages/euterpe/package.json
@@ -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"
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/euterpe/project.json b/packages/euterpe/project.json
new file mode 100644
index 0000000..4a9a193
--- /dev/null
+++ b/packages/euterpe/project.json
@@ -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": []
+}
diff --git a/packages/euterpe/src/index.ts b/packages/euterpe/src/index.ts
new file mode 100644
index 0000000..086e4c5
--- /dev/null
+++ b/packages/euterpe/src/index.ts
@@ -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)
+ }
+}
diff --git a/packages/euterpe/src/lib/euterpe.ts b/packages/euterpe/src/lib/euterpe.ts
new file mode 100644
index 0000000..d1d16af
--- /dev/null
+++ b/packages/euterpe/src/lib/euterpe.ts
@@ -0,0 +1,3 @@
+export function euterpe(): string {
+ return "euterpe"
+}
diff --git a/packages/euterpe/tsconfig.json b/packages/euterpe/tsconfig.json
new file mode 100644
index 0000000..7758f7b
--- /dev/null
+++ b/packages/euterpe/tsconfig.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/euterpe/tsconfig.lib.json b/packages/euterpe/tsconfig.lib.json
new file mode 100644
index 0000000..33eca2c
--- /dev/null
+++ b/packages/euterpe/tsconfig.lib.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "declaration": true,
+ "types": ["node"]
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
+}
diff --git a/packages/music-library-web-test/src/db.ts b/packages/music-library-web-test/src/db.ts
index dfa24e3..16cb9f7 100644
--- a/packages/music-library-web-test/src/db.ts
+++ b/packages/music-library-web-test/src/db.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([
diff --git a/packages/music-library-web-test/src/main.ts b/packages/music-library-web-test/src/main.ts
index eed8d5b..b0bb067 100644
--- a/packages/music-library-web-test/src/main.ts
+++ b/packages/music-library-web-test/src/main.ts
@@ -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)
diff --git a/packages/music-library/tsconfig.json b/packages/music-library/tsconfig.json
index db7b566..7758f7b 100644
--- a/packages/music-library/tsconfig.json
+++ b/packages/music-library/tsconfig.json
@@ -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"
}
]
-}
+}
\ No newline at end of file
diff --git a/packages/player-web-test/src/main.ts b/packages/player-web-test/src/main.ts
index 18c14cd..115557f 100644
--- a/packages/player-web-test/src/main.ts
+++ b/packages/player-web-test/src/main.ts
@@ -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)
diff --git a/packages/player/README.md b/packages/player/README.md
index 276dd80..472dcf4 100644
--- a/packages/player/README.md
+++ b/packages/player/README.md
@@ -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 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
})
diff --git a/packages/player/src/index.ts b/packages/player/src/index.ts
index 2e1645b..12ceb74 100644
--- a/packages/player/src/index.ts
+++ b/packages/player/src/index.ts
@@ -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)
}
}
\ No newline at end of file
diff --git a/packages/visualizer-web-test/src/main.ts b/packages/visualizer-web-test/src/main.ts
index fdda253..fb7df7f 100644
--- a/packages/visualizer-web-test/src/main.ts
+++ b/packages/visualizer-web-test/src/main.ts
@@ -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
})
diff --git a/packages/visualizer/src/index.ts b/packages/visualizer/src/index.ts
index bc12b30..5fd6676 100644
--- a/packages/visualizer/src/index.ts
+++ b/packages/visualizer/src/index.ts
@@ -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 ``
}
+ 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))
}
}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 7aef2bc..bf68c10 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -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"
]
-}
\ No newline at end of file
+}