fixed all demos, readmes, formatting, some bugs
This commit is contained in:
parent
cd97233bce
commit
eb4e4f91d5
18 changed files with 652 additions and 431 deletions
22
README.md
22
README.md
|
@ -1,22 +0,0 @@
|
|||
# Euterpe
|
||||
|
||||
The sole mono-repo for all things Euterpe.
|
||||
|
||||
## Understand this workspace
|
||||
|
||||
Run `npx nx graph` to see a diagram of the dependencies of the projects.
|
||||
All programs go like this: `npx nx {package script} {package}`
|
||||
|
||||
## Test this workspace
|
||||
|
||||
Run `npx nx server player-web-test` to see the music player in a minimal demo.
|
||||
Run `npx nx server visualizer-web-test` to see the music player in a minimal demo.
|
||||
|
||||
## Build
|
||||
|
||||
Run `npx nx build player` to build the player.
|
||||
Run `npx nx build visualizer` to build the player.
|
||||
|
||||
## Publish
|
||||
|
||||
First build, then run `npm run publish-[package]`(scripts)
|
1
README.md
Symbolic link
1
README.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
./packages/euterpe/README.md
|
|
@ -3,16 +3,13 @@ import { EuterpeBuilder } from "@euterpe.js/euterpe"
|
|||
|
||||
let is_seeking = false
|
||||
// document.addEventListener("click", start, { once: true })
|
||||
const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db).build()
|
||||
const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db, {
|
||||
auto_play_after_seek: true,
|
||||
auto_play_after_changing_song: true,
|
||||
use_only_pathname_url: true
|
||||
}).build()
|
||||
add_library_to_dom()
|
||||
|
||||
euterpe.try_preload_song(0).then(
|
||||
() => {
|
||||
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
|
||||
},
|
||||
(e) => console.log(e + " Failed to preload")
|
||||
)
|
||||
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
try {
|
||||
euterpe.try_seek(e.target?.valueAsNumber)
|
||||
|
@ -23,7 +20,7 @@ document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
|||
is_seeking = false
|
||||
})
|
||||
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
// Subscriptions to song and AudioContext changes, eg. time, name..
|
||||
euterpe.on_duration_formatted((time) => {
|
||||
document.querySelector("#duration")!.innerHTML = time
|
||||
document.querySelector("#seek")!.max = "" + euterpe.current_song_duration
|
||||
|
@ -32,6 +29,7 @@ euterpe.on_duration_formatted((time) => {
|
|||
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
|
||||
|
@ -39,44 +37,49 @@ euterpe.on_time_tick((time) => {
|
|||
dev_history_update()
|
||||
})
|
||||
|
||||
document.querySelector("#previous")?.addEventListener("click", () => {
|
||||
euterpe.try_previous_song_looping().then(
|
||||
() => {
|
||||
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
|
||||
},
|
||||
(e) => alert(e + "Failed to change song")
|
||||
)
|
||||
euterpe.on_song_change((_, song_name) => {
|
||||
document.querySelector("#text-playing")!.innerHTML = song_name
|
||||
})
|
||||
|
||||
//preload after setting all listeners to make sure you capture the song update!
|
||||
euterpe.try_preload_song(0).catch((e) => console.log(e + " Failed to preload"))
|
||||
|
||||
document.querySelector("#previous")?.addEventListener("click", () => {
|
||||
euterpe.try_previous_song_looping().catch((e) => alert(e + "Failed to change song"))
|
||||
})
|
||||
|
||||
document.querySelector("#next")?.addEventListener("click", () => {
|
||||
euterpe.try_next_song_looping().then(
|
||||
() => {
|
||||
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
|
||||
},
|
||||
(e) => alert(e + "Failed to change song")
|
||||
)
|
||||
euterpe.try_next_song_looping().catch((e) => alert(e + "Failed to change song"))
|
||||
})
|
||||
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
euterpe.try_play().catch((e) => alert("Failed to play, " + e))
|
||||
})
|
||||
|
||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||
euterpe.pause()
|
||||
})
|
||||
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
euterpe.mute()
|
||||
})
|
||||
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
euterpe.unmute()
|
||||
})
|
||||
|
||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||
euterpe.mute_toggle()
|
||||
})
|
||||
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
euterpe.try_play_toggle().catch((e) => alert("failed to toggle pause/play!" + e))
|
||||
})
|
||||
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
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
|
||||
|
|
|
@ -2,13 +2,16 @@
|
|||
|
||||
Fully featured AudioContext music player for the web.
|
||||
|
||||
## Euterpe in production:
|
||||
- Hypertrance ( [site](https://hypertrance.eu/), [repository](https://github.com/nuphory/hypertrance.eu) )
|
||||
|
||||
Features:
|
||||
- "Local" library/Database for songs, collections etc.
|
||||
- Queue
|
||||
- History
|
||||
- "Local" library/database for songs, collections, artists, waveforms, artist links and much more!
|
||||
- Queue and history
|
||||
- Easy way to create Vector based audio visuals
|
||||
- Library automatization based on folder/file structure, preprocessing and encoding media files for all platforms
|
||||
- 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.
|
||||
- Frontend library agnostic
|
||||
|
||||
## How to use:
|
||||
|
||||
|
@ -19,4 +22,155 @@ Since this package is just a compilation of our smaller modules, you can read in
|
|||
- [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.
|
||||
You can further check out how to automate database creation from folder structure, auto encode media for all platforms and create waveform svgs for songs here:
|
||||
- [Euterpe Preprocessor](https://www.npmjs.com/package/@euterpe.js/preprocessor)
|
||||
|
||||
This module builds on those, and further adds functions for playing backwards, forwards and managing the queue.
|
||||
|
||||
First we create a database with our songs
|
||||
|
||||
`db.ts`
|
||||
```ts
|
||||
import { DB, Song, Artist, Ref, RefTo, Platforms } from "@euterpe.js/music-library"
|
||||
export const db = new 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 Artist({
|
||||
name: "Machinedrum",
|
||||
}),
|
||||
new Artist({
|
||||
name: "Tanerélle",
|
||||
}),
|
||||
new Artist({
|
||||
name: "Mono/Poly",
|
||||
}),
|
||||
new Artist({
|
||||
name: "IMANU",
|
||||
links: [
|
||||
[Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]
|
||||
]
|
||||
}),
|
||||
])
|
||||
db.add([
|
||||
new Song({
|
||||
//Refrences are constructed as such. This allows to get to the artist from either collection or song
|
||||
artists: [new Ref(RefTo.Artists, 2), new Ref(RefTo.Artists, 3), new Ref(RefTo.Artists, 4)],
|
||||
duration: 252,
|
||||
name: "Star",
|
||||
remix_artists: [new Ref(RefTo.Artists, 5)],
|
||||
url: new URL("http://" + window.location.host + "/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3")
|
||||
}),
|
||||
])
|
||||
|
||||
```
|
||||
|
||||
Then we build our Euterpe player and assign the db to it. Then it's just a matter of creating event listeners to the dom and binding them to Euterpes functions.
|
||||
|
||||
`main.ts`
|
||||
```ts
|
||||
import { db } from "./db";
|
||||
import { EuterpeBuilder } from "@euterpe.js/euterpe"
|
||||
|
||||
let is_seeking = false
|
||||
const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db)
|
||||
.build()
|
||||
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
try {
|
||||
euterpe.try_seek(e.target?.valueAsNumber)
|
||||
} catch {
|
||||
alert("Failed seeking! " + e)
|
||||
}
|
||||
is_seeking = false
|
||||
})
|
||||
|
||||
euterpe.on_song_change((_, song_name) => {
|
||||
document.querySelector("#text-playing")!.innerHTML = song_name
|
||||
})
|
||||
|
||||
document.querySelector("#previous")?.addEventListener("click", () => {
|
||||
euterpe.try_previous_song_looping().catch((e) => alert(e + "Failed to change song"))
|
||||
})
|
||||
|
||||
document.querySelector("#next")?.addEventListener("click", () => {
|
||||
euterpe.try_next_song_looping().catch((e) => alert(e + "Failed to change song"))
|
||||
})
|
||||
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
euterpe.mute()
|
||||
})
|
||||
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
euterpe.unmute()
|
||||
})
|
||||
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
euterpe.try_play_toggle().catch((e) => alert("failed to toggle pause/play!" + e))
|
||||
})
|
||||
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
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
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
Then we can set up listeners to Euterpes events to keep the UI up todate as well
|
||||
|
||||
`main.ts`
|
||||
```ts
|
||||
//...
|
||||
// Subscriptions to song and AudioContext changes, eg. time, name..
|
||||
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()
|
||||
})
|
||||
|
||||
euterpe.on_song_change((_, song_name) => {
|
||||
document.querySelector("#text-playing")!.innerHTML = song_name
|
||||
})
|
||||
|
||||
//preload after setting all listeners to make sure you capture the song update!
|
||||
euterpe.try_preload_song(0).catch((e) => console.log(e + " Failed to preload"))
|
||||
|
||||
//..
|
||||
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()
|
||||
}
|
||||
|
||||
```
|
||||
and it's done!
|
||||
For vizualizer demo, or how to use the core parts of the Euterpe libraries separately, check out the individual repos readmes.
|
||||
|
||||
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
{
|
||||
"name": "@euterpe.js/euterpe",
|
||||
"version": "2.0.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "@euterpe.js/euterpe",
|
||||
"version": "2.1.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ class Euterpe extends MusicPlayer {
|
|||
current_song_id = 0
|
||||
queue: Song[] = []
|
||||
played_history: Song[] = []
|
||||
#sub_list: Array<(song_id: number, song_name: string) => void> = []
|
||||
constructor(
|
||||
public db: DB,
|
||||
audio_context: AudioContext,
|
||||
|
@ -22,15 +23,34 @@ class Euterpe extends MusicPlayer {
|
|||
) {
|
||||
super(audio_context, audio_element, track, gain, volume, current_song_path)
|
||||
|
||||
audio_element.addEventListener("ended", () => {
|
||||
audio_element.currentTime = 0
|
||||
audio_element.pause()
|
||||
audio_element.addEventListener("ended", async () => {
|
||||
try {
|
||||
this.try_next_song()
|
||||
} catch (e) { }
|
||||
await this.try_next_song()
|
||||
} catch (e) {
|
||||
audio_element.currentTime = 0
|
||||
this.pause()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafe, throws error if failed. Use try_seek or seek unless you don't care about the result.
|
||||
*/
|
||||
override seek(new_time: number) {
|
||||
super.seek(new_time)
|
||||
if (this.options?.auto_play_after_seek) this.play()
|
||||
}
|
||||
|
||||
/**
|
||||
* Safer seek. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed.
|
||||
* won't resume playback by default unless `EuterpeBuilderOptions.auto_play_after_seek == true`
|
||||
* @throws if "Can't seek - Audiocontext is not running"
|
||||
*/
|
||||
override async try_seek(new_time: number) {
|
||||
await super.try_seek(new_time)
|
||||
if (this.options?.auto_play_after_seek) this.play()
|
||||
}
|
||||
|
||||
/**
|
||||
* Use to load song on page load.
|
||||
* @throws if song with ID doesn't exist
|
||||
|
@ -39,8 +59,9 @@ class Euterpe extends MusicPlayer {
|
|||
const next = this.db.songs.find((song) => song!.id == id)
|
||||
if (!next) throw new Error(`Song with id ${id} doesn't exist`)
|
||||
else {
|
||||
await this.try_new_song(next.url.pathname)
|
||||
this.current_song = next
|
||||
this.#emit_on_song_change()
|
||||
await this.try_new_song(next.url.pathname)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,10 +84,11 @@ class Euterpe extends MusicPlayer {
|
|||
}
|
||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||
await this.try_new_song(url)
|
||||
await this.try_play()
|
||||
if (this.options?.auto_play_after_changing_song) await this.try_play()
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
this.#emit_on_song_change()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,14 +109,15 @@ class Euterpe extends MusicPlayer {
|
|||
}
|
||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||
await this.try_new_song(url)
|
||||
await this.try_play()
|
||||
if (this.options?.auto_play_after_changing_song) await this.try_play()
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
this.#emit_on_song_change()
|
||||
}
|
||||
|
||||
/**
|
||||
* Won't tell you if the playback was successsful & wil loop back if already on last song. Best use try_next_song_async()
|
||||
* Won't tell you if the playback was successsful & will loop back if already on last song. Best use try_next_song()
|
||||
* If queue present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
next_song_looping() {
|
||||
|
@ -111,14 +134,15 @@ class Euterpe extends MusicPlayer {
|
|||
}
|
||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||
this.new_song(url)
|
||||
this.play()
|
||||
if (this.options?.auto_play_after_changing_song) 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!
|
||||
this.#emit_on_song_change()
|
||||
}
|
||||
|
||||
/**
|
||||
* Won't tell you if the playback was successsful, won't loop back if already on last song and won't throw error if attempted. Best use next_song_async()
|
||||
* Won't tell you if the playback was successsful, won't loop back if already on last song and won't throw error if attempted. Best use next_song()
|
||||
* If queue present, uses that, if not, relies on Song ID directly from DB
|
||||
*/
|
||||
next_song() {
|
||||
|
@ -133,39 +157,42 @@ class Euterpe extends MusicPlayer {
|
|||
}
|
||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||
this.new_song(url)
|
||||
this.play()
|
||||
if (this.options?.auto_play_after_changing_song) 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!
|
||||
this.#emit_on_song_change()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Uses safer try_play. Normal play / play will try to start the player even if the track hasn't started yet, or was previously suspended/closed
|
||||
*/
|
||||
async try_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.try_new_song(new_song.url.pathname)
|
||||
await this.try_play()
|
||||
if (this.options?.auto_play_after_changing_song) await this.try_play()
|
||||
if (this.current_song) this.played_history.push(this.current_song)
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
this.#emit_on_song_change()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Won't 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()
|
||||
* Won't throw an error if new ID not found. Won't tell you if the play was successful, best use specific_song() or try_specific_song()
|
||||
*/
|
||||
specific_song(new_song_id: number) {
|
||||
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
|
||||
if (!new_song) return
|
||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||
this.new_song(url)
|
||||
this.play()
|
||||
if (this.options?.auto_play_after_changing_song) 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!
|
||||
this.#emit_on_song_change()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -187,10 +214,10 @@ class Euterpe extends MusicPlayer {
|
|||
}
|
||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||
await this.try_new_song(url)
|
||||
await this.try_play()
|
||||
//if (this.current_song) this.played_history.push(this.current_song)
|
||||
if (this.options?.auto_play_after_changing_song) await this.try_play()
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
this.#emit_on_song_change()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -211,10 +238,10 @@ class Euterpe extends MusicPlayer {
|
|||
}
|
||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||
await this.try_new_song(url)
|
||||
await this.try_play()
|
||||
//if (this.current_song) this.played_history.push(this.current_song)
|
||||
if (this.options?.auto_play_after_changing_song) await this.try_play()
|
||||
this.current_song = new_song
|
||||
this.current_song_id = new_song.id!
|
||||
this.#emit_on_song_change()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -235,9 +262,9 @@ class Euterpe extends MusicPlayer {
|
|||
}
|
||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||
this.new_song(url)
|
||||
this.play()
|
||||
//if (this.current_song) this.played_history.push(this.current_song)
|
||||
if (this.options?.auto_play_after_changing_song) this.play()
|
||||
this.current_song_id = new_song.id!
|
||||
this.#emit_on_song_change()
|
||||
this.current_song = new_song
|
||||
}
|
||||
|
||||
|
@ -259,10 +286,10 @@ class Euterpe extends MusicPlayer {
|
|||
}
|
||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||
this.new_song(url)
|
||||
this.play()
|
||||
//if (this.current_song) this.played_history.push(this.current_song)
|
||||
if (this.options?.auto_play_after_changing_song) this.play()
|
||||
this.current_song_id = new_song.id!
|
||||
this.current_song = new_song
|
||||
this.#emit_on_song_change()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -350,9 +377,21 @@ class Euterpe extends MusicPlayer {
|
|||
if (i == -1) return
|
||||
return this.queue.splice(i, 1)
|
||||
}
|
||||
|
||||
on_song_change(callback: (song_id: number, song_name: string) => void) {
|
||||
this.#sub_list.push(callback)
|
||||
}
|
||||
|
||||
#emit_on_song_change() {
|
||||
for (const func of this.#sub_list) {
|
||||
func(this.current_song_id, this.format_current_song(this.current_song_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
type BuilderOptions = {
|
||||
use_only_pathname_url?: boolean
|
||||
auto_play_after_seek?: boolean
|
||||
auto_play_after_changing_song?: boolean
|
||||
}
|
||||
|
||||
class EuterpeBuilder {
|
||||
|
|
|
@ -10,100 +10,109 @@ music_player.change_volume(1)
|
|||
let curr_song_id = 1
|
||||
const elem_curr_song = document.querySelector("#text-playing")
|
||||
|
||||
music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then(
|
||||
() => {
|
||||
let is_seeking = false
|
||||
change_current_song_text(db)
|
||||
let is_seeking = false
|
||||
|
||||
document.querySelector("#previous")?.addEventListener("click", () => {
|
||||
curr_song_id--
|
||||
if (curr_song_id < 0) curr_song_id = 2
|
||||
music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then(
|
||||
(s) => {
|
||||
change_current_song_text(db)
|
||||
music_player.play_async().catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
(e) => {
|
||||
console.log(e)
|
||||
}
|
||||
)
|
||||
})
|
||||
document.querySelector("#next")?.addEventListener("click", () => {
|
||||
curr_song_id++
|
||||
if (curr_song_id > 2) curr_song_id = 0
|
||||
music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then(
|
||||
(s) => {
|
||||
change_current_song_text(db)
|
||||
music_player.play_async().catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
(e) => {
|
||||
console.log(e)
|
||||
}
|
||||
)
|
||||
})
|
||||
document.querySelector("#previous")?.addEventListener("click", () => {
|
||||
curr_song_id--
|
||||
if (curr_song_id < 0) curr_song_id = 2
|
||||
music_player.try_new_song(db.songs[curr_song_id].url.pathname).then(
|
||||
() => {
|
||||
change_current_song_text(db)
|
||||
music_player.try_play().catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
(e) => {
|
||||
console.log(e)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
music_player.play_async().then(
|
||||
() => {
|
||||
console.log("Playing!")
|
||||
},
|
||||
(e) => alert("Failed to play, " + e)
|
||||
)
|
||||
})
|
||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||
music_player.pause()
|
||||
})
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
music_player.mute()
|
||||
})
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
music_player.unmute()
|
||||
})
|
||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||
music_player.mute_toggle()
|
||||
})
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
music_player.play_toggle_async().then(
|
||||
(s) => console.log("toggled play/pause"),
|
||||
(e) => alert("failed to toggle pause/play!" + e)
|
||||
)
|
||||
})
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
music_player.change_volume(e.target?.valueAsNumber)
|
||||
})
|
||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||
is_seeking = true
|
||||
})
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
music_player.try_seek_async(e.target?.valueAsNumber).then(
|
||||
() => {
|
||||
console.log("seeked to " + e.target?.valueAsNumber)
|
||||
},
|
||||
() => {
|
||||
alert("Failed seeking! " + e)
|
||||
}
|
||||
)
|
||||
is_seeking = false
|
||||
})
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.subscribe_to_formatted_duration_time((time) => {
|
||||
document.querySelector("#duration").innerHTML = time
|
||||
document.querySelector("#seek").max = "" + music_player.get_current_duration()
|
||||
})
|
||||
music_player.subscribe_to_formatted_current_time_tick((time) => {
|
||||
document.querySelector("#current").innerHTML = time
|
||||
})
|
||||
music_player.subscribe_to_time_tick((time) => {
|
||||
if (is_seeking) return
|
||||
document.querySelector("#seek").value = "" + time
|
||||
})
|
||||
},
|
||||
(e) => console.log(e)
|
||||
)
|
||||
document.querySelector("#next")?.addEventListener("click", () => {
|
||||
curr_song_id++
|
||||
if (curr_song_id > 2) curr_song_id = 0
|
||||
music_player.try_new_song(db.songs[curr_song_id].url.pathname).then(
|
||||
() => {
|
||||
change_current_song_text(db)
|
||||
music_player.try_play().catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
(e) => {
|
||||
console.log(e)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
music_player.try_play().then(
|
||||
() => {
|
||||
console.log("Playing!")
|
||||
},
|
||||
(e) => alert("Failed to play, " + e)
|
||||
)
|
||||
})
|
||||
|
||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||
music_player.pause()
|
||||
})
|
||||
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
music_player.mute()
|
||||
})
|
||||
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
music_player.unmute()
|
||||
})
|
||||
|
||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||
music_player.mute_toggle()
|
||||
})
|
||||
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
music_player.try_play_toggle().then(
|
||||
(s) => console.log("toggled play/pause"),
|
||||
(e) => alert("failed to toggle pause/play!" + e)
|
||||
)
|
||||
})
|
||||
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
music_player.change_volume(e.target?.valueAsNumber)
|
||||
})
|
||||
|
||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||
is_seeking = true
|
||||
})
|
||||
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
music_player.try_seek(e.target?.valueAsNumber).then(
|
||||
() => {
|
||||
console.log("seeked to " + e.target?.valueAsNumber)
|
||||
},
|
||||
() => {
|
||||
alert("Failed seeking! " + e)
|
||||
}
|
||||
)
|
||||
is_seeking = false
|
||||
})
|
||||
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.on_duration_formatted((time) => {
|
||||
document.querySelector("#duration")!.innerHTML = time
|
||||
document.querySelector("#seek")!.max = "" + music_player.current_song_duration
|
||||
})
|
||||
|
||||
music_player.on_time_tick_formatted((time) => {
|
||||
document.querySelector("#current")!.innerHTML = time
|
||||
})
|
||||
|
||||
music_player.on_time_tick((time) => {
|
||||
if (is_seeking) return
|
||||
document.querySelector("#seek")!.value = "" + time
|
||||
})
|
||||
|
||||
music_player.try_new_song(db.songs[curr_song_id].url.pathname).then(() => {})
|
||||
change_current_song_text(db)
|
||||
|
||||
function change_current_song_text(db: DB) {
|
||||
const curr_song = db.songs[curr_song_id]
|
||||
|
|
|
@ -62,17 +62,17 @@ let curr_song_id = 1;
|
|||
document.querySelector("#previous")?.addEventListener("click", () => {
|
||||
curr_song_id--
|
||||
if (curr_song_id < 0) curr_song_id = 2
|
||||
music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then((s) => {
|
||||
music_player.try_new_song(db.songs[curr_song_id].url.pathname).then((s) => {
|
||||
change_current_song_text(db)
|
||||
music_player.play_async().catch((err) => { console.log(err) })
|
||||
music_player.try_play().catch((err) => { console.log(err) })
|
||||
}, (e) => { console.log(e) })
|
||||
})
|
||||
document.querySelector("#next")?.addEventListener("click", () => {
|
||||
curr_song_id++
|
||||
if (curr_song_id > 2) curr_song_id = 0
|
||||
music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then((s) => {
|
||||
music_player.try_new_song(db.songs[curr_song_id].url.pathname).then((s) => {
|
||||
change_current_song_text(db)
|
||||
music_player.play_async().catch((err) => { console.log(err) })
|
||||
music_player.try_play().catch((err) => { console.log(err) })
|
||||
}, (e) => { console.log(e) })
|
||||
})
|
||||
```
|
||||
|
@ -151,4 +151,4 @@ class Collection {
|
|||
publish_date?: Date
|
||||
id?: ID
|
||||
}
|
||||
```
|
||||
```
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
{
|
||||
"name": "@euterpe.js/music-library",
|
||||
"version": "1.0.11",
|
||||
"type": "module",
|
||||
"description": "A simple music library, acting as a Local DB as JS Object. Contains everything a person would need to store their music data for website playback.",
|
||||
"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/music-library#readme",
|
||||
"keywords": [
|
||||
"audio",
|
||||
"library",
|
||||
"music-database",
|
||||
"audio-player",
|
||||
"webaudio",
|
||||
"database",
|
||||
"db"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.d.ts",
|
||||
"import": "./src/index.js",
|
||||
"require": "./src/lib/music-library.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "@euterpe.js/music-library",
|
||||
"version": "1.0.12",
|
||||
"type": "module",
|
||||
"description": "A simple music library, acting as a Local DB as JS Object. Contains everything a person would need to store their music data for website playback.",
|
||||
"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/music-library#readme",
|
||||
"keywords": [
|
||||
"audio",
|
||||
"library",
|
||||
"music-database",
|
||||
"audio-player",
|
||||
"webaudio",
|
||||
"database",
|
||||
"db"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.d.ts",
|
||||
"import": "./src/index.js",
|
||||
"require": "./src/lib/music-library.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,63 +4,72 @@ const music_player_builder = new MusicPlayerBuilder(audio_el)
|
|||
const music_player = music_player_builder.build()
|
||||
music_player.change_volume(1)
|
||||
|
||||
music_player.try_new_song(encodeURI("http://" + window.location.host + "/nuphory - NVISION (EXTENDED MIX).ogg")).then(
|
||||
() => {
|
||||
let is_seeking = false
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
//const analyser_node = music_player_builder.add_analyser()
|
||||
music_player.try_play().then(
|
||||
() => {
|
||||
console.log("Playing!")
|
||||
},
|
||||
(e) => alert("Failed to play, " + e)
|
||||
)
|
||||
})
|
||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||
music_player.pause()
|
||||
})
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
music_player.mute()
|
||||
})
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
music_player.unmute()
|
||||
})
|
||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||
music_player.mute_toggle()
|
||||
})
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
music_player.try_play_toggle().then(
|
||||
(s) => console.log("toggled play/pause"),
|
||||
(e) => alert("failed to toggle pause/play!" + e)
|
||||
)
|
||||
})
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
music_player.change_volume(e.target?.valueAsNumber)
|
||||
})
|
||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||
is_seeking = true
|
||||
})
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
try {
|
||||
music_player.try_seek(e.target?.valueAsNumber)
|
||||
console.log("seeked to " + e.target?.valueAsNumber)
|
||||
} catch (e) {
|
||||
alert("Failed seeking! " + e)
|
||||
}
|
||||
is_seeking = false
|
||||
})
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.on_duration_formatted((time) => {
|
||||
document.querySelector("#duration")!.innerHTML = time
|
||||
document.querySelector("#seek")!.max = "" + music_player.current_song_duration
|
||||
})
|
||||
music_player.on_time_tick_formatted((time) => {
|
||||
document.querySelector("#current")!.innerHTML = time
|
||||
})
|
||||
music_player.on_time_tick((time) => {
|
||||
if (is_seeking) return
|
||||
document.querySelector("#seek")!.value = "" + time
|
||||
})
|
||||
},
|
||||
(e) => console.log(e)
|
||||
)
|
||||
let is_seeking = false
|
||||
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
//const analyser_node = music_player_builder.add_analyser()
|
||||
music_player.try_play().then(
|
||||
() => {
|
||||
console.log("Playing!")
|
||||
},
|
||||
(e) => alert("Failed to play, " + e)
|
||||
)
|
||||
})
|
||||
|
||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||
music_player.pause()
|
||||
})
|
||||
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
music_player.mute()
|
||||
})
|
||||
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
music_player.unmute()
|
||||
})
|
||||
|
||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||
music_player.mute_toggle()
|
||||
})
|
||||
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
music_player.try_play_toggle().then(
|
||||
(s) => console.log("toggled play/pause"),
|
||||
(e) => alert("failed to toggle pause/play!" + e)
|
||||
)
|
||||
})
|
||||
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
music_player.change_volume(e.target?.valueAsNumber)
|
||||
})
|
||||
|
||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||
is_seeking = true
|
||||
})
|
||||
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
try {
|
||||
music_player.try_seek(e.target?.valueAsNumber)
|
||||
console.log("seeked to " + e.target?.valueAsNumber)
|
||||
} catch (e) {
|
||||
alert("Failed seeking! " + e)
|
||||
}
|
||||
is_seeking = false
|
||||
})
|
||||
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.on_duration_formatted((time) => {
|
||||
document.querySelector("#duration")!.innerHTML = time
|
||||
document.querySelector("#seek")!.max = "" + music_player.current_song_duration
|
||||
})
|
||||
|
||||
music_player.on_time_tick_formatted((time) => {
|
||||
document.querySelector("#current")!.innerHTML = time
|
||||
})
|
||||
|
||||
music_player.on_time_tick((time) => {
|
||||
if (is_seeking) return
|
||||
document.querySelector("#seek")!.value = "" + time
|
||||
})
|
||||
|
||||
music_player.try_new_song(encodeURI("http://" + window.location.host + "/nuphory - NVISION (EXTENDED MIX).ogg"))
|
||||
|
|
|
@ -5,13 +5,12 @@ A simple, safe AudioContext web music player.
|
|||
|
||||
##### Full demo at [github link](https://github.com/euterpe-js/euterpe-source/tree/master/packages/player-web-test)
|
||||
|
||||
Euterpe player is very Rust inspired, meaning there's always a safer function. `play()`,`play_async()` and `try_play_async()` for example. The goal is to provide the developer with knowledge of what happened when they ran the function, so they can decide what to do if it failed or succeeded.
|
||||
```js
|
||||
All we need to do is import the player builder and build it
|
||||
```ts
|
||||
import { MusicPlayerBuilder } from "@euterpe/player";
|
||||
const audio_el = document.querySelector("#audio")
|
||||
|
||||
const music_player_builder = MusicPlayerBuilder(audio_el)
|
||||
music_player_builder.start()
|
||||
|
||||
// Builder allows for attaching custom nodes if necessary, eg.
|
||||
const panning_node = music_player_builder.add_stereo_panner_node()
|
||||
|
@ -22,24 +21,24 @@ waves_shaper_node.oversample = '4x'
|
|||
const music_player = music_player_builder.build()
|
||||
|
||||
//Next we add a song URL to the Audio Element,
|
||||
music_player.try_new_song_async(encodeURI("my_song.ogg"))
|
||||
.then(() => {
|
||||
//and wait for the user input to resume the AudioContext
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
music_player.play_async()
|
||||
.then(
|
||||
//Easily follow up with what to do next
|
||||
() => { console.log("Playing!") },
|
||||
(e) => alert("Failed to play, " + e)
|
||||
)
|
||||
})
|
||||
})
|
||||
music_player.try_new_song(encodeURI("my_song.ogg"))
|
||||
//and wait for the user input to resume the AudioContext
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
music_player.try_play()
|
||||
.then(
|
||||
//Easily follow up with what to do next
|
||||
() => { console.log("Playing!") },
|
||||
(e) => alert("Failed to play, " + e)
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
It's quite easy to give user the control in UI
|
||||
```js
|
||||
|
||||
```ts
|
||||
// Play when user clicks a <button></button>
|
||||
document.querySelector("#play-button")?.addEventListener("click", () => {
|
||||
music_player.play_async()
|
||||
music_player.try_play()
|
||||
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
|
||||
})
|
||||
// Mute when user clicks another <button></button>
|
||||
|
@ -53,7 +52,8 @@ 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
|
||||
|
||||
```ts
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.on_duration_formatted((time) => {
|
||||
//time == "4:53, "15:59", "1756:15:59"...
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@euterpe.js/player",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"type": "module",
|
||||
"description": "A simple, safe AudioContext web music player",
|
||||
"main": "./src/index.js",
|
||||
|
@ -29,4 +29,4 @@
|
|||
"require": "./src/lib/player.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ export class MusicPlayer {
|
|||
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.
|
||||
* Safer seek. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed.
|
||||
* will not resume playback
|
||||
* @throws if "Can't seek - Audiocontext is not running"
|
||||
*/
|
||||
|
@ -131,14 +131,14 @@ export class MusicPlayer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Unsafe, throws error if failed. Use try_seek_async or seek_async unless you don't care about the result.
|
||||
* Unsafe, throws error if failed. Use try_seek or seek unless you don't care about the result.
|
||||
*/
|
||||
seek(new_time: number) {
|
||||
this.audio_element.currentTime = new_time
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Safer play_toggle. Normal play_toggle will try to start the player even if the track hasn't started yet, or was previously suspended/closed
|
||||
* @throws Error if playback failed
|
||||
*/
|
||||
async try_play_toggle() {
|
||||
|
@ -196,7 +196,7 @@ export class MusicPlayer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Unsafe, can just fail. Use play_async or try_play_async unless you don't care about the result.
|
||||
* Unsafe, can just fail. Use play or try_play unless you don't care about the result.
|
||||
*/
|
||||
play() {
|
||||
if (this.is_playing) return
|
||||
|
@ -214,7 +214,7 @@ export class MusicPlayer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Will only load metadata of the upcoming song. Need to call try_play_async() afterwards to start the playback
|
||||
* Will only load metadata of the upcoming song and change audio dom elements url. Need to call try_play() afterwards to start the playback
|
||||
* @throws Error if adding element throwed Error or Stalled
|
||||
*/
|
||||
async try_new_song(path: string) {
|
||||
|
@ -255,16 +255,17 @@ export class MusicPlayer {
|
|||
)
|
||||
|
||||
//once aborted, try to set current_song_duration
|
||||
controller.signal.addEventListener("abort", (r) => {
|
||||
controller.signal.addEventListener("abort", () => {
|
||||
this.current_song_duration = this.audio_element.duration
|
||||
if (typeof controller.signal.reason == "string") reject(new Error(controller.signal.reason))
|
||||
resolve()
|
||||
})
|
||||
|
||||
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
|
||||
* Won't tell if you if the song actually got loaded or if it failed. For a safer version use try_new_song() unless you don't care about the result
|
||||
*/
|
||||
new_song(path: string) {
|
||||
this.audio_element.src = this.current_song_path = path
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { MusicPlayerBuilder } from "@euterpe.js/player"
|
||||
import { fft_data } from "./waveform_data"
|
||||
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType, WaveformOrientation, WaveformShape } from "@euterpe.js/visualizer"
|
||||
|
||||
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
||||
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
||||
const trapnation_analyser_node = music_player_builder.add_analyser()
|
||||
|
@ -12,6 +13,7 @@ const waveform_canvas = document.querySelector("#waveform-canvas") as SVGSVGElem
|
|||
const seek_element = document.querySelector("#seek") as HTMLInputElement
|
||||
const duration_element = document.querySelector("#duration") as HTMLElement
|
||||
const current_time_element = document.querySelector("#current") as HTMLElement
|
||||
|
||||
/**
|
||||
* Create the Audio Visualizer
|
||||
*/
|
||||
|
@ -26,28 +28,32 @@ const trapnation_visual_builder = new AudioVisualBuilder(trapnation_analyser_nod
|
|||
fft_offset_i: 150
|
||||
})
|
||||
.set_fft_time_smoothing(0.6)
|
||||
//If not using typescript enums, CatmullRom = number 2
|
||||
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
||||
|
||||
const trapnation_visual = trapnation_visual_builder.build(ShapeType.Circle, false)
|
||||
|
||||
const bar_visual_builder = new AudioVisualBuilder(bar_analyser_node, document.querySelector("#bar-canvas") as SVGSVGElement)
|
||||
.set_fft_size(4096)
|
||||
.set_fft_data_tresholds({
|
||||
point_count_i: 50,
|
||||
fft_multiplier_i: 2,
|
||||
fft_offset_i: -100
|
||||
point_count_i: 20,
|
||||
fft_multiplier_i: 3,
|
||||
fft_offset_i: -130
|
||||
})
|
||||
.set_fft_time_smoothing(0.8)
|
||||
.set_fft_time_smoothing(0.5)
|
||||
.set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular)
|
||||
|
||||
//currently broken, duno why...
|
||||
const bar_visual = bar_visual_builder.build(ShapeType.Line, false)
|
||||
|
||||
const waveform_visual_builder = new AudioVisualBuilder(bar_analyser_node, waveform_canvas)
|
||||
.set_fft_data_tresholds({
|
||||
point_count_i: 100,
|
||||
fft_multiplier_i: 1,
|
||||
fft_offset_i: -80
|
||||
fft_offset_i: -50
|
||||
})
|
||||
.set_fft_time_smoothing(0.8)
|
||||
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
||||
|
||||
const waveform_visual = waveform_visual_builder.build(ShapeType.Waveform, true, {
|
||||
fft_data: new Float32Array(fft_data.fft_data),
|
||||
orientation: WaveformOrientation.Horizontal,
|
||||
|
@ -98,9 +104,11 @@ waveform_canvas.appendChild(seek_clip_use)
|
|||
seek_clip_path.appendChild(seek_clip_rect)
|
||||
time_clip_path.appendChild(time_clip_rect)
|
||||
*/
|
||||
|
||||
function convert_range(value: number, r1: number[], r2: number[]) {
|
||||
return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0]
|
||||
}
|
||||
|
||||
waveform_canvas.addEventListener("mousemove", (e) => {
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
|
@ -108,74 +116,86 @@ waveform_canvas.addEventListener("mousemove", (e) => {
|
|||
const polygon = `polygon(0 0, ${resX}px 0, ${resX}px 100%, 0 100%)`
|
||||
document.documentElement.style.setProperty("--clip-seek-path", polygon)
|
||||
})
|
||||
|
||||
waveform_canvas.addEventListener("mouseleave", (e) => {
|
||||
const polygon = `polygon(0 0, 0 0, 0 100%, 0 100%)`
|
||||
document.documentElement.style.setProperty("--clip-seek-path", polygon)
|
||||
})
|
||||
|
||||
/*
|
||||
* The player part
|
||||
*/
|
||||
music_player.try_new_song_async(encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg")).then(
|
||||
() => {
|
||||
let is_seeking = false
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
music_player.play_async().then(
|
||||
() => {
|
||||
console.log("Playing!")
|
||||
},
|
||||
(e) => alert("Failed to play, " + e)
|
||||
)
|
||||
})
|
||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||
music_player.pause()
|
||||
})
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
music_player.mute()
|
||||
})
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
music_player.unmute()
|
||||
})
|
||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||
music_player.mute_toggle()
|
||||
})
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
music_player.play_toggle_async().then(
|
||||
(s) => console.log("toggled play/pause"),
|
||||
(e) => alert("failed to toggle pause/play!" + e)
|
||||
)
|
||||
})
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
music_player.change_volume(e.target?.valueAsNumber)
|
||||
})
|
||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||
is_seeking = true
|
||||
})
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
music_player.try_seek_async(e.target?.valueAsNumber).then(
|
||||
() => {
|
||||
console.log("seeked to " + e.target?.valueAsNumber)
|
||||
},
|
||||
() => {
|
||||
alert("Failed seeking! " + e)
|
||||
}
|
||||
)
|
||||
is_seeking = false
|
||||
})
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.on_duration_formatted((time) => {
|
||||
duration_element.innerHTML = time
|
||||
seek_element.max = "" + music_player.current_song_duration
|
||||
})
|
||||
music_player.on_time_tick_formatted((time) => {
|
||||
current_time_element.innerHTML = time
|
||||
})
|
||||
music_player.on_time_tick((time) => {
|
||||
if (is_seeking) return
|
||||
seek_element.value = "" + time
|
||||
const x = `${(time / music_player.current_song_duration) * 100}%`
|
||||
const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)`
|
||||
document.documentElement.style.setProperty("--clip-time-path", polygon)
|
||||
})
|
||||
},
|
||||
(e) => console.log(e)
|
||||
)
|
||||
|
||||
let is_seeking = false
|
||||
|
||||
document.querySelector("#play")?.addEventListener("click", () => {
|
||||
music_player.try_play().then(
|
||||
() => {
|
||||
console.log("Playing!")
|
||||
},
|
||||
(e) => alert("Failed to play, " + e)
|
||||
)
|
||||
})
|
||||
|
||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||
music_player.pause()
|
||||
})
|
||||
|
||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||
music_player.mute()
|
||||
})
|
||||
|
||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||
music_player.unmute()
|
||||
})
|
||||
|
||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||
music_player.mute_toggle()
|
||||
})
|
||||
|
||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||
music_player.try_play_toggle().then(
|
||||
(s) => console.log("toggled play/pause"),
|
||||
(e) => alert("failed to toggle pause/play!" + e)
|
||||
)
|
||||
})
|
||||
|
||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||
music_player.change_volume(e.target?.valueAsNumber)
|
||||
})
|
||||
|
||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||
is_seeking = true
|
||||
})
|
||||
|
||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||
music_player.try_seek(e.target?.valueAsNumber).then(
|
||||
() => {
|
||||
console.log("seeked to " + e.target?.valueAsNumber)
|
||||
},
|
||||
() => {
|
||||
alert("Failed seeking! " + e)
|
||||
}
|
||||
)
|
||||
is_seeking = false
|
||||
})
|
||||
|
||||
// Subscriptions to AudioContext changes, eg. time..
|
||||
music_player.on_duration_formatted((time) => {
|
||||
duration_element.innerHTML = time
|
||||
seek_element.max = "" + music_player.current_song_duration
|
||||
})
|
||||
|
||||
music_player.on_time_tick_formatted((time) => {
|
||||
current_time_element.innerHTML = time
|
||||
})
|
||||
|
||||
music_player.on_time_tick((time) => {
|
||||
if (is_seeking) return
|
||||
seek_element.value = "" + time
|
||||
const x = `${(time / music_player.current_song_duration) * 100}%`
|
||||
const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)`
|
||||
document.documentElement.style.setProperty("--clip-time-path", polygon)
|
||||
})
|
||||
|
||||
music_player.try_new_song(encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg"))
|
||||
|
|
|
@ -31,9 +31,9 @@ body {
|
|||
}
|
||||
.waveform-time {
|
||||
clip-path: var(--clip-time-path);
|
||||
fill: rgba(50,145,200,0.3);
|
||||
fill: rgba(0,255,50,0.5);
|
||||
}
|
||||
.waveform-seek {
|
||||
clip-path: var(--clip-seek-path);
|
||||
fill: rgba(50,145,200,0.3);
|
||||
}
|
||||
fill: rgba(0,50,255,0.5);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//To see how to get these, check the preprocessor repo!
|
||||
export const fft_data = {
|
||||
fft_data: [
|
||||
-106.24, -99.73, -100.98, -101.34, -107.01, -92.38, -84.85, -90.28, -93.68, -95.02, -97.16, -96.32, -99.23, -103.13, -85.57, -98.17, -103.27,
|
||||
|
|
|
@ -5,7 +5,8 @@ Euterpe Visualizer is a unique audio visual library for the web. Using SVG eleme
|
|||
### How to use
|
||||
|
||||
This library relies on AudioContext API, especially on AnalyserNode. To use the Visualizer without our player, first we need to create an AudioContext, from which we can create and connect an AnalyserNode:
|
||||
```js
|
||||
|
||||
```ts
|
||||
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer"
|
||||
// Refrence to an <audio id="audio"></audio> element inside your HTML
|
||||
const audio_element = document.querySelector("#audio") as HTMLAudioElement
|
||||
|
@ -15,8 +16,10 @@ const track = audio_context.createMediaElementSource(audio_element)
|
|||
const analyzer = audio_context.createAnalyser()
|
||||
track.connect(analyzer).connect(audio_context.destination)
|
||||
```
|
||||
|
||||
Now that the AudioContext is ready, we start constructing our Visualizer
|
||||
```js
|
||||
|
||||
```ts
|
||||
// For more options during the building process, I recommend reading the docs
|
||||
/* Refrence an
|
||||
* "<svg id="canvas" viewBox="0 0 500 500" preserveAspectRatio="none" ></svg>"
|
||||
|
@ -25,11 +28,14 @@ Now that the AudioContext is ready, we start constructing our Visualizer
|
|||
const visualizer = new AudioVisualBuilder(analyser_node, document.querySelector("#canvas") as SVGSVGElement)
|
||||
.build(ShapeType.Circle)
|
||||
```
|
||||
|
||||
And it's ready! Once the audio context starts playing, the visualizer should start generating the path inside our SVG element.
|
||||
```js
|
||||
|
||||
```ts
|
||||
visualizer.draw()
|
||||
audio_element.play()
|
||||
```
|
||||
|
||||
This is a minimal setup, and more options during the build process are recommended. Especially the `.set_fft_data_tresholds()` method, as each setting affects another. There's really no scientific method to this for now unfortunately, so some trial and error will be necessary.
|
||||
|
||||
#### How to use with Euterpe Player
|
||||
|
@ -37,7 +43,8 @@ This is a minimal setup, and more options during the build process are recommend
|
|||
##### Full demo on how to use together with Euterpe player at [github](https://github.com/euterpe-js/euterpe-source/tree/master/packages/visualizer-web-test).
|
||||
|
||||
First we need to create our [Euterpe Player](https://www.npmjs.com/package/@euterpe.js/player)
|
||||
```js
|
||||
|
||||
```ts
|
||||
import { MusicPlayerBuilder } from "@euterpe/player";
|
||||
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer"
|
||||
|
||||
|
@ -49,8 +56,9 @@ const analyzer_node = music_player_builder.add_analyser()
|
|||
const music_player = music_player_builder.build()
|
||||
|
||||
```
|
||||
|
||||
Now that the AudioContext is ready, we start constructing our Visualizer
|
||||
```js
|
||||
```ts
|
||||
// For more options during the building process, I recommend reading the docs
|
||||
/* Refrence an
|
||||
* "<svg id="canvas" viewBox="0 0 500 500" preserveAspectRatio="none" ></svg>"
|
||||
|
@ -62,19 +70,20 @@ const visual_builder = new AudioVisualBuilder(analyzer_node, document.querySelec
|
|||
//Tells the Visualiser how to parse data which mutates our initial shape
|
||||
.set_fft_data_tresholds({ to_fft_range_i: 3, point_count_i: 40, fft_multiplier_i: 1.5, fft_offset_i: 150 })
|
||||
.set_fft_time_smoothing(0.6)
|
||||
//If not using typescript enums, CatmullRom = number 2
|
||||
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
||||
|
||||
const visualizer = visual_builder.build(ShapeType.Circle)
|
||||
```
|
||||
|
||||
And it's ready! Once the audio context starts playing, the visualizer should start generating the path inside our SVG element.
|
||||
```js
|
||||
|
||||
```ts
|
||||
visualizer.draw()
|
||||
|
||||
music_player.try_new_song_async(url)
|
||||
.then( music_player.play_async()
|
||||
music_player.try_new_song(url)
|
||||
.then( music_player.try_play()
|
||||
.then(
|
||||
console.log("It's working!")
|
||||
)
|
||||
)
|
||||
```
|
||||
```
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@euterpe.js/visualizer",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"type": "module",
|
||||
"description": "Music visualizer based on SVG and AudioContext",
|
||||
"main": "./src/index.js",
|
||||
|
@ -27,4 +27,4 @@
|
|||
"require": "./src/lib/visualizer.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ export enum ShapeType {
|
|||
Line,
|
||||
Waveform
|
||||
/*To be Implmeneted
|
||||
Custom,
|
||||
FullSongWaveForm
|
||||
*/
|
||||
Custom,
|
||||
FullSongWaveForm
|
||||
*/
|
||||
}
|
||||
export enum WaveformOrientation {
|
||||
Vertical,
|
||||
|
@ -211,8 +211,8 @@ export class AudioVisual {
|
|||
this.#canvas_height / 2
|
||||
})
|
||||
/* TODO: IMPLEMENT SCALING TO BEAT
|
||||
this.injectingHTMLElement.parentElement.style.transform = `scale(${(100 + Math.max((frequencyData[2] * 2 + 130) / 5, 1)) / 100})`
|
||||
*/
|
||||
this.injectingHTMLElement.parentElement.style.transform = `scale(${(100 + Math.max((frequencyData[2] * 2 + 130) / 5, 1)) / 100})`
|
||||
*/
|
||||
}
|
||||
|
||||
break
|
||||
|
@ -296,10 +296,7 @@ export class AudioVisual {
|
|||
for (let i = 0; i < arr.length; i++) {
|
||||
path += `L ${arr[i].x.toFixed(2)},${arr[i].y.toFixed(2)} `
|
||||
}
|
||||
if (this.#shape.shape_type == ShapeType.Line) {
|
||||
path += `L ${this.#canvas_width} ${this.#canvas_height} `
|
||||
//path += `L ${canvas_width} ${canvas_height} `
|
||||
}
|
||||
path += `L ${this.#canvas_width} ${this.#canvas_height} `
|
||||
break
|
||||
}
|
||||
case ShapeType.Circle: {
|
||||
|
@ -339,10 +336,10 @@ export class AudioVisual {
|
|||
|
||||
case SmoothingAlgorythm.BezierWeighted: {
|
||||
/*THIS IS VERY MUCH BROKEN ATM :(
|
||||
for (let i = 2; i < arr.length; i++) {
|
||||
const end = [arr.x[i], arr.y[i]] // the current point is the end of this segment of the curve
|
||||
path += `C ${startControl[0]} ${startControl[1]} ${endControl[0]} ${endControl[1]} ${end[0]} ${end[1]}`
|
||||
}*/
|
||||
for (let i = 2; i < arr.length; i++) {
|
||||
const end = [arr.x[i], arr.y[i]] // the current point is the end of this segment of the curve
|
||||
path += `C ${startControl[0]} ${startControl[1]} ${endControl[0]} ${endControl[1]} ${end[0]} ${end[1]}`
|
||||
}*/
|
||||
console.error("BezierWeighted not implemented yet...")
|
||||
break
|
||||
}
|
||||
|
@ -488,19 +485,19 @@ export class AudioVisualBuilder {
|
|||
return this
|
||||
}
|
||||
/**
|
||||
* Defines what the svg d attribute path command will use.
|
||||
*
|
||||
* `Linear` - Uses no interpolation between points, jagged but very fast
|
||||
*
|
||||
* `BezierPerpendicular` - Sets the Cubic Bézier anchors perpendicular to the point. Great for Line shapes with no curves.
|
||||
*
|
||||
* `CatmullRom` - Uses Centripetal Catmull–Rom spline under the hood, then translates them to Cubic Bézier points. Best quality, worst performance.
|
||||
*
|
||||
* `BezierWeighted` - DO NOT USE! It's broken at the moment :/. Sets the Cubic Bézier anchors halfway between the next and previous point. Better than Linear on Circular shapes and doesn't have the "invards bulding" side effect of Catmull Rom
|
||||
* Defines what the svg d attribute path command will use.
|
||||
*
|
||||
* `Linear` - Uses no interpolation between points, jagged but very fast
|
||||
*
|
||||
* `BezierPerpendicular` - Sets the Cubic Bézier anchors perpendicular to the point. Great for Line shapes with no curves.
|
||||
*
|
||||
* `CatmullRom` - Uses Centripetal Catmull–Rom spline under the hood, then translates them to Cubic Bézier points. Best quality, worst performance.
|
||||
*
|
||||
* `BezierWeighted` - DO NOT USE! It's broken at the moment :/. Sets the Cubic Bézier anchors halfway between the next and previous point. Better than Linear on Circular shapes and doesn't have the "invards bulding" side effect of Catmull Rom
|
||||
|
||||
* @param algorythm Linear = 0; BezierPerpendicular = 1; CatmullRom = 2; BezierWeighted = 3;
|
||||
* @returns
|
||||
*/
|
||||
* @param algorythm Linear = 0; BezierPerpendicular = 1; CatmullRom = 2; BezierWeighted = 3;
|
||||
* @returns
|
||||
*/
|
||||
set_smoothing_algorythm(algorythm: SmoothingAlgorythm) {
|
||||
this.#smoothing_algorythm = algorythm
|
||||
return this
|
||||
|
|
Loading…
Reference in a new issue