Compare commits
4 commits
linux-prep
...
master
Author | SHA1 | Date | |
---|---|---|---|
55ae4686fa | |||
eb4e4f91d5 | |||
cd97233bce | |||
ed2d865f2d |
36 changed files with 3043 additions and 2655 deletions
|
@ -8,5 +8,6 @@
|
||||||
"endOfLine": "lf",
|
"endOfLine": "lf",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"bracketSameLine": false,
|
"bracketSameLine": false,
|
||||||
"arrowParens": "always"
|
"arrowParens": "always",
|
||||||
|
"printWidth": 150
|
||||||
}
|
}
|
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,6 +3,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"format": "prettier --write '**/*.{js,ts,css,html,json,mjs}'",
|
||||||
"publish-player": "nx build player && cd dist/packages/player && npm publish --access=public",
|
"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-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",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Artist, Collection, DB, Ref, RefTo, Song } from "@euterpe.js/music-library";
|
import { Artist, Collection, DB, Ref, RefTo, Song } from "@euterpe.js/music-library"
|
||||||
export { DJSong, DJDB }
|
export { DJSong, DJDB }
|
||||||
type ID = number
|
type ID = number
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class DJSong extends Song {
|
||||||
this.audio_buffer = audio_buffer
|
this.audio_buffer = audio_buffer
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(new Error("Failed to preprocess DJSong. " + e))
|
console.error(new Error("Failed to preprocess DJSong. " + e))
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class DJSong extends Song {
|
||||||
class DJDB extends DB {
|
class DJDB extends DB {
|
||||||
dj_add(dj_songs: DJSong[]): void {
|
dj_add(dj_songs: DJSong[]): void {
|
||||||
let inputs
|
let inputs
|
||||||
typeof dj_songs[Symbol.iterator] == "function" ? inputs = dj_songs : inputs = [dj_songs]
|
typeof dj_songs[Symbol.iterator] == "function" ? (inputs = dj_songs) : (inputs = [dj_songs])
|
||||||
for (const input of inputs) {
|
for (const input of inputs) {
|
||||||
if (input instanceof DJSong) {
|
if (input instanceof DJSong) {
|
||||||
const song = input as DJSong
|
const song = input as DJSong
|
||||||
|
@ -70,5 +70,4 @@ class DJDB extends DB {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Euterpe } from "@euterpe.js/euterpe";
|
import { Euterpe } from "@euterpe.js/euterpe"
|
||||||
import { Song } from "@euterpe.js/music-library";
|
import { Song } from "@euterpe.js/music-library"
|
||||||
import { MusicPlayer } from "@euterpe.js/player";
|
import { MusicPlayer } from "@euterpe.js/player"
|
||||||
export { DJ }
|
export { DJ }
|
||||||
/**
|
/**
|
||||||
* To change volume of a track, use track[i].gain.gain, to change master volume, use euterpe/music players volume.
|
* To change volume of a track, use track[i].gain.gain, to change master volume, use euterpe/music players volume.
|
||||||
|
@ -11,15 +11,13 @@ class DJ {
|
||||||
/**in ms */
|
/**in ms */
|
||||||
beat_duration?: number
|
beat_duration?: number
|
||||||
beat = { current: 0, max: 4, next_bar_in: 4 }
|
beat = { current: 0, max: 4, next_bar_in: 4 }
|
||||||
on_beat?: (beat: { current: number, max: number, next_bar_in: number }) => void
|
on_beat?: (beat: { current: number; max: number; next_bar_in: number }) => void
|
||||||
constructor(public player: Euterpe | MusicPlayer, public master_bpm: number | 120) {
|
constructor(public player: Euterpe | MusicPlayer, public master_bpm: number | 120) {
|
||||||
this.beat_duration = 60 / master_bpm
|
this.beat_duration = 60 / master_bpm
|
||||||
this.#emit_beats()
|
this.#emit_beats()
|
||||||
}
|
}
|
||||||
#emit_beats() {
|
#emit_beats() {
|
||||||
this.beat.current >= 4 ?
|
this.beat.current >= 4 ? (this.beat.current++, this.beat.next_bar_in--) : ((this.beat.current = 0), (this.beat.next_bar_in = this.beat.max))
|
||||||
(this.beat.current++, this.beat.next_bar_in--) :
|
|
||||||
(this.beat.current = 0, this.beat.next_bar_in = this.beat.max)
|
|
||||||
|
|
||||||
if (this.on_beat) this.on_beat(this.beat)
|
if (this.on_beat) this.on_beat(this.beat)
|
||||||
//This makes it break if BPM >= 300!!!!
|
//This makes it break if BPM >= 300!!!!
|
||||||
|
@ -38,7 +36,10 @@ class DJ {
|
||||||
*/
|
*/
|
||||||
async try_queue_track(track_i: number, delay: number) {
|
async try_queue_track(track_i: number, delay: number) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.tracks[track_i].try_start(delay).then(() => resolve(this), (e) => reject(e))
|
this.tracks[track_i].try_start(delay).then(
|
||||||
|
() => resolve(this),
|
||||||
|
(e) => reject(e)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -47,7 +48,10 @@ class DJ {
|
||||||
*/
|
*/
|
||||||
async try_load_song_into_track(track_i: number, song: Song) {
|
async try_load_song_into_track(track_i: number, song: Song) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.tracks[track_i].change_song(song).then(() => resolve(this), (e) => reject(e))
|
this.tracks[track_i].change_song(song).then(
|
||||||
|
() => resolve(this),
|
||||||
|
(e) => reject(e)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -57,7 +61,10 @@ class DJ {
|
||||||
*/
|
*/
|
||||||
async try_start_track(track_i: number) {
|
async try_start_track(track_i: number) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.tracks[track_i].try_start().then(() => resolve(this), (e) => reject(e))
|
this.tracks[track_i].try_start().then(
|
||||||
|
() => resolve(this),
|
||||||
|
(e) => reject(e)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -68,7 +75,6 @@ class DJ {
|
||||||
this.master_bpm = new_master_bpm
|
this.master_bpm = new_master_bpm
|
||||||
this.beat_duration = 60 / this.master_bpm
|
this.beat_duration = 60 / this.master_bpm
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Track {
|
class Track {
|
||||||
|
@ -86,13 +92,13 @@ class Track {
|
||||||
async #prepare() {
|
async #prepare() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.current_song) reject(new Error("No current song"))
|
if (!this.current_song) reject(new Error("No current song"))
|
||||||
fetch(this.current_song!.url)
|
fetch(this.current_song!.url).then(
|
||||||
.then(
|
|
||||||
async (file) => {
|
async (file) => {
|
||||||
this.audio_buffer = await this.audio_context.decodeAudioData(await file.arrayBuffer())
|
this.audio_buffer = await this.audio_context.decodeAudioData(await file.arrayBuffer())
|
||||||
resolve(this)
|
resolve(this)
|
||||||
},
|
},
|
||||||
(reason) => reject(reason))
|
(reason) => reject(reason)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
#connect() {
|
#connect() {
|
||||||
|
@ -106,10 +112,13 @@ class Track {
|
||||||
async change_song(new_song: Song) {
|
async change_song(new_song: Song) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.current_song = new_song
|
this.current_song = new_song
|
||||||
this.#prepare().then(() => {
|
this.#prepare().then(
|
||||||
|
() => {
|
||||||
this.#connect()
|
this.#connect()
|
||||||
resolve(this)
|
resolve(this)
|
||||||
}, (reason) => reject(reason))
|
},
|
||||||
|
(reason) => reject(reason)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
import { MusicPlayerBuilder } from "@euterpe.js/player";
|
import { MusicPlayerBuilder } from "@euterpe.js/player"
|
||||||
|
|
|
@ -1,34 +1,32 @@
|
||||||
import { DB, Song, Artist, Ref, RefTo, Platforms } from "@euterpe.js/music-library"
|
import { DB, Song, Artist, Ref, RefTo, Platforms } from "@euterpe.js/music-library"
|
||||||
export const db = new DB
|
export const db = new DB()
|
||||||
|
|
||||||
db.add([
|
db.add([
|
||||||
//The IDs are added incrementally & are 0 based., so first artists ID added is 0, next 1 etc...
|
//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
|
//You can specify the ID manually if you want
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "Jamie xx",
|
name: "Jamie xx"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "janz",
|
name: "janz"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "Machinedrum",
|
name: "Machinedrum"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "Tanerélle",
|
name: "Tanerélle"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "Mono/Poly",
|
name: "Mono/Poly"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "IMANU",
|
name: "IMANU",
|
||||||
links: [
|
links: [[Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]]
|
||||||
[Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]
|
|
||||||
]
|
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "toe",
|
name: "toe",
|
||||||
id: 10
|
id: 10
|
||||||
}),
|
})
|
||||||
])
|
])
|
||||||
db.add([
|
db.add([
|
||||||
new Song({
|
new Song({
|
||||||
|
@ -59,4 +57,3 @@ db.add([
|
||||||
url: new URL("http://" + window.location.host + "/16.サニーボーイ・ラプソディ.ogg")
|
url: new URL("http://" + window.location.host + "/16.サニーボーイ・ラプソディ.ogg")
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { db } from "./db";
|
import { db } from "./db"
|
||||||
import { EuterpeBuilder } from "@euterpe.js/euterpe";
|
import { EuterpeBuilder } from "@euterpe.js/euterpe"
|
||||||
|
|
||||||
let is_seeking = false
|
let is_seeking = false
|
||||||
// document.addEventListener("click", start, { once: true })
|
// document.addEventListener("click", start, { once: true })
|
||||||
const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db)
|
const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db, {
|
||||||
.build()
|
auto_play_after_seek: true,
|
||||||
|
auto_play_after_changing_song: true,
|
||||||
|
use_only_pathname_url: true
|
||||||
|
}).build()
|
||||||
add_library_to_dom()
|
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) => {
|
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||||
try {
|
try {
|
||||||
euterpe.try_seek(e.target?.valueAsNumber)
|
euterpe.try_seek(e.target?.valueAsNumber)
|
||||||
|
@ -21,7 +20,7 @@ document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||||
is_seeking = false
|
is_seeking = false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscriptions to AudioContext changes, eg. time..
|
// Subscriptions to song and AudioContext changes, eg. time, name..
|
||||||
euterpe.on_duration_formatted((time) => {
|
euterpe.on_duration_formatted((time) => {
|
||||||
document.querySelector("#duration")!.innerHTML = time
|
document.querySelector("#duration")!.innerHTML = time
|
||||||
document.querySelector("#seek")!.max = "" + euterpe.current_song_duration
|
document.querySelector("#seek")!.max = "" + euterpe.current_song_duration
|
||||||
|
@ -30,6 +29,7 @@ euterpe.on_duration_formatted((time) => {
|
||||||
euterpe.on_time_tick_formatted((time) => {
|
euterpe.on_time_tick_formatted((time) => {
|
||||||
document.querySelector("#current")!.innerHTML = time
|
document.querySelector("#current")!.innerHTML = time
|
||||||
})
|
})
|
||||||
|
|
||||||
euterpe.on_time_tick((time) => {
|
euterpe.on_time_tick((time) => {
|
||||||
if (is_seeking) return
|
if (is_seeking) return
|
||||||
document.querySelector("#seek")!.value = "" + time
|
document.querySelector("#seek")!.value = "" + time
|
||||||
|
@ -37,41 +37,52 @@ euterpe.on_time_tick((time) => {
|
||||||
dev_history_update()
|
dev_history_update()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#previous")?.addEventListener("click", () => {
|
euterpe.on_song_change((_, song_name) => {
|
||||||
euterpe.try_previous_song_looping().then(() => {
|
document.querySelector("#text-playing")!.innerHTML = song_name
|
||||||
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
|
|
||||||
}, (e) => alert(e + "Failed to change song"))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//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", () => {
|
document.querySelector("#next")?.addEventListener("click", () => {
|
||||||
euterpe.try_next_song_looping().then(() => {
|
euterpe.try_next_song_looping().catch((e) => alert(e + "Failed to change song"))
|
||||||
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
|
|
||||||
}, (e) => alert(e + "Failed to change song"))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#play")?.addEventListener("click", () => {
|
document.querySelector("#play")?.addEventListener("click", () => {
|
||||||
euterpe.try_play().catch((e) => alert("Failed to play, " + e))
|
euterpe.try_play().catch((e) => alert("Failed to play, " + e))
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||||
euterpe.pause()
|
euterpe.pause()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||||
euterpe.mute()
|
euterpe.mute()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||||
euterpe.unmute()
|
euterpe.unmute()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||||
euterpe.mute_toggle()
|
euterpe.mute_toggle()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
||||||
euterpe.try_play_toggle().catch((e) => alert("failed to toggle pause/play!" + e))
|
euterpe.try_play_toggle().catch((e) => alert("failed to toggle pause/play!" + e))
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||||
euterpe.change_volume(e.target?.valueAsNumber)
|
euterpe.change_volume(e.target?.valueAsNumber)
|
||||||
})
|
})
|
||||||
|
|
||||||
//disables time updates so the time slider doesn't slip away from user
|
//disables time updates so the time slider doesn't slip away from user
|
||||||
document.querySelector("#seek")?.addEventListener("mousedown", () => {
|
document.querySelector("#seek")?.addEventListener("mousedown", () => {
|
||||||
is_seeking = true;
|
is_seeking = true
|
||||||
})
|
})
|
||||||
|
|
||||||
function add_library_to_dom() {
|
function add_library_to_dom() {
|
||||||
|
@ -103,7 +114,7 @@ function add_library_to_dom() {
|
||||||
function library_play(e: MouseEvent) {
|
function library_play(e: MouseEvent) {
|
||||||
const b = e.currentTarget as HTMLButtonElement
|
const b = e.currentTarget as HTMLButtonElement
|
||||||
euterpe.try_specific_song(parseInt(b.dataset["id"]!)).then(
|
euterpe.try_specific_song(parseInt(b.dataset["id"]!)).then(
|
||||||
() => document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song(),
|
() => (document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()),
|
||||||
(e) => alert(e)
|
(e) => alert(e)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,16 @@
|
||||||
|
|
||||||
Fully featured AudioContext music player for the web.
|
Fully featured AudioContext music player for the web.
|
||||||
|
|
||||||
|
## Euterpe in production:
|
||||||
|
- Hypertrance ( [site](https://hypertrance.eu/), [repository](https://github.com/nuphory/hypertrance.eu) )
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- "Local" library/Database for songs, collections etc.
|
- "Local" library/database for songs, collections, artists, waveforms, artist links and much more!
|
||||||
- Queue
|
- Queue and history
|
||||||
- History
|
|
||||||
- Easy way to create Vector based audio visuals
|
- 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.)
|
- 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:
|
## 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 Visualizer](https://www.npmjs.com/package/@euterpe.js/visualizer)
|
||||||
- [Euterpe Music Library](https://www.npmjs.com/package/@euterpe.js/music-library)
|
- [Euterpe Music Library](https://www.npmjs.com/package/@euterpe.js/music-library)
|
||||||
|
|
||||||
|
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.
|
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,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@euterpe.js/euterpe",
|
"name": "@euterpe.js/euterpe",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!",
|
"description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
|
|
|
@ -10,6 +10,7 @@ class Euterpe extends MusicPlayer {
|
||||||
current_song_id = 0
|
current_song_id = 0
|
||||||
queue: Song[] = []
|
queue: Song[] = []
|
||||||
played_history: Song[] = []
|
played_history: Song[] = []
|
||||||
|
#sub_list: Array<(song_id: number, song_name: string) => void> = []
|
||||||
constructor(
|
constructor(
|
||||||
public db: DB,
|
public db: DB,
|
||||||
audio_context: AudioContext,
|
audio_context: AudioContext,
|
||||||
|
@ -20,8 +21,34 @@ class Euterpe extends MusicPlayer {
|
||||||
current_song_path?: string,
|
current_song_path?: string,
|
||||||
private options?: BuilderOptions
|
private options?: BuilderOptions
|
||||||
) {
|
) {
|
||||||
|
|
||||||
super(audio_context, audio_element, track, gain, volume, current_song_path)
|
super(audio_context, audio_element, track, gain, volume, current_song_path)
|
||||||
|
|
||||||
|
audio_element.addEventListener("ended", async () => {
|
||||||
|
try {
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,8 +59,9 @@ class Euterpe extends MusicPlayer {
|
||||||
const next = this.db.songs.find((song) => song!.id == id)
|
const next = this.db.songs.find((song) => song!.id == id)
|
||||||
if (!next) throw new Error(`Song with id ${id} doesn't exist`)
|
if (!next) throw new Error(`Song with id ${id} doesn't exist`)
|
||||||
else {
|
else {
|
||||||
await this.try_new_song(next.url.pathname)
|
|
||||||
this.current_song = next
|
this.current_song = next
|
||||||
|
this.#emit_on_song_change()
|
||||||
|
await this.try_new_song(next.url.pathname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,19 +75,20 @@ class Euterpe extends MusicPlayer {
|
||||||
if (this.queue.length > 0) {
|
if (this.queue.length > 0) {
|
||||||
new_song = this.queue.shift()!
|
new_song = this.queue.shift()!
|
||||||
} else {
|
} else {
|
||||||
let id_i = this.db.songs.length;
|
let id_i = this.db.songs.length
|
||||||
while (this.db.songs[--id_i].id! > this.current_song_id);
|
while (this.db.songs[--id_i].id! > this.current_song_id);
|
||||||
const next_id = ++id_i;
|
const next_id = ++id_i
|
||||||
|
|
||||||
if (next_id == this.db.songs.length) throw new Error("Won't go past the last song")
|
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)!
|
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||||
}
|
}
|
||||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
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_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)
|
if (this.current_song) this.played_history.push(this.current_song)
|
||||||
this.current_song = new_song
|
this.current_song = new_song
|
||||||
this.current_song_id = new_song.id!
|
this.current_song_id = new_song.id!
|
||||||
|
this.#emit_on_song_change()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -71,7 +100,7 @@ class Euterpe extends MusicPlayer {
|
||||||
if (this.queue.length > 0) {
|
if (this.queue.length > 0) {
|
||||||
new_song = this.queue.shift()!
|
new_song = this.queue.shift()!
|
||||||
} else {
|
} else {
|
||||||
let id_i = this.db.songs.length;
|
let id_i = this.db.songs.length
|
||||||
while (this.db.songs[--id_i].id! > this.current_song_id);
|
while (this.db.songs[--id_i].id! > this.current_song_id);
|
||||||
let next_id = ++id_i
|
let next_id = ++id_i
|
||||||
|
|
||||||
|
@ -80,14 +109,15 @@ class Euterpe extends MusicPlayer {
|
||||||
}
|
}
|
||||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
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_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)
|
if (this.current_song) this.played_history.push(this.current_song)
|
||||||
this.current_song = new_song
|
this.current_song = new_song
|
||||||
this.current_song_id = new_song.id!
|
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
|
* If queue present, uses that, if not, relies on Song ID directly from DB
|
||||||
*/
|
*/
|
||||||
next_song_looping() {
|
next_song_looping() {
|
||||||
|
@ -95,23 +125,24 @@ class Euterpe extends MusicPlayer {
|
||||||
if (this.queue.length > 0) {
|
if (this.queue.length > 0) {
|
||||||
new_song = this.queue.shift()!
|
new_song = this.queue.shift()!
|
||||||
} else {
|
} else {
|
||||||
let id_i = this.db.songs.length;
|
let id_i = this.db.songs.length
|
||||||
while (this.db.songs[--id_i].id! > this.current_song_id);
|
while (this.db.songs[--id_i].id! > this.current_song_id);
|
||||||
let next_id = ++id_i;
|
let next_id = ++id_i
|
||||||
|
|
||||||
if (next_id == this.db.songs.length) next_id = this.db.songs[0].id!
|
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)!
|
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||||
}
|
}
|
||||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||||
this.new_song(url)
|
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)
|
if (this.current_song) this.played_history.push(this.current_song)
|
||||||
this.current_song = new_song
|
this.current_song = new_song
|
||||||
this.current_song_id = new_song.id!
|
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
|
* If queue present, uses that, if not, relies on Song ID directly from DB
|
||||||
*/
|
*/
|
||||||
next_song() {
|
next_song() {
|
||||||
|
@ -119,46 +150,49 @@ class Euterpe extends MusicPlayer {
|
||||||
if (this.queue.length > 0) {
|
if (this.queue.length > 0) {
|
||||||
new_song = this.queue.shift()!
|
new_song = this.queue.shift()!
|
||||||
} else {
|
} else {
|
||||||
let id_i = this.db.songs.length;
|
let id_i = this.db.songs.length
|
||||||
while (this.db.songs[--id_i].id! > this.current_song_id);
|
while (this.db.songs[--id_i].id! > this.current_song_id);
|
||||||
const next_id = ++id_i;
|
const next_id = ++id_i
|
||||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||||
}
|
}
|
||||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||||
this.new_song(url)
|
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)
|
if (this.current_song) this.played_history.push(this.current_song)
|
||||||
this.current_song = new_song
|
this.current_song = new_song
|
||||||
this.current_song_id = new_song.id!
|
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) {
|
async try_specific_song(new_song_id: number) {
|
||||||
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
|
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`)
|
if (!new_song) throw new Error(`No song with id "${new_song_id}" found`)
|
||||||
else {
|
else {
|
||||||
this.try_new_song(new_song.url.pathname)
|
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)
|
if (this.current_song) this.played_history.push(this.current_song)
|
||||||
this.current_song = new_song
|
this.current_song = new_song
|
||||||
this.current_song_id = new_song.id!
|
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) {
|
specific_song(new_song_id: number) {
|
||||||
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
|
const new_song = this.db.songs.find((song) => song.id! == new_song_id)
|
||||||
if (!new_song) return
|
if (!new_song) return
|
||||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||||
this.new_song(url)
|
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)
|
if (this.current_song) this.played_history.push(this.current_song)
|
||||||
this.current_song = new_song
|
this.current_song = new_song
|
||||||
this.current_song_id = new_song.id!
|
this.current_song_id = new_song.id!
|
||||||
|
this.#emit_on_song_change()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,19 +205,19 @@ class Euterpe extends MusicPlayer {
|
||||||
if (this.played_history.length > 0) {
|
if (this.played_history.length > 0) {
|
||||||
new_song = this.played_history.pop()!
|
new_song = this.played_history.pop()!
|
||||||
} else {
|
} else {
|
||||||
let id_i = 0;
|
let id_i = 0
|
||||||
while (this.db.songs[++id_i].id! < this.current_song_id);
|
while (this.db.songs[++id_i].id! < this.current_song_id);
|
||||||
const next_id = --id_i;
|
const next_id = --id_i
|
||||||
|
|
||||||
if (next_id == this.db.songs.length) throw new Error("Won't roll backwards to last song")
|
if (next_id == this.db.songs.length) throw new Error("Won't roll backwards to last song")
|
||||||
new_song = this.db.songs.find((song) => song.id == next_id)!
|
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||||
}
|
}
|
||||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
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_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 = new_song
|
||||||
this.current_song_id = new_song.id!
|
this.current_song_id = new_song.id!
|
||||||
|
this.#emit_on_song_change()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -195,19 +229,19 @@ class Euterpe extends MusicPlayer {
|
||||||
if (this.played_history.length > 0) {
|
if (this.played_history.length > 0) {
|
||||||
new_song = this.played_history.pop()!
|
new_song = this.played_history.pop()!
|
||||||
} else {
|
} else {
|
||||||
let id_i = -1;
|
let id_i = -1
|
||||||
while (this.db.songs[++id_i].id! < this.current_song_id);
|
while (this.db.songs[++id_i].id! < this.current_song_id);
|
||||||
let next_id = --id_i;
|
let next_id = --id_i
|
||||||
|
|
||||||
if (next_id == -1) next_id = this.db.songs[this.db.songs.length - 1].id!
|
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)!
|
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||||
}
|
}
|
||||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
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_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 = new_song
|
||||||
this.current_song_id = new_song.id!
|
this.current_song_id = new_song.id!
|
||||||
|
this.#emit_on_song_change()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -219,18 +253,18 @@ class Euterpe extends MusicPlayer {
|
||||||
if (this.played_history.length > 0) {
|
if (this.played_history.length > 0) {
|
||||||
new_song = this.played_history.pop()!
|
new_song = this.played_history.pop()!
|
||||||
} else {
|
} else {
|
||||||
let id_i = 0;
|
let id_i = 0
|
||||||
while (this.db.songs[++id_i].id! < this.current_song_id);
|
while (this.db.songs[++id_i].id! < this.current_song_id);
|
||||||
const next_id = -id_i;
|
const next_id = -id_i
|
||||||
|
|
||||||
if (next_id == this.db.songs.length) throw new Error("Won't go past the last song")
|
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)!
|
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||||
}
|
}
|
||||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||||
this.new_song(url)
|
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_id = new_song.id!
|
this.current_song_id = new_song.id!
|
||||||
|
this.#emit_on_song_change()
|
||||||
this.current_song = new_song
|
this.current_song = new_song
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,19 +277,19 @@ class Euterpe extends MusicPlayer {
|
||||||
if (this.played_history.length > 0) {
|
if (this.played_history.length > 0) {
|
||||||
new_song = this.played_history.pop()!
|
new_song = this.played_history.pop()!
|
||||||
} else {
|
} else {
|
||||||
let id_i = 0;
|
let id_i = 0
|
||||||
while (this.db.songs[++id_i].id! < this.current_song_id);
|
while (this.db.songs[++id_i].id! < this.current_song_id);
|
||||||
let next_id = -id_i;
|
let next_id = -id_i
|
||||||
|
|
||||||
if (next_id == this.db.songs.length) next_id = this.db.songs[this.db.songs.length].id!
|
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)!
|
new_song = this.db.songs.find((song) => song.id == next_id)!
|
||||||
}
|
}
|
||||||
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
|
||||||
this.new_song(url)
|
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_id = new_song.id!
|
this.current_song_id = new_song.id!
|
||||||
this.current_song = new_song
|
this.current_song = new_song
|
||||||
|
this.#emit_on_song_change()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -263,7 +297,6 @@ class Euterpe extends MusicPlayer {
|
||||||
* @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix)
|
* @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix)
|
||||||
*/
|
*/
|
||||||
format_current_song(id = this.current_song?.id) {
|
format_current_song(id = this.current_song?.id) {
|
||||||
|
|
||||||
const curr_song = this.db.songs.find((song) => song.id == id)
|
const curr_song = this.db.songs.find((song) => song.id == id)
|
||||||
if (!curr_song) {
|
if (!curr_song) {
|
||||||
return "ID - ID"
|
return "ID - ID"
|
||||||
|
@ -345,9 +378,20 @@ class Euterpe extends MusicPlayer {
|
||||||
return this.queue.splice(i, 1)
|
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 = {
|
type BuilderOptions = {
|
||||||
use_only_pathname_url?: boolean
|
use_only_pathname_url?: boolean
|
||||||
|
auto_play_after_seek?: boolean
|
||||||
|
auto_play_after_changing_song?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class EuterpeBuilder {
|
class EuterpeBuilder {
|
||||||
|
@ -355,7 +399,7 @@ class EuterpeBuilder {
|
||||||
#gain: GainNode
|
#gain: GainNode
|
||||||
#track: MediaElementAudioSourceNode
|
#track: MediaElementAudioSourceNode
|
||||||
#volume = 1
|
#volume = 1
|
||||||
#prev_node: any;
|
#prev_node: any
|
||||||
|
|
||||||
#is_gain_connected = false
|
#is_gain_connected = false
|
||||||
/**
|
/**
|
||||||
|
@ -366,7 +410,7 @@ class EuterpeBuilder {
|
||||||
constructor(private audio_element: HTMLAudioElement, private db: DB, private options?: BuilderOptions) {
|
constructor(private audio_element: HTMLAudioElement, private db: DB, private options?: BuilderOptions) {
|
||||||
if (audio_element === undefined) throw Error("audio_element was undefined")
|
if (audio_element === undefined) throw Error("audio_element was undefined")
|
||||||
// ↓ For old browsers
|
// ↓ For old browsers
|
||||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
const AudioContext = window.AudioContext || window.webkitAudioContext
|
||||||
this.#audio_context = new AudioContext()
|
this.#audio_context = new AudioContext()
|
||||||
this.#track = this.#audio_context.createMediaElementSource(audio_element)
|
this.#track = this.#audio_context.createMediaElementSource(audio_element)
|
||||||
this.#gain = this.#audio_context.createGain()
|
this.#gain = this.#audio_context.createGain()
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
import { DB, Artist, Song, RefTo, Ref, Platforms } from "@euterpe.js/music-library";
|
import { DB, Artist, Song, RefTo, Ref, Platforms } from "@euterpe.js/music-library"
|
||||||
export const db = new DB
|
export const db = new DB()
|
||||||
|
|
||||||
db.add([
|
db.add([
|
||||||
//The IDs are added incrementally & are 0 based., so first artists ID added is 0, next 1 etc...
|
//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
|
//You can specify the ID manually if you want
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "Jamie xx",
|
name: "Jamie xx"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "janz",
|
name: "janz"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "Machinedrum",
|
name: "Machinedrum"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "Tanerélle",
|
name: "Tanerélle"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "Mono/Poly",
|
name: "Mono/Poly"
|
||||||
}),
|
}),
|
||||||
new Artist({
|
new Artist({
|
||||||
name: "IMANU",
|
name: "IMANU",
|
||||||
links: [
|
links: [[Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]]
|
||||||
[Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]
|
})
|
||||||
]
|
])
|
||||||
})])
|
|
||||||
db.add([
|
db.add([
|
||||||
new Song({
|
new Song({
|
||||||
//Refrences are constructed as such. This allows to get to the artist from either collection or song
|
//Refrences are constructed as such. This allows to get to the artist from either collection or song
|
||||||
|
|
|
@ -1,83 +1,118 @@
|
||||||
import { MusicPlayerBuilder } from "@euterpe.js/player";
|
import { MusicPlayerBuilder } from "@euterpe.js/player"
|
||||||
import { db } from "./db";
|
import { db } from "./db"
|
||||||
import { Artist } from "@euterpe.js/music-library";
|
import { Artist } from "@euterpe.js/music-library"
|
||||||
import { DB, Platforms } from "@euterpe.js/music-library";
|
import { DB, Platforms } from "@euterpe.js/music-library"
|
||||||
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
||||||
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
||||||
const music_player = music_player_builder.build()
|
const music_player = music_player_builder.build()
|
||||||
music_player.change_volume(1)
|
music_player.change_volume(1)
|
||||||
|
|
||||||
let curr_song_id = 1;
|
let curr_song_id = 1
|
||||||
const elem_curr_song = document.querySelector("#text-playing")
|
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
|
let is_seeking = false
|
||||||
change_current_song_text(db)
|
|
||||||
|
|
||||||
document.querySelector("#previous")?.addEventListener("click", () => {
|
document.querySelector("#previous")?.addEventListener("click", () => {
|
||||||
curr_song_id--
|
curr_song_id--
|
||||||
if (curr_song_id < 0) curr_song_id = 2
|
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(
|
||||||
|
() => {
|
||||||
change_current_song_text(db)
|
change_current_song_text(db)
|
||||||
music_player.play_async().catch((err) => { console.log(err) })
|
music_player.try_play().catch((err) => {
|
||||||
}, (e) => { console.log(e) })
|
console.log(err)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
document.querySelector("#next")?.addEventListener("click", () => {
|
document.querySelector("#next")?.addEventListener("click", () => {
|
||||||
curr_song_id++
|
curr_song_id++
|
||||||
if (curr_song_id > 2) curr_song_id = 0
|
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(
|
||||||
|
() => {
|
||||||
change_current_song_text(db)
|
change_current_song_text(db)
|
||||||
music_player.play_async().catch((err) => { console.log(err) })
|
music_player.try_play().catch((err) => {
|
||||||
}, (e) => { console.log(e) })
|
console.log(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#play")?.addEventListener("click", () => {
|
document.querySelector("#play")?.addEventListener("click", () => {
|
||||||
music_player.play_async()
|
music_player.try_play().then(
|
||||||
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
|
() => {
|
||||||
|
console.log("Playing!")
|
||||||
|
},
|
||||||
|
(e) => alert("Failed to play, " + e)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||||
music_player.pause()
|
music_player.pause()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||||
music_player.mute()
|
music_player.mute()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||||
music_player.unmute()
|
music_player.unmute()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||||
music_player.mute_toggle()
|
music_player.mute_toggle()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
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))
|
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) => {
|
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||||
music_player.change_volume(e.target?.valueAsNumber)
|
music_player.change_volume(e.target?.valueAsNumber)
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||||
is_seeking = true;
|
is_seeking = true
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||||
music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => {
|
music_player.try_seek(e.target?.valueAsNumber).then(
|
||||||
|
() => {
|
||||||
|
console.log("seeked to " + e.target?.valueAsNumber)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
alert("Failed seeking! " + e)
|
alert("Failed seeking! " + e)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
is_seeking = false
|
is_seeking = false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscriptions to AudioContext changes, eg. time..
|
// 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("#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) => {
|
|
||||||
document.querySelector("#current").innerHTML = 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
|
if (is_seeking) return
|
||||||
document.querySelector("#seek").value = "" + time
|
document.querySelector("#seek")!.value = "" + time
|
||||||
})
|
})
|
||||||
|
|
||||||
}, (e) => console.log(e))
|
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) {
|
function change_current_song_text(db: DB) {
|
||||||
const curr_song = db.songs[curr_song_id]
|
const curr_song = db.songs[curr_song_id]
|
||||||
|
|
|
@ -62,17 +62,17 @@ let curr_song_id = 1;
|
||||||
document.querySelector("#previous")?.addEventListener("click", () => {
|
document.querySelector("#previous")?.addEventListener("click", () => {
|
||||||
curr_song_id--
|
curr_song_id--
|
||||||
if (curr_song_id < 0) curr_song_id = 2
|
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)
|
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) })
|
}, (e) => { console.log(e) })
|
||||||
})
|
})
|
||||||
document.querySelector("#next")?.addEventListener("click", () => {
|
document.querySelector("#next")?.addEventListener("click", () => {
|
||||||
curr_song_id++
|
curr_song_id++
|
||||||
if (curr_song_id > 2) curr_song_id = 0
|
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)
|
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) })
|
}, (e) => { console.log(e) })
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@euterpe.js/music-library",
|
"name": "@euterpe.js/music-library",
|
||||||
"version": "1.0.11",
|
"version": "1.0.12",
|
||||||
"type": "module",
|
"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.",
|
"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",
|
"main": "./src/index.js",
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
export {
|
export { RefTo, Ref, Song, Collection, DB, Artist, Platforms, CollectionType, from_json }
|
||||||
RefTo,
|
|
||||||
Ref,
|
|
||||||
Song,
|
|
||||||
Collection,
|
|
||||||
DB,
|
|
||||||
Artist,
|
|
||||||
Platforms,
|
|
||||||
CollectionType,
|
|
||||||
from_json
|
|
||||||
}
|
|
||||||
type ID = number
|
type ID = number
|
||||||
enum RefTo {
|
enum RefTo {
|
||||||
Artists,
|
Artists,
|
||||||
|
@ -20,7 +10,7 @@ enum CollectionType {
|
||||||
EP = "EP",
|
EP = "EP",
|
||||||
Single = "Single",
|
Single = "Single",
|
||||||
Playlist = "Playlist",
|
Playlist = "Playlist",
|
||||||
Release = "Release",
|
Release = "Release"
|
||||||
}
|
}
|
||||||
enum Platforms {
|
enum Platforms {
|
||||||
Youtube = "Youtube",
|
Youtube = "Youtube",
|
||||||
|
@ -33,7 +23,7 @@ enum Platforms {
|
||||||
Instagram = "Instagram",
|
Instagram = "Instagram",
|
||||||
Patreon = "Patreon",
|
Patreon = "Patreon",
|
||||||
Twitter = "Twitter",
|
Twitter = "Twitter",
|
||||||
Facebook = "Facebook",
|
Facebook = "Facebook"
|
||||||
}
|
}
|
||||||
|
|
||||||
class Ref {
|
class Ref {
|
||||||
|
@ -68,7 +58,7 @@ interface SongConstructor {
|
||||||
bpm?: number
|
bpm?: number
|
||||||
key?: string
|
key?: string
|
||||||
fft_data?: number[]
|
fft_data?: number[]
|
||||||
id?: ID,
|
id?: ID
|
||||||
metadata?: any[]
|
metadata?: any[]
|
||||||
}
|
}
|
||||||
class Song {
|
class Song {
|
||||||
|
@ -106,7 +96,7 @@ class Song {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArtistConstructor {
|
interface ArtistConstructor {
|
||||||
name: string,
|
name: string
|
||||||
pfp?: URL
|
pfp?: URL
|
||||||
songs?: Ref[]
|
songs?: Ref[]
|
||||||
collections?: Ref[]
|
collections?: Ref[]
|
||||||
|
@ -145,7 +135,6 @@ interface CollectionConstructor {
|
||||||
metadata?: any[]
|
metadata?: any[]
|
||||||
name?: string
|
name?: string
|
||||||
type?: CollectionType
|
type?: CollectionType
|
||||||
|
|
||||||
}
|
}
|
||||||
class Collection {
|
class Collection {
|
||||||
name?: string
|
name?: string
|
||||||
|
@ -206,9 +195,7 @@ class DB {
|
||||||
curr_col?.artists.push(new Ref(RefTo.Artists, artist.id))
|
curr_col?.artists.push(new Ref(RefTo.Artists, artist.id))
|
||||||
}
|
}
|
||||||
this.artists.push(artist)
|
this.artists.push(artist)
|
||||||
}
|
} else if (input instanceof Collection) {
|
||||||
|
|
||||||
else if (input instanceof Collection) {
|
|
||||||
const col = input as Collection
|
const col = input as Collection
|
||||||
if (!col.id) col.id = this.collections.length
|
if (!col.id) col.id = this.collections.length
|
||||||
|
|
||||||
|
@ -221,9 +208,7 @@ class DB {
|
||||||
curr_artist.collections.push(new Ref(RefTo.Collections, col.id))
|
curr_artist.collections.push(new Ref(RefTo.Collections, col.id))
|
||||||
}
|
}
|
||||||
this.collections.push(col)
|
this.collections.push(col)
|
||||||
}
|
} else if (input instanceof Song) {
|
||||||
|
|
||||||
else if (input instanceof Song) {
|
|
||||||
const song = input as Song
|
const song = input as Song
|
||||||
if (!song.id) song.id = this.songs.length
|
if (!song.id) song.id = this.songs.length
|
||||||
|
|
||||||
|
@ -250,32 +235,51 @@ class DB {
|
||||||
this.collections.sort((a, b) => a.id! - b.id!)
|
this.collections.sort((a, b) => a.id! - b.id!)
|
||||||
this.artists.sort((a, b) => a.id! - b.id!)
|
this.artists.sort((a, b) => a.id! - b.id!)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
function from_json(db_stringified: { artists?: any, songs?: any, collections?: any }): DB {
|
function from_json(db_stringified: { artists?: any; songs?: any; collections?: any }): DB {
|
||||||
const db = new DB
|
const db = new DB()
|
||||||
if (db_stringified.artists) {
|
if (db_stringified.artists) {
|
||||||
for (const artist of db_stringified.artists) {
|
for (const artist of db_stringified.artists) {
|
||||||
if (artist.songs) artist.songs = artist.songs.map((e: any) => ref_from_json(e))
|
if (artist.songs) artist.songs = artist.songs.map((e: any) => ref_from_json(e))
|
||||||
if (artist.collections) artist.collections = artist.collections.map((e: any) => ref_from_json(e))
|
if (artist.collections) artist.collections = artist.collections.map((e: any) => ref_from_json(e))
|
||||||
if (artist.links) artist.links = artist.links.map((e: any) => { try { [e[0] as Platforms, new URL(e[1])] } catch (e) { console.log(e) } })
|
if (artist.links)
|
||||||
|
artist.links = artist.links.map((e: any) => {
|
||||||
|
try {
|
||||||
|
;[e[0] as Platforms, new URL(e[1])]
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
if (artist.publish_date) artist.publish_date = new Date(JSON.parse(artist.publish_date))
|
if (artist.publish_date) artist.publish_date = new Date(JSON.parse(artist.publish_date))
|
||||||
if (artist.id) artist.id = artist.id as ID
|
if (artist.id) artist.id = artist.id as ID
|
||||||
try { if (artist.pfp) artist.pfp = new URL(artist.pfp) }
|
try {
|
||||||
catch (e) { console.error(e), console.error("failed to parse artist URL") }
|
if (artist.pfp) artist.pfp = new URL(artist.pfp)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e), console.error("failed to parse artist URL")
|
||||||
|
}
|
||||||
db.artists.push(artist)
|
db.artists.push(artist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (db_stringified.songs) {
|
if (db_stringified.songs) {
|
||||||
for (const song of db_stringified.songs) {
|
for (const song of db_stringified.songs) {
|
||||||
try { if (song.url) song.url = new URL(song.url) } catch (e) { console.error("failed to parse song.url" + e) }
|
try {
|
||||||
|
if (song.url) song.url = new URL(song.url)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("failed to parse song.url" + e)
|
||||||
|
}
|
||||||
if (song.artists) song.artists = song.artists.map((e: any) => ref_from_json(e))
|
if (song.artists) song.artists = song.artists.map((e: any) => ref_from_json(e))
|
||||||
if (song.remix_artists) song.remix_artists = song.remix_artists.map((e: any) => ref_from_json(e))
|
if (song.remix_artists) song.remix_artists = song.remix_artists.map((e: any) => ref_from_json(e))
|
||||||
if (song.in_collection) song.in_collection = ref_from_json(song.in_collection)
|
if (song.in_collection) song.in_collection = ref_from_json(song.in_collection)
|
||||||
try { if (song.cover) song.cover = new URL(song.cover) }
|
try {
|
||||||
catch (e) { console.error(e), console.error("failed to parse artist URL") }
|
if (song.cover) song.cover = new URL(song.cover)
|
||||||
try { if (song.publish_date) song.publish_date = new Date(JSON.parse(song.publish_date)) }
|
} catch (e) {
|
||||||
catch (e) { console.error(e), console.error("Failed to song cover url") }
|
console.error(e), console.error("failed to parse artist URL")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (song.publish_date) song.publish_date = new Date(JSON.parse(song.publish_date))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e), console.error("Failed to song cover url")
|
||||||
|
}
|
||||||
if (song.id) song.id = song.id as ID
|
if (song.id) song.id = song.id as ID
|
||||||
db.songs.push(song)
|
db.songs.push(song)
|
||||||
}
|
}
|
||||||
|
@ -285,10 +289,16 @@ function from_json(db_stringified: { artists?: any, songs?: any, collections?: a
|
||||||
if (collection.artists) collection.artists = collection.artists.map((e: any) => ref_from_json(e))
|
if (collection.artists) collection.artists = collection.artists.map((e: any) => ref_from_json(e))
|
||||||
if (collection.songs) collection.songs = collection.songs.map((e: any) => ref_from_json(e))
|
if (collection.songs) collection.songs = collection.songs.map((e: any) => ref_from_json(e))
|
||||||
if (collection.type) collection.type = collection.type.map((e: any) => e as CollectionType)
|
if (collection.type) collection.type = collection.type.map((e: any) => e as CollectionType)
|
||||||
try { if (collection.publish_date) collection.publish_date = new Date(JSON.parse(collection.publish_date)) }
|
try {
|
||||||
catch (e) { console.error(e), console.error("Failed to parse date") }
|
if (collection.publish_date) collection.publish_date = new Date(JSON.parse(collection.publish_date))
|
||||||
try { if (collection.cover) collection.cover = new URL(collection.cover) }
|
} catch (e) {
|
||||||
catch (e) { console.error(e), console.error("failed to parse collection cover url") }
|
console.error(e), console.error("Failed to parse date")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (collection.cover) collection.cover = new URL(collection.cover)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e), console.error("failed to parse collection cover url")
|
||||||
|
}
|
||||||
if (collection.id) collection.id = collection.id as ID
|
if (collection.id) collection.id = collection.id as ID
|
||||||
db.collections.push(collection)
|
db.collections.push(collection)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,52 @@
|
||||||
import { MusicPlayerBuilder } from "@euterpe.js/player";
|
import { MusicPlayerBuilder } from "@euterpe.js/player"
|
||||||
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
||||||
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
||||||
const music_player = music_player_builder.build()
|
const music_player = music_player_builder.build()
|
||||||
music_player.change_volume(1)
|
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
|
let is_seeking = false
|
||||||
|
|
||||||
document.querySelector("#play")?.addEventListener("click", () => {
|
document.querySelector("#play")?.addEventListener("click", () => {
|
||||||
//const analyser_node = music_player_builder.add_analyser()
|
//const analyser_node = music_player_builder.add_analyser()
|
||||||
music_player.try_play()
|
music_player.try_play().then(
|
||||||
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
|
() => {
|
||||||
|
console.log("Playing!")
|
||||||
|
},
|
||||||
|
(e) => alert("Failed to play, " + e)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||||
music_player.pause()
|
music_player.pause()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||||
music_player.mute()
|
music_player.mute()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||||
music_player.unmute()
|
music_player.unmute()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||||
music_player.mute_toggle()
|
music_player.mute_toggle()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
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))
|
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) => {
|
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||||
music_player.change_volume(e.target?.valueAsNumber)
|
music_player.change_volume(e.target?.valueAsNumber)
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||||
is_seeking = true;
|
is_seeking = true
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||||
try {
|
try {
|
||||||
music_player.try_seek(e.target?.valueAsNumber)
|
music_player.try_seek(e.target?.valueAsNumber)
|
||||||
|
@ -42,17 +56,20 @@ music_player.try_new_song(encodeURI("http://" + window.location.host + "/nuphory
|
||||||
}
|
}
|
||||||
is_seeking = false
|
is_seeking = false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscriptions to AudioContext changes, eg. time..
|
// Subscriptions to AudioContext changes, eg. time..
|
||||||
music_player.on_duration_formatted((time) => {
|
music_player.on_duration_formatted((time) => {
|
||||||
document.querySelector("#duration")!.innerHTML = time
|
document.querySelector("#duration")!.innerHTML = time
|
||||||
document.querySelector("#seek")!.max = "" + music_player.current_song_duration
|
document.querySelector("#seek")!.max = "" + music_player.current_song_duration
|
||||||
})
|
})
|
||||||
|
|
||||||
music_player.on_time_tick_formatted((time) => {
|
music_player.on_time_tick_formatted((time) => {
|
||||||
document.querySelector("#current")!.innerHTML = time
|
document.querySelector("#current")!.innerHTML = time
|
||||||
})
|
})
|
||||||
|
|
||||||
music_player.on_time_tick((time) => {
|
music_player.on_time_tick((time) => {
|
||||||
if (is_seeking) return
|
if (is_seeking) return
|
||||||
document.querySelector("#seek")!.value = "" + time
|
document.querySelector("#seek")!.value = "" + time
|
||||||
})
|
})
|
||||||
|
|
||||||
}, (e) => console.log(e))
|
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)
|
##### 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.
|
All we need to do is import the player builder and build it
|
||||||
```js
|
```ts
|
||||||
import { MusicPlayerBuilder } from "@euterpe/player";
|
import { MusicPlayerBuilder } from "@euterpe/player";
|
||||||
const audio_el = document.querySelector("#audio")
|
const audio_el = document.querySelector("#audio")
|
||||||
|
|
||||||
const music_player_builder = MusicPlayerBuilder(audio_el)
|
const music_player_builder = MusicPlayerBuilder(audio_el)
|
||||||
music_player_builder.start()
|
|
||||||
|
|
||||||
// Builder allows for attaching custom nodes if necessary, eg.
|
// Builder allows for attaching custom nodes if necessary, eg.
|
||||||
const panning_node = music_player_builder.add_stereo_panner_node()
|
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()
|
const music_player = music_player_builder.build()
|
||||||
|
|
||||||
//Next we add a song URL to the Audio Element,
|
//Next we add a song URL to the Audio Element,
|
||||||
music_player.try_new_song_async(encodeURI("my_song.ogg"))
|
music_player.try_new_song(encodeURI("my_song.ogg"))
|
||||||
.then(() => {
|
|
||||||
//and wait for the user input to resume the AudioContext
|
//and wait for the user input to resume the AudioContext
|
||||||
document.querySelector("#play")?.addEventListener("click", () => {
|
document.querySelector("#play")?.addEventListener("click", () => {
|
||||||
music_player.play_async()
|
music_player.try_play()
|
||||||
.then(
|
.then(
|
||||||
//Easily follow up with what to do next
|
//Easily follow up with what to do next
|
||||||
() => { console.log("Playing!") },
|
() => { console.log("Playing!") },
|
||||||
(e) => alert("Failed to play, " + e)
|
(e) => alert("Failed to play, " + e)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
It's quite easy to give user the control in UI
|
It's quite easy to give user the control in UI
|
||||||
```js
|
|
||||||
|
```ts
|
||||||
// Play when user clicks a <button></button>
|
// Play when user clicks a <button></button>
|
||||||
document.querySelector("#play-button")?.addEventListener("click", () => {
|
document.querySelector("#play-button")?.addEventListener("click", () => {
|
||||||
music_player.play_async()
|
music_player.try_play()
|
||||||
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
|
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
|
||||||
})
|
})
|
||||||
// Mute when user clicks another <button></button>
|
// 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.
|
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..
|
// Subscriptions to AudioContext changes, eg. time..
|
||||||
music_player.on_duration_formatted((time) => {
|
music_player.on_duration_formatted((time) => {
|
||||||
//time == "4:53, "15:59", "1756:15:59"...
|
//time == "4:53, "15:59", "1756:15:59"...
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@euterpe.js/player",
|
"name": "@euterpe.js/player",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "A simple, safe AudioContext web music player",
|
"description": "A simple, safe AudioContext web music player",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export enum SubscribeEvents {
|
export enum SubscribeEvents {
|
||||||
CurrentTimeTick,
|
CurrentTimeTick,
|
||||||
FormattedDurationTick,
|
FormattedDurationTick,
|
||||||
FormattedCurrentTimeTick,
|
FormattedCurrentTimeTick
|
||||||
}
|
}
|
||||||
class PubSub {
|
class PubSub {
|
||||||
//el = event listener
|
//el = event listener
|
||||||
|
@ -78,20 +78,20 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class MusicPlayer {
|
export class MusicPlayer {
|
||||||
current_song_duration = 0
|
current_song_duration = 0
|
||||||
#volume_cache: number
|
#volume_cache: number
|
||||||
is_playing = false
|
is_playing = false
|
||||||
time = 0
|
time = 0
|
||||||
#pub_sub = new PubSub
|
#pub_sub = new PubSub()
|
||||||
constructor(
|
constructor(
|
||||||
public audio_context: AudioContext,
|
public audio_context: AudioContext,
|
||||||
private audio_element: HTMLAudioElement,
|
private audio_element: HTMLAudioElement,
|
||||||
public track: MediaElementAudioSourceNode,
|
public track: MediaElementAudioSourceNode,
|
||||||
public gain: GainNode,
|
public gain: GainNode,
|
||||||
public volume: number,
|
public volume: number,
|
||||||
private current_song_path?: string) {
|
private current_song_path?: string
|
||||||
|
) {
|
||||||
this.#volume_cache = volume
|
this.#volume_cache = volume
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,8 +118,8 @@ export class MusicPlayer {
|
||||||
this.volume = this.gain.gain.value = volume_i
|
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 also resume playback if player is paused (by finishing the song etc)
|
* will not resume playback
|
||||||
* @throws if "Can't seek - Audiocontext is not running"
|
* @throws if "Can't seek - Audiocontext is not running"
|
||||||
*/
|
*/
|
||||||
async try_seek(new_time: number) {
|
async try_seek(new_time: number) {
|
||||||
|
@ -127,19 +127,18 @@ export class MusicPlayer {
|
||||||
this.is_playing = false
|
this.is_playing = false
|
||||||
throw new Error("Can't seek - audioContext not running, audio_context.state : " + this.audio_context.state)
|
throw new Error("Can't seek - audioContext not running, audio_context.state : " + this.audio_context.state)
|
||||||
}
|
}
|
||||||
if (this.audio_element.paused) await this.try_play()
|
|
||||||
this.audio_element.currentTime = new_time
|
this.audio_element.currentTime = new_time
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
seek(new_time: number) {
|
||||||
this.audio_element.currentTime = new_time
|
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
|
* @throws Error if playback failed
|
||||||
*/
|
*/
|
||||||
async try_play_toggle() {
|
async try_play_toggle() {
|
||||||
|
@ -197,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() {
|
play() {
|
||||||
if (this.is_playing) return
|
if (this.is_playing) return
|
||||||
|
@ -215,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
|
* @throws Error if adding element throwed Error or Stalled
|
||||||
*/
|
*/
|
||||||
async try_new_song(path: string) {
|
async try_new_song(path: string) {
|
||||||
|
@ -229,31 +228,44 @@ export class MusicPlayer {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
this.audio_element.src = this.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!
|
//Found out today about this. Such a nice new way to mass remove event listeners!
|
||||||
const controller = new AbortController();
|
const controller = new AbortController()
|
||||||
|
|
||||||
this.audio_element.addEventListener("canplaythrough", function canplay_listener() {
|
this.audio_element.addEventListener(
|
||||||
|
"canplaythrough",
|
||||||
|
function canplay_listener() {
|
||||||
controller.abort()
|
controller.abort()
|
||||||
}, { signal: controller.signal })
|
},
|
||||||
|
{ signal: controller.signal }
|
||||||
|
)
|
||||||
|
|
||||||
this.audio_element.addEventListener("error", function error_listener() {
|
this.audio_element.addEventListener(
|
||||||
|
"error",
|
||||||
|
function error_listener() {
|
||||||
controller.abort("new src error")
|
controller.abort("new src error")
|
||||||
}, { signal: controller.signal })
|
},
|
||||||
|
{ signal: controller.signal }
|
||||||
|
)
|
||||||
|
|
||||||
this.audio_element.addEventListener("stalled", function stalled_listener() {
|
this.audio_element.addEventListener(
|
||||||
|
"stalled",
|
||||||
|
function stalled_listener() {
|
||||||
controller.abort("new src stalled")
|
controller.abort("new src stalled")
|
||||||
}, { signal: controller.signal })
|
},
|
||||||
|
{ signal: controller.signal }
|
||||||
|
)
|
||||||
|
|
||||||
//once aborted, try to set current_song_duration
|
//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
|
this.current_song_duration = this.audio_element.duration
|
||||||
if (typeof controller.signal.reason == "string") reject(new Error(controller.signal.reason))
|
if (typeof controller.signal.reason == "string") reject(new Error(controller.signal.reason))
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.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
|
* 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) {
|
new_song(path: string) {
|
||||||
this.audio_element.src = this.current_song_path = path
|
this.audio_element.src = this.current_song_path = path
|
||||||
|
@ -270,18 +282,18 @@ export class MusicPlayer {
|
||||||
if (dur == 0 || !dur) return "0:00"
|
if (dur == 0 || !dur) return "0:00"
|
||||||
|
|
||||||
// ~ is Bitwise NOT, equivalent to Math.floor()
|
// ~ is Bitwise NOT, equivalent to Math.floor()
|
||||||
const hrs = ~~(dur / 3600);
|
const hrs = ~~(dur / 3600)
|
||||||
const mins = ~~((dur % 3600) / 60);
|
const mins = ~~((dur % 3600) / 60)
|
||||||
const secs = ~~dur % 60;
|
const secs = ~~dur % 60
|
||||||
|
|
||||||
let ret = ""
|
let ret = ""
|
||||||
if (hrs > 0) {
|
if (hrs > 0) {
|
||||||
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
|
ret += "" + hrs + ":" + (mins < 10 ? "0" : "")
|
||||||
}
|
}
|
||||||
|
|
||||||
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
|
ret += "" + mins + ":" + (secs < 10 ? "0" : "")
|
||||||
ret += "" + secs;
|
ret += "" + secs
|
||||||
return ret;
|
return ret
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Will parse the current time of the song to make it easy to display in UI
|
* Will parse the current time of the song to make it easy to display in UI
|
||||||
|
@ -292,18 +304,18 @@ export class MusicPlayer {
|
||||||
|
|
||||||
if (curr == 0 || !curr) return "0:00"
|
if (curr == 0 || !curr) return "0:00"
|
||||||
// ~~ is Bitwise OR, equivalent to Math.floor()
|
// ~~ is Bitwise OR, equivalent to Math.floor()
|
||||||
const hrs = ~~(curr / 3600);
|
const hrs = ~~(curr / 3600)
|
||||||
const mins = ~~((curr % 3600) / 60);
|
const mins = ~~((curr % 3600) / 60)
|
||||||
const secs = ~~curr % 60;
|
const secs = ~~curr % 60
|
||||||
|
|
||||||
let ret = ""
|
let ret = ""
|
||||||
if (hrs > 0) {
|
if (hrs > 0) {
|
||||||
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
|
ret += "" + hrs + ":" + (mins < 10 ? "0" : "")
|
||||||
}
|
}
|
||||||
|
|
||||||
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
|
ret += "" + mins + ":" + (secs < 10 ? "0" : "")
|
||||||
ret += "" + secs;
|
ret += "" + secs
|
||||||
return ret;
|
return ret
|
||||||
}
|
}
|
||||||
#emit_time() {
|
#emit_time() {
|
||||||
const request_id = requestAnimationFrame(this.#emit_time.bind(this))
|
const request_id = requestAnimationFrame(this.#emit_time.bind(this))
|
||||||
|
@ -352,15 +364,12 @@ export class MusicPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class MusicPlayerBuilder {
|
export class MusicPlayerBuilder {
|
||||||
#audio_context: AudioContext
|
#audio_context: AudioContext
|
||||||
#gain: GainNode
|
#gain: GainNode
|
||||||
#track: MediaElementAudioSourceNode
|
#track: MediaElementAudioSourceNode
|
||||||
#volume = 1
|
#volume = 1
|
||||||
#prev_node: any;
|
#prev_node: any
|
||||||
#is_gain_connected = false
|
#is_gain_connected = false
|
||||||
/**
|
/**
|
||||||
* Creates a context and #gain( Gets connected at the end )
|
* Creates a context and #gain( Gets connected at the end )
|
||||||
|
@ -370,7 +379,7 @@ export class MusicPlayerBuilder {
|
||||||
constructor(private audio_element: HTMLAudioElement) {
|
constructor(private audio_element: HTMLAudioElement) {
|
||||||
if (audio_element === undefined) throw Error("audio_element was undefined")
|
if (audio_element === undefined) throw Error("audio_element was undefined")
|
||||||
// ↓ For old browsers
|
// ↓ For old browsers
|
||||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
const AudioContext = window.AudioContext || window.webkitAudioContext
|
||||||
this.#audio_context = new AudioContext()
|
this.#audio_context = new AudioContext()
|
||||||
this.#track = this.#audio_context.createMediaElementSource(audio_element)
|
this.#track = this.#audio_context.createMediaElementSource(audio_element)
|
||||||
this.#gain = this.#audio_context.createGain()
|
this.#gain = this.#audio_context.createGain()
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import filehound from "filehound"
|
import filehound from "filehound"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
const songs = filehound.create().path("../public/samples").ext(["ogg"]).findSync()
|
const songs = filehound.create().path("../public/samples").ext(["ogg"]).findSync()
|
||||||
fs.writeFile('songs_list.ts', `export const songs = ` + JSON.stringify(songs), 'utf8', () => { 1 + 1 })
|
fs.writeFile("songs_list.ts", `export const songs = ` + JSON.stringify(songs), "utf8", () => {
|
||||||
|
1 + 1
|
||||||
|
})
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Collection, Ref, RefTo, Song, DB, } from "@euterpe.js/music-library"
|
import { Collection, Ref, RefTo, Song, DB } from "@euterpe.js/music-library"
|
||||||
import { songs } from "./songs_list"
|
import { songs } from "./songs_list"
|
||||||
|
|
||||||
export function generate_db() {
|
export function generate_db() {
|
||||||
console.log(songs)
|
console.log(songs)
|
||||||
// construct db
|
// construct db
|
||||||
let db = new DB
|
let db = new DB()
|
||||||
let collections: string[] = new Array()
|
let collections: string[] = new Array()
|
||||||
let new_songs = []
|
let new_songs = []
|
||||||
const path_char = songs[0].includes("\\") ? "\\" : "/"
|
const path_char = songs[0].includes("\\") ? "\\" : "/"
|
||||||
|
@ -20,15 +20,17 @@ export function generate_db() {
|
||||||
*/
|
*/
|
||||||
if (!collections.includes(collection_name)) {
|
if (!collections.includes(collection_name)) {
|
||||||
console.log(`creating collection ${collection_name}`)
|
console.log(`creating collection ${collection_name}`)
|
||||||
db.add([new Collection({
|
db.add([
|
||||||
|
new Collection({
|
||||||
name: collection_name,
|
name: collection_name,
|
||||||
songs: [],
|
songs: [],
|
||||||
artists: [],
|
artists: []
|
||||||
})])
|
})
|
||||||
|
])
|
||||||
collections.push(collection_name)
|
collections.push(collection_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
let col = db.collections.find(col => col.name == collection_name)!
|
let col = db.collections.find((col) => col.name == collection_name)!
|
||||||
let col_id = col.id
|
let col_id = col.id
|
||||||
new_songs.push({ song: song, collection_id: col_id! })
|
new_songs.push({ song: song, collection_id: col_id! })
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { DB, from_json } from "@euterpe.js/music-library"
|
||||||
import { generate_db } from "./generate_db"
|
import { generate_db } from "./generate_db"
|
||||||
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType, WaveformOrientation, WaveformShape } from "@euterpe.js/visualizer"
|
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType, WaveformOrientation, WaveformShape } from "@euterpe.js/visualizer"
|
||||||
|
|
||||||
let result: AnalyzeReturn | undefined;
|
let result: AnalyzeReturn | undefined
|
||||||
|
|
||||||
let db = generate_db()
|
let db = generate_db()
|
||||||
//Create all audio nodes
|
//Create all audio nodes
|
||||||
|
@ -19,7 +19,6 @@ const FFTDataArray = new Float32Array(analyserBufferLength)
|
||||||
//Connect all audio Nodes
|
//Connect all audio Nodes
|
||||||
track.connect(audioContextAnalyser).connect(gain).connect(audioContext.destination)
|
track.connect(audioContextAnalyser).connect(gain).connect(audioContext.destination)
|
||||||
|
|
||||||
|
|
||||||
document.getElementById("analyze")!.addEventListener("click", async (ev) => {
|
document.getElementById("analyze")!.addEventListener("click", async (ev) => {
|
||||||
audioContext.resume()
|
audioContext.resume()
|
||||||
result = await analyze()
|
result = await analyze()
|
||||||
|
@ -35,7 +34,7 @@ document.getElementById("upload")!.addEventListener("change", (ev) => {
|
||||||
audioContext.resume()
|
audioContext.resume()
|
||||||
const fileReader = new FileReader()
|
const fileReader = new FileReader()
|
||||||
fileReader.readAsText(ev.target.files[0])
|
fileReader.readAsText(ev.target.files[0])
|
||||||
fileReader.onload = event => {
|
fileReader.onload = (event) => {
|
||||||
let str = JSON.parse(event.target.result)
|
let str = JSON.parse(event.target.result)
|
||||||
let new_db = from_json(str)
|
let new_db = from_json(str)
|
||||||
//-infinity get stringified to null, undo that
|
//-infinity get stringified to null, undo that
|
||||||
|
@ -48,7 +47,6 @@ document.getElementById("upload")!.addEventListener("change", (ev) => {
|
||||||
}
|
}
|
||||||
result = { db: new_db, analyzer_node: audioContextAnalyser }
|
result = { db: new_db, analyzer_node: audioContextAnalyser }
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function svg() {
|
async function svg() {
|
||||||
|
@ -69,10 +67,18 @@ async function svg() {
|
||||||
const curr_waveform_canvas = waveform_canvas.cloneNode() as SVGSVGElement
|
const curr_waveform_canvas = waveform_canvas.cloneNode() as SVGSVGElement
|
||||||
waveform_canvas.parentElement?.append(curr_waveform_canvas)
|
waveform_canvas.parentElement?.append(curr_waveform_canvas)
|
||||||
const waveform_visual_builder = new AudioVisualBuilder(result.analyzer_node, curr_waveform_canvas)
|
const waveform_visual_builder = new AudioVisualBuilder(result.analyzer_node, curr_waveform_canvas)
|
||||||
.set_fft_data_tresholds({ point_count_i: 100, fft_multiplier_i: .9, fft_offset_i: -65 })
|
.set_fft_data_tresholds({
|
||||||
|
point_count_i: 100,
|
||||||
|
fft_multiplier_i: 0.9,
|
||||||
|
fft_offset_i: -65
|
||||||
|
})
|
||||||
.set_fft_time_smoothing(0.8)
|
.set_fft_time_smoothing(0.8)
|
||||||
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
||||||
const waveform_visual = waveform_visual_builder.build(ShapeType.Waveform, true, { fft_data: new Float32Array(new Float64Array(song.fft_data!)), orientation: WaveformOrientation.Horizontal, shape_type: WaveformShape.LineLike })
|
const waveform_visual = waveform_visual_builder.build(ShapeType.Waveform, true, {
|
||||||
|
fft_data: new Float32Array(new Float64Array(song.fft_data!)),
|
||||||
|
orientation: WaveformOrientation.Horizontal,
|
||||||
|
shape_type: WaveformShape.LineLike
|
||||||
|
})
|
||||||
waveform_visual.draw_once()
|
waveform_visual.draw_once()
|
||||||
// await new Promise<void>((done) => setTimeout(() => done(), 500))
|
// await new Promise<void>((done) => setTimeout(() => done(), 500))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -82,7 +88,6 @@ async function svg() {
|
||||||
waveform_canvas.remove()
|
waveform_canvas.remove()
|
||||||
console.dir(result.db, { depth: null })
|
console.dir(result.db, { depth: null })
|
||||||
download(JSON.stringify(result.db), "db.json", "text/plain")
|
download(JSON.stringify(result.db), "db.json", "text/plain")
|
||||||
|
|
||||||
}
|
}
|
||||||
async function analyze(): Promise<AnalyzeReturn> {
|
async function analyze(): Promise<AnalyzeReturn> {
|
||||||
console.clear()
|
console.clear()
|
||||||
|
@ -117,18 +122,21 @@ async function analyze(): Promise<AnalyzeReturn> {
|
||||||
console.log(song.fft_data)
|
console.log(song.fft_data)
|
||||||
}
|
}
|
||||||
console.log("Analyzation finished!")
|
console.log("Analyzation finished!")
|
||||||
const result: AnalyzeReturn = { analyzer_node: audioContextAnalyser, db: db }
|
const result: AnalyzeReturn = {
|
||||||
|
analyzer_node: audioContextAnalyser,
|
||||||
|
db: db
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
function download(content: BlobPart, fileName: string, contentType: string) {
|
function download(content: BlobPart, fileName: string, contentType: string) {
|
||||||
var a = document.querySelector("#download") as HTMLAnchorElement;
|
var a = document.querySelector("#download") as HTMLAnchorElement
|
||||||
var file = new Blob([content], { type: contentType });
|
var file = new Blob([content], { type: contentType })
|
||||||
a.href = URL.createObjectURL(file);
|
a.href = URL.createObjectURL(file)
|
||||||
a.download = fileName;
|
a.download = fileName
|
||||||
// a.click();
|
// a.click();
|
||||||
}
|
}
|
||||||
type AnalyzeReturn = {
|
type AnalyzeReturn = {
|
||||||
analyzer_node: AnalyserNode,
|
analyzer_node: AnalyserNode
|
||||||
db: DB
|
db: DB
|
||||||
}
|
}
|
||||||
function awaitLoad(audioEl: HTMLAudioElement) {
|
function awaitLoad(audioEl: HTMLAudioElement) {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import filehound from "filehound"
|
import filehound from "filehound"
|
||||||
import { execSync, exec } from 'child_process'
|
import { execSync, exec } from "child_process"
|
||||||
import { fstat, unlinkSync } from "fs"
|
import { fstat, unlinkSync } from "fs"
|
||||||
|
|
||||||
function generate_new_photo_sizes(file, currentExtention) {
|
function generate_new_photo_sizes(file, currentExtention) {
|
||||||
|
@ -22,8 +22,12 @@ function generate_new_photo_sizes(file, currentExtention) {
|
||||||
exec(command)
|
exec(command)
|
||||||
}
|
}
|
||||||
function generate_new_anim_photo_sizes(file, currentExtention) {
|
function generate_new_anim_photo_sizes(file, currentExtention) {
|
||||||
exec(`start ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_ogw_static.webp" -vf scale=1000:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_1000w_static.webp" -vf scale=800:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_800w_static.webp" -vf scale=500:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_500w_static.webp" -vf scale=320:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_320w_static.webp" -vf scale=-1:64,gblur=sigma=10:steps=2 -lossless 0 -frames:v 1 -r 1 -compression_level 6 -quality 85 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_placeholder_static.webp"`)
|
exec(
|
||||||
exec(`start ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_ogw.webp" -vf scale=1000:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_1000w.webp" -vf scale=800:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_800w.webp" -vf scale=500:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_500w.webp" -vf scale=320:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_320w.webp" -vf scale=-1:64,gblur=sigma=10:steps=2 -frames:v 1 -lossless 0 -c:v libwebp -compression_level 6 -quality 85 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_placeholder.webp"`)
|
`start ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_ogw_static.webp" -vf scale=1000:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_1000w_static.webp" -vf scale=800:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_800w_static.webp" -vf scale=500:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_500w_static.webp" -vf scale=320:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_320w_static.webp" -vf scale=-1:64,gblur=sigma=10:steps=2 -lossless 0 -frames:v 1 -r 1 -compression_level 6 -quality 85 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_placeholder_static.webp"`
|
||||||
|
)
|
||||||
|
exec(
|
||||||
|
`start ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_ogw.webp" -vf scale=1000:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_1000w.webp" -vf scale=800:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_800w.webp" -vf scale=500:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_500w.webp" -vf scale=320:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_320w.webp" -vf scale=-1:64,gblur=sigma=10:steps=2 -frames:v 1 -lossless 0 -c:v libwebp -compression_level 6 -quality 85 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_placeholder.webp"`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
function generate_new_sounds_ogg(file, currentExtention) {
|
function generate_new_sounds_ogg(file, currentExtention) {
|
||||||
const path = file.substring(0, file.lastIndexOf("\\"))
|
const path = file.substring(0, file.lastIndexOf("\\"))
|
||||||
|
@ -72,7 +76,7 @@ function generate_new_video_sizes_mp4(file, currentExtention, width_resolutions)
|
||||||
res_command += `cd "${path}" && `
|
res_command += `cd "${path}" && `
|
||||||
res_command += `ffmpeg -y -i "${file}.${currentExtention}" `
|
res_command += `ffmpeg -y -i "${file}.${currentExtention}" `
|
||||||
res_command += `-vcodec libx264 -g 240 -b:v 3M -vf scale=${resolution}:-2 -pass 2 "${file}_${resolution}p.mp4"`
|
res_command += `-vcodec libx264 -g 240 -b:v 3M -vf scale=${resolution}:-2 -pass 2 "${file}_${resolution}p.mp4"`
|
||||||
res_command += "&& exit\""
|
res_command += '&& exit"'
|
||||||
exec(res_command)
|
exec(res_command)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -95,27 +99,24 @@ function generate_new_video_sizes_webm(file, currentExtention, width_resolutions
|
||||||
res_command += `cd "${path}" && `
|
res_command += `cd "${path}" && `
|
||||||
res_command += `ffmpeg -y -i "${file}.${currentExtention}" `
|
res_command += `ffmpeg -y -i "${file}.${currentExtention}" `
|
||||||
res_command += `-vcodec libvpx-vp9 -cpu-used 0 -deadline good -quality good -g 240 -vf scale=${resolution}:-1 -crf 42 -b:v 0 -c:a libopus -row-mt 1 -tile-rows 2 -tile-columns 4 -threads 16 -auto-alt-ref 6 -pass 2 "${file}_${resolution}p.webm"`
|
res_command += `-vcodec libvpx-vp9 -cpu-used 0 -deadline good -quality good -g 240 -vf scale=${resolution}:-1 -crf 42 -b:v 0 -c:a libopus -row-mt 1 -tile-rows 2 -tile-columns 4 -threads 16 -auto-alt-ref 6 -pass 2 "${file}_${resolution}p.webm"`
|
||||||
res_command += "&& exit\""
|
res_command += '&& exit"'
|
||||||
exec(res_command)
|
exec(res_command)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let dirs = filehound.create()
|
let dirs = filehound.create().path("../public").directory().findSync()
|
||||||
.path("../public")
|
|
||||||
.directory()
|
|
||||||
.findSync()
|
|
||||||
console.log(dirs)
|
console.log(dirs)
|
||||||
|
|
||||||
for (let i = 0; i < dirs.length; i++) {
|
for (let i = 0; i < dirs.length; i++) {
|
||||||
//gets current name file+ext
|
//gets current name file+ext
|
||||||
let current_folder_files = filehound.create()
|
let current_folder_files = filehound.create().path(`${dirs[i]}`).findSync()
|
||||||
.path(`${dirs[i]}`)
|
|
||||||
.findSync()
|
|
||||||
|
|
||||||
if (current_folder_files[0] != undefined) {
|
if (current_folder_files[0] != undefined) {
|
||||||
//if previous encode was cancelled and 2pass log not removed, remove it :)
|
//if previous encode was cancelled and 2pass log not removed, remove it :)
|
||||||
if (current_folder_files[0].includes("ffmpeg2pass-0.log")) {
|
if (current_folder_files[0].includes("ffmpeg2pass-0.log")) {
|
||||||
try { unlinkSync(`${dirs[i]}/ffmpeg2pass-0.log`) } catch (err) { }
|
try {
|
||||||
|
unlinkSync(`${dirs[i]}/ffmpeg2pass-0.log`)
|
||||||
|
} catch (err) {}
|
||||||
current_folder_files = current_folder_files.slice(1)
|
current_folder_files = current_folder_files.slice(1)
|
||||||
}
|
}
|
||||||
for (let current_media of current_folder_files) {
|
for (let current_media of current_folder_files) {
|
||||||
|
@ -147,6 +148,3 @@ for (let i = 0; i < dirs.length; i++) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1 +1,33 @@
|
||||||
export const songs = ["../public/samples/FX/01 HTS Boom Kicks.ogg","../public/samples/FX/02 HTS Verbclaps.ogg","../public/samples/FX/03 HTS Noisesweep.ogg","../public/samples/FX/04 HTS Combined FX.ogg","../public/samples/bass/01 HTS Arp Bass.ogg","../public/samples/bass/02 HTS Hard Bass.ogg","../public/samples/bass/03 HTS Break Bass.ogg","../public/samples/bass/04 HTS Sub Bass.ogg","../public/samples/demos/01 demo 1 - Violet Delta & Eyhz.ogg","../public/samples/demos/02 demo 2 - Walras.ogg","../public/samples/demos/03 demo 3 - G4TE-16.ogg","../public/samples/demos/04 demo 4 - shadeux.ogg","../public/samples/demos/05 demo 5 - crowit.ogg","../public/samples/demos/06 demo 6 - sh0wtime.ogg","../public/samples/demos/07 demo 7 - nuphory & Luna Lenta.ogg","../public/samples/drums/01 HTS Rides and Hats.ogg","../public/samples/drums/02 HTS Claps and Hats.ogg","../public/samples/drums/03 HTS Club Snares.ogg","../public/samples/drums/04 HTS Buildup Snares.ogg","../public/samples/kicks/01 HTS Trancekick.ogg","../public/samples/kicks/02 HTS Sizzle Layer.ogg","../public/samples/kicks/03 HTS Transients.ogg","../public/samples/kicks/04 HTS Kick Combined.ogg","../public/samples/loops/01 HTS Loop CH.ogg","../public/samples/loops/02 HTS Loop OH.ogg","../public/samples/loops/03 HTS Perc Loop.ogg","../public/samples/loops/04 HTS Full Loop.ogg","../public/samples/synths/01 HTS Leads and Bass 01.ogg","../public/samples/synths/02 HTS Leads and Pads 01.ogg","../public/samples/synths/03 HTS Leads and Bass 02.ogg","../public/samples/synths/04 HTS Leads and Pads 02.ogg"]
|
export const songs = [
|
||||||
|
"../public/samples/FX/01 HTS Boom Kicks.ogg",
|
||||||
|
"../public/samples/FX/02 HTS Verbclaps.ogg",
|
||||||
|
"../public/samples/FX/03 HTS Noisesweep.ogg",
|
||||||
|
"../public/samples/FX/04 HTS Combined FX.ogg",
|
||||||
|
"../public/samples/bass/01 HTS Arp Bass.ogg",
|
||||||
|
"../public/samples/bass/02 HTS Hard Bass.ogg",
|
||||||
|
"../public/samples/bass/03 HTS Break Bass.ogg",
|
||||||
|
"../public/samples/bass/04 HTS Sub Bass.ogg",
|
||||||
|
"../public/samples/demos/01 demo 1 - Violet Delta & Eyhz.ogg",
|
||||||
|
"../public/samples/demos/02 demo 2 - Walras.ogg",
|
||||||
|
"../public/samples/demos/03 demo 3 - G4TE-16.ogg",
|
||||||
|
"../public/samples/demos/04 demo 4 - shadeux.ogg",
|
||||||
|
"../public/samples/demos/05 demo 5 - crowit.ogg",
|
||||||
|
"../public/samples/demos/06 demo 6 - sh0wtime.ogg",
|
||||||
|
"../public/samples/demos/07 demo 7 - nuphory & Luna Lenta.ogg",
|
||||||
|
"../public/samples/drums/01 HTS Rides and Hats.ogg",
|
||||||
|
"../public/samples/drums/02 HTS Claps and Hats.ogg",
|
||||||
|
"../public/samples/drums/03 HTS Club Snares.ogg",
|
||||||
|
"../public/samples/drums/04 HTS Buildup Snares.ogg",
|
||||||
|
"../public/samples/kicks/01 HTS Trancekick.ogg",
|
||||||
|
"../public/samples/kicks/02 HTS Sizzle Layer.ogg",
|
||||||
|
"../public/samples/kicks/03 HTS Transients.ogg",
|
||||||
|
"../public/samples/kicks/04 HTS Kick Combined.ogg",
|
||||||
|
"../public/samples/loops/01 HTS Loop CH.ogg",
|
||||||
|
"../public/samples/loops/02 HTS Loop OH.ogg",
|
||||||
|
"../public/samples/loops/03 HTS Perc Loop.ogg",
|
||||||
|
"../public/samples/loops/04 HTS Full Loop.ogg",
|
||||||
|
"../public/samples/synths/01 HTS Leads and Bass 01.ogg",
|
||||||
|
"../public/samples/synths/02 HTS Leads and Pads 01.ogg",
|
||||||
|
"../public/samples/synths/03 HTS Leads and Bass 02.ogg",
|
||||||
|
"../public/samples/synths/04 HTS Leads and Pads 02.ogg"
|
||||||
|
]
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import { MusicPlayerBuilder } from "@euterpe.js/player";
|
import { MusicPlayerBuilder } from "@euterpe.js/player"
|
||||||
import { fft_data } from "./waveform_data";
|
import { fft_data } from "./waveform_data"
|
||||||
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType, WaveformOrientation, WaveformShape } from "@euterpe.js/visualizer"
|
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType, WaveformOrientation, WaveformShape } from "@euterpe.js/visualizer"
|
||||||
|
|
||||||
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
const audio_el = document.querySelector("#audio") as HTMLAudioElement
|
||||||
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
const music_player_builder = new MusicPlayerBuilder(audio_el)
|
||||||
const trapnation_analyser_node = music_player_builder.add_analyser()
|
const trapnation_analyser_node = music_player_builder.add_analyser()
|
||||||
const bar_analyser_node = music_player_builder.add_analyser()
|
const bar_analyser_node = music_player_builder.add_analyser()
|
||||||
const music_player = music_player_builder.build()
|
const music_player = music_player_builder.build()
|
||||||
music_player.change_volume(.5)
|
music_player.change_volume(0.5)
|
||||||
|
|
||||||
const waveform_canvas = document.querySelector("#waveform-canvas") as SVGSVGElement
|
const waveform_canvas = document.querySelector("#waveform-canvas") as SVGSVGElement
|
||||||
const seek_element = document.querySelector("#seek") as HTMLInputElement
|
const seek_element = document.querySelector("#seek") as HTMLInputElement
|
||||||
const duration_element = document.querySelector("#duration") as HTMLElement
|
const duration_element = document.querySelector("#duration") as HTMLElement
|
||||||
const current_time_element = document.querySelector("#current") as HTMLElement
|
const current_time_element = document.querySelector("#current") as HTMLElement
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the Audio Visualizer
|
* Create the Audio Visualizer
|
||||||
*/
|
*/
|
||||||
|
@ -19,25 +21,44 @@ const trapnation_visual_builder = new AudioVisualBuilder(trapnation_analyser_nod
|
||||||
//Because the to_fft_range is so low, it needs more FFT data.
|
//Because the to_fft_range is so low, it needs more FFT data.
|
||||||
.set_fft_size(8192)
|
.set_fft_size(8192)
|
||||||
//Tells the Visualiser how to parse data which mutates our initial shape
|
//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_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)
|
.set_fft_time_smoothing(0.6)
|
||||||
//If not using typescript enums, CatmullRom = number 2
|
|
||||||
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
||||||
|
|
||||||
const trapnation_visual = trapnation_visual_builder.build(ShapeType.Circle, false)
|
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)
|
const bar_visual_builder = new AudioVisualBuilder(bar_analyser_node, document.querySelector("#bar-canvas") as SVGSVGElement)
|
||||||
.set_fft_data_tresholds({ point_count_i: 50, fft_multiplier_i: 2, fft_offset_i: -100 })
|
.set_fft_size(4096)
|
||||||
.set_fft_time_smoothing(0.8)
|
.set_fft_data_tresholds({
|
||||||
|
point_count_i: 20,
|
||||||
|
fft_multiplier_i: 3,
|
||||||
|
fft_offset_i: -130
|
||||||
|
})
|
||||||
|
.set_fft_time_smoothing(0.5)
|
||||||
.set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular)
|
.set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular)
|
||||||
|
|
||||||
|
//currently broken, duno why...
|
||||||
const bar_visual = bar_visual_builder.build(ShapeType.Line, false)
|
const bar_visual = bar_visual_builder.build(ShapeType.Line, false)
|
||||||
|
|
||||||
const waveform_visual_builder = new AudioVisualBuilder(bar_analyser_node, waveform_canvas)
|
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 })
|
.set_fft_data_tresholds({
|
||||||
|
point_count_i: 100,
|
||||||
|
fft_multiplier_i: 1,
|
||||||
|
fft_offset_i: -50
|
||||||
|
})
|
||||||
.set_fft_time_smoothing(0.8)
|
.set_fft_time_smoothing(0.8)
|
||||||
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
.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, shape_type: WaveformShape.LineLike })
|
|
||||||
|
|
||||||
|
|
||||||
|
const waveform_visual = waveform_visual_builder.build(ShapeType.Waveform, true, {
|
||||||
|
fft_data: new Float32Array(fft_data.fft_data),
|
||||||
|
orientation: WaveformOrientation.Horizontal,
|
||||||
|
shape_type: WaveformShape.LineLike
|
||||||
|
})
|
||||||
|
|
||||||
trapnation_visual.draw()
|
trapnation_visual.draw()
|
||||||
bar_visual.draw()
|
bar_visual.draw()
|
||||||
|
@ -83,9 +104,11 @@ waveform_canvas.appendChild(seek_clip_use)
|
||||||
seek_clip_path.appendChild(seek_clip_rect)
|
seek_clip_path.appendChild(seek_clip_rect)
|
||||||
time_clip_path.appendChild(time_clip_rect)
|
time_clip_path.appendChild(time_clip_rect)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function convert_range(value: number, r1: number[], r2: number[]) {
|
function convert_range(value: number, r1: number[], r2: number[]) {
|
||||||
return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0]
|
return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
waveform_canvas.addEventListener("mousemove", (e) => {
|
waveform_canvas.addEventListener("mousemove", (e) => {
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
const x = e.clientX - rect.left
|
const x = e.clientX - rect.left
|
||||||
|
@ -93,61 +116,86 @@ waveform_canvas.addEventListener("mousemove", (e) => {
|
||||||
const polygon = `polygon(0 0, ${resX}px 0, ${resX}px 100%, 0 100%)`
|
const polygon = `polygon(0 0, ${resX}px 0, ${resX}px 100%, 0 100%)`
|
||||||
document.documentElement.style.setProperty("--clip-seek-path", polygon)
|
document.documentElement.style.setProperty("--clip-seek-path", polygon)
|
||||||
})
|
})
|
||||||
|
|
||||||
waveform_canvas.addEventListener("mouseleave", (e) => {
|
waveform_canvas.addEventListener("mouseleave", (e) => {
|
||||||
const polygon = `polygon(0 0, 0 0, 0 100%, 0 100%)`
|
const polygon = `polygon(0 0, 0 0, 0 100%, 0 100%)`
|
||||||
document.documentElement.style.setProperty("--clip-seek-path", polygon)
|
document.documentElement.style.setProperty("--clip-seek-path", polygon)
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The player part
|
* The player part
|
||||||
*/
|
*/
|
||||||
music_player.try_new_song_async(encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg"))
|
|
||||||
.then(() => {
|
|
||||||
let is_seeking = false
|
let is_seeking = false
|
||||||
|
|
||||||
document.querySelector("#play")?.addEventListener("click", () => {
|
document.querySelector("#play")?.addEventListener("click", () => {
|
||||||
music_player.play_async()
|
music_player.try_play().then(
|
||||||
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
|
() => {
|
||||||
|
console.log("Playing!")
|
||||||
|
},
|
||||||
|
(e) => alert("Failed to play, " + e)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#pause")?.addEventListener("click", () => {
|
document.querySelector("#pause")?.addEventListener("click", () => {
|
||||||
music_player.pause()
|
music_player.pause()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#mute")?.addEventListener("click", () => {
|
document.querySelector("#mute")?.addEventListener("click", () => {
|
||||||
music_player.mute()
|
music_player.mute()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#unmute")?.addEventListener("click", () => {
|
document.querySelector("#unmute")?.addEventListener("click", () => {
|
||||||
music_player.unmute()
|
music_player.unmute()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
|
||||||
music_player.mute_toggle()
|
music_player.mute_toggle()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#toggle-play")?.addEventListener("click", () => {
|
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))
|
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) => {
|
document.querySelector("#volume")?.addEventListener("input", (e) => {
|
||||||
music_player.change_volume(e.target?.valueAsNumber)
|
music_player.change_volume(e.target?.valueAsNumber)
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
|
||||||
is_seeking = true;
|
is_seeking = true
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
|
||||||
music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => {
|
music_player.try_seek(e.target?.valueAsNumber).then(
|
||||||
|
() => {
|
||||||
|
console.log("seeked to " + e.target?.valueAsNumber)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
alert("Failed seeking! " + e)
|
alert("Failed seeking! " + e)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
is_seeking = false
|
is_seeking = false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscriptions to AudioContext changes, eg. time..
|
// Subscriptions to AudioContext changes, eg. time..
|
||||||
music_player.on_duration_formatted((time) => {
|
music_player.on_duration_formatted((time) => {
|
||||||
duration_element.innerHTML = time
|
duration_element.innerHTML = time
|
||||||
seek_element.max = "" + music_player.current_song_duration
|
seek_element.max = "" + music_player.current_song_duration
|
||||||
})
|
})
|
||||||
|
|
||||||
music_player.on_time_tick_formatted((time) => {
|
music_player.on_time_tick_formatted((time) => {
|
||||||
current_time_element.innerHTML = time
|
current_time_element.innerHTML = time
|
||||||
})
|
})
|
||||||
|
|
||||||
music_player.on_time_tick((time) => {
|
music_player.on_time_tick((time) => {
|
||||||
if (is_seeking) return
|
if (is_seeking) return
|
||||||
seek_element.value = "" + time
|
seek_element.value = "" + time
|
||||||
const x = `${time / music_player.current_song_duration * 100}%`
|
const x = `${(time / music_player.current_song_duration) * 100}%`
|
||||||
const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)`
|
const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)`
|
||||||
document.documentElement.style.setProperty("--clip-time-path", polygon)
|
document.documentElement.style.setProperty("--clip-time-path", polygon)
|
||||||
})
|
})
|
||||||
|
|
||||||
}, (e) => console.log(e))
|
music_player.try_new_song(encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg"))
|
||||||
|
|
|
@ -31,9 +31,9 @@ body {
|
||||||
}
|
}
|
||||||
.waveform-time {
|
.waveform-time {
|
||||||
clip-path: var(--clip-time-path);
|
clip-path: var(--clip-time-path);
|
||||||
fill: rgba(50,145,200,0.3);
|
fill: rgba(0,255,50,0.5);
|
||||||
}
|
}
|
||||||
.waveform-seek {
|
.waveform-seek {
|
||||||
clip-path: var(--clip-seek-path);
|
clip-path: var(--clip-seek-path);
|
||||||
fill: rgba(50,145,200,0.3);
|
fill: rgba(0,50,255,0.5);
|
||||||
}
|
}
|
|
@ -1,14 +1,11 @@
|
||||||
|
//To see how to get these, check the preprocessor repo!
|
||||||
export const fft_data = {
|
export const fft_data = {
|
||||||
"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,
|
-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,
|
||||||
-96.32, -99.23, -103.13, -85.57, -98.17, -103.27, -107.5, -83.62, -95.23, -97.12, -94.78,
|
-107.5, -83.62, -95.23, -97.12, -94.78, -95.93, -101.42, -97.83, -102.42, -111.74, -101.38, -106.8, -111.05, -88.04, -90.88, -97.67, -96.31,
|
||||||
-95.93, -101.42, -97.83, -102.42, -111.74, -101.38, -106.8, -111.05, -88.04, -90.88, -97.67,
|
-96.69, -102.15, -102.03, -100.51, -107.14, -101.48, -101.6, -106.62, -73.94, -79.53, -92.74, -96.08, -96.26, -100.35, -99.13, -102.03,
|
||||||
-96.31, -96.69, -102.15, -102.03, -100.51, -107.14, -101.48, -101.6, -106.62, -73.94,
|
-107.4, -93.57, -102.31, -102.3, -109.04, -81.85, -92.79, -100.06, -95.79, -96.49, -99.89, -100.27, -102.69, -107.35, -103.94, -104.64,
|
||||||
-79.53, -92.74, -96.08, -96.26, -100.35, -99.13, -102.03, -107.4, -93.57, -102.31, -102.3,
|
-104.3, -78.82, -84.2, -95.29, -92.57, -93.47, -98.08, -98.9, -101.56, -109.38, -102.01, -102.51, -104.83, -72.18, -76.52, -91.69, -99.97,
|
||||||
-109.04, -81.85, -92.79, -100.06, -95.79, -96.49, -99.89, -100.27, -102.69, -107.35,
|
-96.63, -98.61, -76.97, -90.41, -100.38, -106.77, -102.83, -104.46, -108.59, -80.97, -88.05, -100.77, -79.64, -72.3, -87.96, -92.89, -93.03
|
||||||
-103.94, -104.64, -104.3, -78.82, -84.2, -95.29, -92.57, -93.47, -98.08, -98.9, -101.56,
|
]
|
||||||
-109.38, -102.01, -102.51, -104.83, -72.18, -76.52, -91.69, -99.97, -96.63, -98.61, -76.97,
|
|
||||||
-90.41, -100.38, -106.77, -102.83, -104.46, -108.59, -80.97, -88.05, -100.77, -79.64, -72.3,
|
|
||||||
-87.96, -92.89, -93.03
|
|
||||||
],
|
|
||||||
}
|
}
|
|
@ -5,7 +5,8 @@ Euterpe Visualizer is a unique audio visual library for the web. Using SVG eleme
|
||||||
### How to use
|
### 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:
|
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"
|
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer"
|
||||||
// Refrence to an <audio id="audio"></audio> element inside your HTML
|
// Refrence to an <audio id="audio"></audio> element inside your HTML
|
||||||
const audio_element = document.querySelector("#audio") as HTMLAudioElement
|
const audio_element = document.querySelector("#audio") as HTMLAudioElement
|
||||||
|
@ -15,8 +16,10 @@ const track = audio_context.createMediaElementSource(audio_element)
|
||||||
const analyzer = audio_context.createAnalyser()
|
const analyzer = audio_context.createAnalyser()
|
||||||
track.connect(analyzer).connect(audio_context.destination)
|
track.connect(analyzer).connect(audio_context.destination)
|
||||||
```
|
```
|
||||||
|
|
||||||
Now that the AudioContext is ready, we start constructing our Visualizer
|
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
|
// For more options during the building process, I recommend reading the docs
|
||||||
/* Refrence an
|
/* Refrence an
|
||||||
* "<svg id="canvas" viewBox="0 0 500 500" preserveAspectRatio="none" ></svg>"
|
* "<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)
|
const visualizer = new AudioVisualBuilder(analyser_node, document.querySelector("#canvas") as SVGSVGElement)
|
||||||
.build(ShapeType.Circle)
|
.build(ShapeType.Circle)
|
||||||
```
|
```
|
||||||
|
|
||||||
And it's ready! Once the audio context starts playing, the visualizer should start generating the path inside our SVG element.
|
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()
|
visualizer.draw()
|
||||||
audio_element.play()
|
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.
|
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
|
#### 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).
|
##### 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)
|
First we need to create our [Euterpe Player](https://www.npmjs.com/package/@euterpe.js/player)
|
||||||
```js
|
|
||||||
|
```ts
|
||||||
import { MusicPlayerBuilder } from "@euterpe/player";
|
import { MusicPlayerBuilder } from "@euterpe/player";
|
||||||
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer"
|
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()
|
const music_player = music_player_builder.build()
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now that the AudioContext is ready, we start constructing our Visualizer
|
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
|
// For more options during the building process, I recommend reading the docs
|
||||||
/* Refrence an
|
/* Refrence an
|
||||||
* "<svg id="canvas" viewBox="0 0 500 500" preserveAspectRatio="none" ></svg>"
|
* "<svg id="canvas" viewBox="0 0 500 500" preserveAspectRatio="none" ></svg>"
|
||||||
|
@ -62,17 +70,18 @@ const visual_builder = new AudioVisualBuilder(analyzer_node, document.querySelec
|
||||||
//Tells the Visualiser how to parse data which mutates our initial shape
|
//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_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)
|
.set_fft_time_smoothing(0.6)
|
||||||
//If not using typescript enums, CatmullRom = number 2
|
|
||||||
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
|
||||||
|
|
||||||
const visualizer = visual_builder.build(ShapeType.Circle)
|
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.
|
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()
|
visualizer.draw()
|
||||||
|
|
||||||
music_player.try_new_song_async(url)
|
music_player.try_new_song(url)
|
||||||
.then( music_player.play_async()
|
.then( music_player.try_play()
|
||||||
.then(
|
.then(
|
||||||
console.log("It's working!")
|
console.log("It's working!")
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@euterpe.js/visualizer",
|
"name": "@euterpe.js/visualizer",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Music visualizer based on SVG and AudioContext",
|
"description": "Music visualizer based on SVG and AudioContext",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
|
|
|
@ -2,12 +2,12 @@ export enum SmoothingAlgorythm {
|
||||||
Linear,
|
Linear,
|
||||||
BezierPerpendicular,
|
BezierPerpendicular,
|
||||||
CatmullRom,
|
CatmullRom,
|
||||||
BezierWeighted,
|
BezierWeighted
|
||||||
}
|
}
|
||||||
export enum ShapeType {
|
export enum ShapeType {
|
||||||
Circle,
|
Circle,
|
||||||
Line,
|
Line,
|
||||||
Waveform,
|
Waveform
|
||||||
/*To be Implmeneted
|
/*To be Implmeneted
|
||||||
Custom,
|
Custom,
|
||||||
FullSongWaveForm
|
FullSongWaveForm
|
||||||
|
@ -15,23 +15,23 @@ export enum ShapeType {
|
||||||
}
|
}
|
||||||
export enum WaveformOrientation {
|
export enum WaveformOrientation {
|
||||||
Vertical,
|
Vertical,
|
||||||
Horizontal,
|
Horizontal
|
||||||
}
|
}
|
||||||
export enum WaveformShape {
|
export enum WaveformShape {
|
||||||
LineLike,
|
LineLike,
|
||||||
Striped,
|
Striped
|
||||||
}
|
}
|
||||||
export type WaveformOptions = {
|
export type WaveformOptions = {
|
||||||
fft_data: Float32Array,
|
fft_data: Float32Array
|
||||||
shape_type: WaveformShape,
|
shape_type: WaveformShape
|
||||||
orientation: WaveformOrientation
|
orientation: WaveformOrientation
|
||||||
}
|
}
|
||||||
type Point = {
|
type Point = {
|
||||||
x: number,
|
x: number
|
||||||
y: number,
|
y: number
|
||||||
}
|
}
|
||||||
type Shape = {
|
type Shape = {
|
||||||
shape_type: ShapeType,
|
shape_type: ShapeType
|
||||||
//Algo-rythm, because this is about music. Get it? xd
|
//Algo-rythm, because this is about music. Get it? xd
|
||||||
smoothing_algorythm: SmoothingAlgorythm
|
smoothing_algorythm: SmoothingAlgorythm
|
||||||
points: Point[]
|
points: Point[]
|
||||||
|
@ -62,7 +62,7 @@ export class AudioVisual {
|
||||||
fft_offset: number,
|
fft_offset: number,
|
||||||
from_fft_range: number,
|
from_fft_range: number,
|
||||||
to_fft_range: number,
|
to_fft_range: number,
|
||||||
point_count: number,
|
point_count: number
|
||||||
) {
|
) {
|
||||||
this.#analyzer_node = analyzer_node
|
this.#analyzer_node = analyzer_node
|
||||||
this.#svg_injecting_element = svg_injecting_element
|
this.#svg_injecting_element = svg_injecting_element
|
||||||
|
@ -104,7 +104,7 @@ export class AudioVisual {
|
||||||
return [x / magnitude, y / magnitude]
|
return [x / magnitude, y / magnitude]
|
||||||
}
|
}
|
||||||
|
|
||||||
#create_perpendicular_anchors(arr: { x: number, y: number }[]) {
|
#create_perpendicular_anchors(arr: { x: number; y: number }[]) {
|
||||||
const anchors = []
|
const anchors = []
|
||||||
switch (this.#shape.shape_type) {
|
switch (this.#shape.shape_type) {
|
||||||
case ShapeType.Circle: {
|
case ShapeType.Circle: {
|
||||||
|
@ -115,12 +115,12 @@ export class AudioVisual {
|
||||||
anchors.push({
|
anchors.push({
|
||||||
leftAnchor: {
|
leftAnchor: {
|
||||||
x: arr[curPoint].x + pointDistance * perpendicular[0],
|
x: arr[curPoint].x + pointDistance * perpendicular[0],
|
||||||
y: arr[curPoint].y + pointDistance * perpendicular[1],
|
y: arr[curPoint].y + pointDistance * perpendicular[1]
|
||||||
},
|
},
|
||||||
rightAnchor: {
|
rightAnchor: {
|
||||||
x: arr[curPoint].x - pointDistance * perpendicular[0],
|
x: arr[curPoint].x - pointDistance * perpendicular[0],
|
||||||
y: arr[curPoint].y - pointDistance * perpendicular[1],
|
y: arr[curPoint].y - pointDistance * perpendicular[1]
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -131,12 +131,12 @@ export class AudioVisual {
|
||||||
anchors.push({
|
anchors.push({
|
||||||
leftAnchor: {
|
leftAnchor: {
|
||||||
x: pointDistance * curPoint - pointDistance / 3,
|
x: pointDistance * curPoint - pointDistance / 3,
|
||||||
y: arr[curPoint].y,
|
y: arr[curPoint].y
|
||||||
},
|
},
|
||||||
rightAnchor: {
|
rightAnchor: {
|
||||||
x: pointDistance * curPoint + pointDistance / 3,
|
x: pointDistance * curPoint + pointDistance / 3,
|
||||||
y: arr[curPoint].y,
|
y: arr[curPoint].y
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,13 +145,12 @@ export class AudioVisual {
|
||||||
return anchors
|
return anchors
|
||||||
}
|
}
|
||||||
|
|
||||||
#catmull_rom_smooth(arr: { x: number, y: number }[], k: number) {
|
#catmull_rom_smooth(arr: { x: number; y: number }[], k: number) {
|
||||||
if (k == null) k = 1
|
if (k == null) k = 1
|
||||||
const last = arr.length - 2
|
const last = arr.length - 2
|
||||||
let path = ""
|
let path = ""
|
||||||
|
|
||||||
for (let i = 0; i < arr.length - 1; i++) {
|
for (let i = 0; i < arr.length - 1; i++) {
|
||||||
|
|
||||||
const x0 = i ? arr[i - 1].x : arr[0].x
|
const x0 = i ? arr[i - 1].x : arr[0].x
|
||||||
const y0 = i ? arr[i - 1].y : arr[0].y
|
const y0 = i ? arr[i - 1].y : arr[0].y
|
||||||
|
|
||||||
|
@ -171,11 +170,11 @@ export class AudioVisual {
|
||||||
const x3 = i !== last ? arr[i + 2].x : subx
|
const x3 = i !== last ? arr[i + 2].x : subx
|
||||||
const y3 = i !== last ? arr[i + 2].y : suby
|
const y3 = i !== last ? arr[i + 2].y : suby
|
||||||
|
|
||||||
const cp1x = x1 + (x2 - x0) / 6 * k
|
const cp1x = x1 + ((x2 - x0) / 6) * k
|
||||||
const cp1y = y1 + (y2 - y0) / 6 * k
|
const cp1y = y1 + ((y2 - y0) / 6) * k
|
||||||
|
|
||||||
const cp2x = x2 - (x3 - x1) / 6 * k
|
const cp2x = x2 - ((x3 - x1) / 6) * k
|
||||||
const cp2y = y2 - (y3 - y1) / 6 * k
|
const cp2y = y2 - ((y3 - y1) / 6) * k
|
||||||
|
|
||||||
path += "C" + [cp1x.toFixed(2), cp1y.toFixed(2), cp2x.toFixed(2), cp2y.toFixed(2), x2.toFixed(2), y2.toFixed(2)]
|
path += "C" + [cp1x.toFixed(2), cp1y.toFixed(2), cp2x.toFixed(2), cp2y.toFixed(2), x2.toFixed(2), y2.toFixed(2)]
|
||||||
}
|
}
|
||||||
|
@ -190,10 +189,12 @@ export class AudioVisual {
|
||||||
switch (this.#shape.shape_type) {
|
switch (this.#shape.shape_type) {
|
||||||
case ShapeType.Line: {
|
case ShapeType.Line: {
|
||||||
for (let i = 0; i < frequency_data.length - 1; i++) {
|
for (let i = 0; i < frequency_data.length - 1; i++) {
|
||||||
const mutator = isFinite(frequency_data[i]) ? this.#convert_range(frequency_data[i] * this.#fft_multiplier + this.#fft_offset, in_range, out_range) : -1 * this.#canvas_height
|
const mutator = isFinite(frequency_data[i])
|
||||||
|
? this.#convert_range(frequency_data[i] * this.#fft_multiplier + this.#fft_offset, in_range, out_range)
|
||||||
|
: -1 * this.#canvas_height
|
||||||
mutated_points.push({
|
mutated_points.push({
|
||||||
x: this.#shape.points[i].x /** ((Math.max(FFTDataArray[i] + 100)) * 4)*/,
|
x: this.#shape.points[i].x /** ((Math.max(FFTDataArray[i] + 100)) * 4)*/,
|
||||||
y: this.#shape.points[i].y - mutator,
|
y: this.#shape.points[i].y - mutator
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -202,8 +203,12 @@ export class AudioVisual {
|
||||||
for (let i = 0; i < frequency_data.length - 1; i++) {
|
for (let i = 0; i < frequency_data.length - 1; i++) {
|
||||||
const new_i = i > (frequency_data.length - 1) / 2 ? frequency_data.length - 1 - i : i
|
const new_i = i > (frequency_data.length - 1) / 2 ? frequency_data.length - 1 - i : i
|
||||||
mutated_points.push({
|
mutated_points.push({
|
||||||
x: this.#shape.points[i].x * Math.max((frequency_data[new_i] * this.#fft_multiplier + this.#fft_offset) / 50, 1) + this.#canvas_width / 2,
|
x:
|
||||||
y: this.#shape.points[i].y * Math.max((frequency_data[new_i] * this.#fft_multiplier + this.#fft_offset) / 50, 1) + this.#canvas_height / 2,
|
this.#shape.points[i].x * Math.max((frequency_data[new_i] * this.#fft_multiplier + this.#fft_offset) / 50, 1) +
|
||||||
|
this.#canvas_width / 2,
|
||||||
|
y:
|
||||||
|
this.#shape.points[i].y * Math.max((frequency_data[new_i] * this.#fft_multiplier + this.#fft_offset) / 50, 1) +
|
||||||
|
this.#canvas_height / 2
|
||||||
})
|
})
|
||||||
/* TODO: IMPLEMENT SCALING TO BEAT
|
/* 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})`
|
||||||
|
@ -291,10 +296,7 @@ export class AudioVisual {
|
||||||
for (let i = 0; i < arr.length; i++) {
|
for (let i = 0; i < arr.length; i++) {
|
||||||
path += `L ${arr[i].x.toFixed(2)},${arr[i].y.toFixed(2)} `
|
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 ${this.#canvas_width} ${this.#canvas_height} `
|
||||||
//path += `L ${canvas_width} ${canvas_height} `
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ShapeType.Circle: {
|
case ShapeType.Circle: {
|
||||||
|
@ -320,7 +322,9 @@ export class AudioVisual {
|
||||||
const anchors = this.#create_perpendicular_anchors(arr)
|
const anchors = this.#create_perpendicular_anchors(arr)
|
||||||
|
|
||||||
for (let i = 1; i < arr.length; i++) {
|
for (let i = 1; i < arr.length; i++) {
|
||||||
path += `C ${anchors[i - 1].rightAnchor.x.toFixed(2)} ${anchors[i - 1].rightAnchor.y.toFixed(2)} ${anchors[i].leftAnchor.x.toFixed(2)} ${anchors[i].leftAnchor.y.toFixed(2)} ${arr[i].x.toFixed(2)} ${arr[i].y.toFixed(2)} `
|
path += `C ${anchors[i - 1].rightAnchor.x.toFixed(2)} ${anchors[i - 1].rightAnchor.y.toFixed(2)} ${anchors[
|
||||||
|
i
|
||||||
|
].leftAnchor.x.toFixed(2)} ${anchors[i].leftAnchor.y.toFixed(2)} ${arr[i].x.toFixed(2)} ${arr[i].y.toFixed(2)} `
|
||||||
}
|
}
|
||||||
if (this.#shape.shape_type == ShapeType.Line) {
|
if (this.#shape.shape_type == ShapeType.Line) {
|
||||||
//path += `L ${this.canvasWidth} ${this.canvasHeight / 2} `
|
//path += `L ${this.canvasWidth} ${this.canvasHeight / 2} `
|
||||||
|
@ -349,8 +353,14 @@ export class AudioVisual {
|
||||||
first_half.push(arr[i])
|
first_half.push(arr[i])
|
||||||
second_half.push(arr[i + 1])
|
second_half.push(arr[i + 1])
|
||||||
}
|
}
|
||||||
first_half.push({ x: this.#canvas_width, y: this.#canvas_height / 2 })
|
first_half.push({
|
||||||
second_half.push({ x: this.#canvas_width, y: this.#canvas_height / 2 })
|
x: this.#canvas_width,
|
||||||
|
y: this.#canvas_height / 2
|
||||||
|
})
|
||||||
|
second_half.push({
|
||||||
|
x: this.#canvas_width,
|
||||||
|
y: this.#canvas_height / 2
|
||||||
|
})
|
||||||
// path += `M ${this.#canvas_width},${this.#canvas_height / 2}`
|
// path += `M ${this.#canvas_width},${this.#canvas_height / 2}`
|
||||||
second_half.reverse()
|
second_half.reverse()
|
||||||
//path += ` L 0 ${this.#canvas_height / 2}`
|
//path += ` L 0 ${this.#canvas_height / 2}`
|
||||||
|
@ -358,8 +368,7 @@ export class AudioVisual {
|
||||||
//path += ` L ${this.#canvas_width} ${this.#canvas_height / 2}`
|
//path += ` L ${this.#canvas_width} ${this.#canvas_height / 2}`
|
||||||
path += this.#catmull_rom_smooth(second_half, 1)
|
path += this.#catmull_rom_smooth(second_half, 1)
|
||||||
//path += `L 0 ${this.#canvas_height / 2}`
|
//path += `L 0 ${this.#canvas_height / 2}`
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
path += this.#catmull_rom_smooth(arr, 1)
|
path += this.#catmull_rom_smooth(arr, 1)
|
||||||
}
|
}
|
||||||
path += `Z`
|
path += `Z`
|
||||||
|
@ -369,7 +378,7 @@ export class AudioVisual {
|
||||||
return `<path width="100%" height="100%" d="${path}"/>`
|
return `<path width="100%" height="100%" d="${path}"/>`
|
||||||
}
|
}
|
||||||
|
|
||||||
on_data(fn: ((data: Float32Array) => void)) {
|
on_data(fn: (data: Float32Array) => void) {
|
||||||
this.#subscriber_fns.push(fn)
|
this.#subscriber_fns.push(fn)
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -461,7 +470,13 @@ export class AudioVisualBuilder {
|
||||||
* @param param0
|
* @param param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
set_fft_data_tresholds({ from_fft_range_i = 0, to_fft_range_i = 100, point_count_i = Math.round((this.#buffer_length / 100) * (from_fft_range_i - to_fft_range_i)), fft_multiplier_i = 2, fft_offset_i = -50 }) {
|
set_fft_data_tresholds({
|
||||||
|
from_fft_range_i = 0,
|
||||||
|
to_fft_range_i = 100,
|
||||||
|
point_count_i = Math.round((this.#buffer_length / 100) * (from_fft_range_i - to_fft_range_i)),
|
||||||
|
fft_multiplier_i = 2,
|
||||||
|
fft_offset_i = -50
|
||||||
|
}) {
|
||||||
this.#from_fft_range = from_fft_range_i
|
this.#from_fft_range = from_fft_range_i
|
||||||
this.#to_fft_range = to_fft_range_i
|
this.#to_fft_range = to_fft_range_i
|
||||||
this.#point_count = point_count_i
|
this.#point_count = point_count_i
|
||||||
|
@ -501,7 +516,17 @@ export class AudioVisualBuilder {
|
||||||
*/
|
*/
|
||||||
build(shape_type: ShapeType, symmetry: boolean, waveform_options?: WaveformOptions) {
|
build(shape_type: ShapeType, symmetry: boolean, waveform_options?: WaveformOptions) {
|
||||||
const shape = this.#create_shape(shape_type, symmetry, waveform_options)
|
const shape = this.#create_shape(shape_type, symmetry, waveform_options)
|
||||||
return new AudioVisual(this.#analyzer_node, this.#svg_injecting_element, shape, this.#buffer_length, this.#fft_multipier, this.#fft_offset, this.#from_fft_range, this.#to_fft_range, this.#point_count)
|
return new AudioVisual(
|
||||||
|
this.#analyzer_node,
|
||||||
|
this.#svg_injecting_element,
|
||||||
|
shape,
|
||||||
|
this.#buffer_length,
|
||||||
|
this.#fft_multipier,
|
||||||
|
this.#fft_offset,
|
||||||
|
this.#from_fft_range,
|
||||||
|
this.#to_fft_range,
|
||||||
|
this.#point_count
|
||||||
|
)
|
||||||
}
|
}
|
||||||
#create_shape(shape_type: ShapeType, symmetry: boolean, waveform_options?: WaveformOptions): Shape {
|
#create_shape(shape_type: ShapeType, symmetry: boolean, waveform_options?: WaveformOptions): Shape {
|
||||||
const point_amount = this.#get_cured_frequency_data().length
|
const point_amount = this.#get_cured_frequency_data().length
|
||||||
|
@ -512,10 +537,14 @@ export class AudioVisualBuilder {
|
||||||
for (let i = 0; i < point_amount; i++) {
|
for (let i = 0; i < point_amount; i++) {
|
||||||
points.push({
|
points.push({
|
||||||
x: (this.#canvas_width / point_amount) * i,
|
x: (this.#canvas_width / point_amount) * i,
|
||||||
y: 0,
|
y: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
new_shape = { shape_type, points, smoothing_algorythm: this.#smoothing_algorythm }
|
new_shape = {
|
||||||
|
shape_type,
|
||||||
|
points,
|
||||||
|
smoothing_algorythm: this.#smoothing_algorythm
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ShapeType.Circle: {
|
case ShapeType.Circle: {
|
||||||
|
@ -524,11 +553,15 @@ export class AudioVisualBuilder {
|
||||||
for (let i = 0; i < point_amount; i++) {
|
for (let i = 0; i < point_amount; i++) {
|
||||||
points.push({
|
points.push({
|
||||||
x: Math.cos(((2 * Math.PI) / point_amount) * i - Math.PI / 2) * radius,
|
x: Math.cos(((2 * Math.PI) / point_amount) * i - Math.PI / 2) * radius,
|
||||||
y: Math.sin(((2 * Math.PI) / point_amount) * i - Math.PI / 2) * radius,
|
y: Math.sin(((2 * Math.PI) / point_amount) * i - Math.PI / 2) * radius
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
new_shape = { shape_type, points, smoothing_algorythm: this.#smoothing_algorythm }
|
new_shape = {
|
||||||
|
shape_type,
|
||||||
|
points,
|
||||||
|
smoothing_algorythm: this.#smoothing_algorythm
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ShapeType.Waveform: {
|
case ShapeType.Waveform: {
|
||||||
|
@ -546,17 +579,19 @@ export class AudioVisualBuilder {
|
||||||
} else {
|
} else {
|
||||||
throw Error("WaveformShape.Striped not implemented yet")
|
throw Error("WaveformShape.Striped not implemented yet")
|
||||||
}
|
}
|
||||||
waveform_options.orientation == WaveformOrientation.Horizontal ?
|
waveform_options.orientation == WaveformOrientation.Horizontal ? points.push({ x: x, y: y }) : points.push({ x: y, y: x })
|
||||||
points.push({ x: x, y: y }) :
|
|
||||||
points.push({ x: y, y: x })
|
|
||||||
//Douple the points needed for symmetry
|
//Douple the points needed for symmetry
|
||||||
if (symmetry) {
|
if (symmetry) {
|
||||||
waveform_options.orientation == WaveformOrientation.Horizontal ?
|
waveform_options.orientation == WaveformOrientation.Horizontal ? points.push({ x: x, y: y }) : points.push({ x: y, y: x })
|
||||||
points.push({ x: x, y: y }) :
|
|
||||||
points.push({ x: y, y: x })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_shape = { shape_type, points, smoothing_algorythm: this.#smoothing_algorythm, symmetry: symmetry, waveform_options: waveform_options }
|
new_shape = {
|
||||||
|
shape_type,
|
||||||
|
points,
|
||||||
|
smoothing_algorythm: this.#smoothing_algorythm,
|
||||||
|
symmetry: symmetry,
|
||||||
|
waveform_options: waveform_options
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new_shape
|
return new_shape
|
||||||
|
@ -577,5 +612,4 @@ export class AudioVisualBuilder {
|
||||||
}
|
}
|
||||||
return return_array
|
return return_array
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
13
test.js
13
test.js
|
@ -20,8 +20,8 @@ class AudioElementHehe {
|
||||||
console.log("Pausing!")
|
console.log("Pausing!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const audio_context = new AudioContexthehe
|
const audio_context = new AudioContexthehe()
|
||||||
const audio_element = new AudioElementHehe
|
const audio_element = new AudioElementHehe()
|
||||||
let is_playing = false
|
let is_playing = false
|
||||||
try_play_toggle_async()
|
try_play_toggle_async()
|
||||||
|
|
||||||
|
@ -31,13 +31,16 @@ function try_play_toggle_async() {
|
||||||
audio_context.resume().catch((e) => reject(e))
|
audio_context.resume().catch((e) => reject(e))
|
||||||
}
|
}
|
||||||
if (audio_element.paused) {
|
if (audio_element.paused) {
|
||||||
audio_element.play().then((s) => {
|
audio_element.play().then(
|
||||||
|
(s) => {
|
||||||
is_playing = true
|
is_playing = true
|
||||||
resolve(s)
|
resolve(s)
|
||||||
}, (r) => {
|
},
|
||||||
|
(r) => {
|
||||||
is_playing = false
|
is_playing = false
|
||||||
reject(r)
|
reject(r)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
audio_element.pause()
|
audio_element.pause()
|
||||||
is_playing = false
|
is_playing = false
|
||||||
|
|
|
@ -35,16 +35,10 @@ invariant(
|
||||||
const graph = readCachedProjectGraph()
|
const graph = readCachedProjectGraph()
|
||||||
const project = graph.nodes[name]
|
const project = graph.nodes[name]
|
||||||
|
|
||||||
invariant(
|
invariant(project, `Could not find project "${name}" in the workspace. Is the project.json configured correctly?`)
|
||||||
project,
|
|
||||||
`Could not find project "${name}" in the workspace. Is the project.json configured correctly?`
|
|
||||||
)
|
|
||||||
|
|
||||||
const outputPath = project.data?.targets?.build?.options?.outputPath
|
const outputPath = project.data?.targets?.build?.options?.outputPath
|
||||||
invariant(
|
invariant(outputPath, `Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?`)
|
||||||
outputPath,
|
|
||||||
`Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?`
|
|
||||||
)
|
|
||||||
|
|
||||||
process.chdir(outputPath)
|
process.chdir(outputPath)
|
||||||
|
|
||||||
|
@ -54,11 +48,7 @@ try {
|
||||||
json.version = version
|
json.version = version
|
||||||
writeFileSync(`package.json`, JSON.stringify(json, null, 2))
|
writeFileSync(`package.json`, JSON.stringify(json, null, 2))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(chalk.bold.red(`Error reading package.json file from library build output.`))
|
||||||
chalk.bold.red(
|
|
||||||
`Error reading package.json file from library build output.`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute "npm publish" to publish
|
// Execute "npm publish" to publish
|
||||||
|
|
Loading…
Reference in a new issue