fix formatting

This commit is contained in:
Djkáťo 2024-02-12 16:28:54 +01:00
parent b57321350d
commit ed2d865f2d
24 changed files with 3217 additions and 2495 deletions

View file

@ -1,5 +1,5 @@
{ {
"babelrcRoots": [ "babelrcRoots": [
"*" "*"
] ]
} }

148
nx.json
View file

@ -1,76 +1,76 @@
{ {
"$schema": "./node_modules/nx/schemas/nx-schema.json", "$schema": "./node_modules/nx/schemas/nx-schema.json",
"affected": { "affected": {
"defaultBase": "master" "defaultBase": "master"
}, },
"tasksRunnerOptions": { "tasksRunnerOptions": {
"default": { "default": {
"runner": "nx/tasks-runners/default", "runner": "nx/tasks-runners/default",
"options": { "options": {
"cacheableOperations": [ "cacheableOperations": [
"build", "build",
"lint", "lint",
"test", "test",
"e2e" "e2e"
] ]
} }
} }
}, },
"targetDefaults": { "targetDefaults": {
"build": { "build": {
"dependsOn": [ "dependsOn": [
"^build" "^build"
], ],
"inputs": [ "inputs": [
"production", "production",
"^production" "^production"
] ]
}, },
"lint": { "lint": {
"inputs": [ "inputs": [
"default", "default",
"{workspaceRoot}/.eslintrc.json", "{workspaceRoot}/.eslintrc.json",
"{workspaceRoot}/.eslintignore" "{workspaceRoot}/.eslintignore"
] ]
}, },
"e2e": { "e2e": {
"inputs": [ "inputs": [
"default", "default",
"^production" "^production"
] ]
}, },
"test": { "test": {
"inputs": [ "inputs": [
"default", "default",
"^production" "^production"
] ]
} }
}, },
"namedInputs": { "namedInputs": {
"default": [ "default": [
"{projectRoot}/**/*", "{projectRoot}/**/*",
"sharedGlobals" "sharedGlobals"
], ],
"production": [ "production": [
"default", "default",
"!{projectRoot}/.eslintrc.json", "!{projectRoot}/.eslintrc.json",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json" "!{projectRoot}/tsconfig.spec.json"
], ],
"sharedGlobals": [ "sharedGlobals": [
"{workspaceRoot}/babel.config.json" "{workspaceRoot}/babel.config.json"
] ]
}, },
"workspaceLayout": { "workspaceLayout": {
"appsDir": "packages", "appsDir": "packages",
"libsDir": "packages" "libsDir": "packages"
}, },
"generators": { "generators": {
"@nx/web:application": { "@nx/web:application": {
"style": "css", "style": "css",
"linter": "eslint", "linter": "eslint",
"unitTestRunner": "vitest", "unitTestRunner": "vitest",
"e2eTestRunner": "cypress" "e2eTestRunner": "cypress"
} }
} }
} }

View file

@ -1,44 +1,45 @@
{ {
"name": "@euterpe.js/source", "name": "@euterpe.js/source",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"publish-player": "nx build player && cd dist/packages/player && npm publish --access=public", "format": "prettier --write '**/*.{js,ts,css,html,json,mjs}'",
"publish-visualizer": "nx build visualizer && cd dist/packages/visualizer && npm publish --access=public", "publish-player": "nx build player && cd dist/packages/player && npm publish --access=public",
"publish-library": "nx build music-library && cd dist/packages/music-library && npm publish --access=public", "publish-visualizer": "nx build visualizer && cd dist/packages/visualizer && npm publish --access=public",
"publish-euterpe": "nx build euterpe && cd dist/packages/euterpe && npm publish --access=public", "publish-library": "nx build music-library && cd dist/packages/music-library && npm publish --access=public",
"publish-all": "npm run publish-player && npm run publish-library && npm run publish-visualizer && npm run publish-euterpe" "publish-euterpe": "nx build euterpe && cd dist/packages/euterpe && npm publish --access=public",
}, "publish-all": "npm run publish-player && npm run publish-library && npm run publish-visualizer && npm run publish-euterpe"
"private": false, },
"devDependencies": { "private": false,
"@nx/cypress": "16.2.1", "devDependencies": {
"@nx/eslint-plugin": "16.2.1", "@nx/cypress": "16.2.1",
"@nx/js": "16.2.1", "@nx/eslint-plugin": "16.2.1",
"@nx/linter": "16.2.1", "@nx/js": "16.2.1",
"@nx/vite": "^16.2.1", "@nx/linter": "16.2.1",
"@nx/web": "^16.2.1", "@nx/vite": "^16.2.1",
"@nx/workspace": "16.2.1", "@nx/web": "^16.2.1",
"@swc/core": "~1.3.51", "@nx/workspace": "16.2.1",
"@types/node": "^20.2.1", "@swc/core": "~1.3.51",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@types/node": "^20.2.1",
"@typescript-eslint/parser": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.58.0",
"@vitest/coverage-c8": "^0.31.0", "@typescript-eslint/parser": "^5.58.0",
"@vitest/ui": "^0.31.0", "@vitest/coverage-c8": "^0.31.0",
"cypress": "^12.11.0", "@vitest/ui": "^0.31.0",
"eslint": "~8.15.0", "cypress": "^12.11.0",
"eslint-config-prettier": "8.1.0", "eslint": "~8.15.0",
"eslint-plugin-cypress": "^2.10.3", "eslint-config-prettier": "8.1.0",
"jsdom": "~20.0.3", "eslint-plugin-cypress": "^2.10.3",
"nx": "16.2.1", "jsdom": "~20.0.3",
"prettier": "^2.6.2", "nx": "16.2.1",
"swc-loader": "0.1.15", "prettier": "^2.6.2",
"typescript": "~5.0.2", "swc-loader": "0.1.15",
"vite": "^4.3.4", "typescript": "~5.0.2",
"vite-plugin-eslint": "^1.8.1", "vite": "^4.3.4",
"vite-tsconfig-paths": "^4.0.2", "vite-plugin-eslint": "^1.8.1",
"vitest": "^0.31.0" "vite-tsconfig-paths": "^4.0.2",
}, "vitest": "^0.31.0"
"dependencies": { },
"tslib": "^2.3.0" "dependencies": {
} "tslib": "^2.3.0"
}
} }

View file

@ -1,74 +1,94 @@
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
interface SongConstructor { interface SongConstructor {
name: string name: string
artists?: Ref[] artists?: Ref[]
url: URL url: URL
duration?: number duration?: number
publish_date?: Date publish_date?: Date
remix_artists?: Ref[] remix_artists?: Ref[]
in_collection?: Ref in_collection?: Ref
cover?: URL cover?: URL
bpm?: number bpm?: number
key?: string key?: string
fft_data?: number[] fft_data?: number[]
id?: ID id?: ID
metadata?: any[] metadata?: any[]
} }
class DJSong extends Song { class DJSong extends Song {
audio_buffer?: AudioBuffer audio_buffer?: AudioBuffer
constructor(data: SongConstructor, audio_context?: AudioContext) { constructor(data: SongConstructor, audio_context?: AudioContext) {
super(data) super(data)
if (!audio_context) return if (!audio_context) return
try { try {
fetch(data.url).then((file) => { fetch(data.url).then((file) => {
file.arrayBuffer().then((buffer) => { file.arrayBuffer().then((buffer) => {
audio_context.decodeAudioData(buffer).then((audio_buffer) => { audio_context
this.audio_buffer = audio_buffer .decodeAudioData(buffer)
}) .then((audio_buffer) => {
}) this.audio_buffer = audio_buffer
}); })
} catch (e) { })
console.error(new Error("Failed to preprocess DJSong. " + e)) })
} } catch (e) {
} console.error(new Error("Failed to preprocess DJSong. " + e))
public async analyze(url: URL, audio_context: AudioContext) { }
this.audio_buffer = await audio_context.decodeAudioData(await (await fetch(url)).arrayBuffer()) }
} public async analyze(url: URL, audio_context: AudioContext) {
this.audio_buffer = await audio_context.decodeAudioData(
await (await fetch(url)).arrayBuffer()
)
}
} }
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"
for (const input of inputs) { ? (inputs = dj_songs)
if (input instanceof DJSong) { : (inputs = [dj_songs])
const song = input as DJSong for (const input of inputs) {
if (!song.id) song.id = this.songs.length if (input instanceof DJSong) {
const song = input as DJSong
if (!song.id) song.id = this.songs.length
if (song.in_collection) { if (song.in_collection) {
const curr_col = song.in_collection.get(this) as Collection const curr_col = song.in_collection.get(this) as Collection
curr_col.songs.push(new Ref(RefTo.Songs, song.id)) curr_col.songs.push(new Ref(RefTo.Songs, song.id))
song.artists.forEach((artist) => curr_col.artists.push(new Ref(RefTo.Artists, artist.get(this)!.id!))) song.artists.forEach((artist) =>
song.remix_artists.forEach((artist) => curr_col.artists.push(new Ref(RefTo.Artists, artist.get(this)!.id!))) curr_col.artists.push(
} new Ref(RefTo.Artists, artist.get(this)!.id!)
)
)
song.remix_artists.forEach((artist) =>
curr_col.artists.push(
new Ref(RefTo.Artists, artist.get(this)!.id!)
)
)
}
for (const artist_ref of song.artists) { for (const artist_ref of song.artists) {
const curr_artist = artist_ref.get(this) as Artist const curr_artist = artist_ref.get(this) as Artist
curr_artist.songs.push(new Ref(RefTo.Songs, song.id)) curr_artist.songs.push(new Ref(RefTo.Songs, song.id))
} }
for (const artist_ref of song.remix_artists) { for (const artist_ref of song.remix_artists) {
const curr_artist = artist_ref.get(this) as Artist const curr_artist = artist_ref.get(this) as Artist
curr_artist.songs.push(new Ref(RefTo.Songs, song.id)) curr_artist.songs.push(new Ref(RefTo.Songs, song.id))
} }
this.songs.push(song) this.songs.push(song)
} }
} }
} }
}
}

View file

@ -1,125 +1,159 @@
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.
* Make sure your master bpm isnt >= 300, on_beat will break cause there's a setTimeout of 200ms (60s/300BPM) * Make sure your master bpm isnt >= 300, on_beat will break cause there's a setTimeout of 200ms (60s/300BPM)
*/ */
class DJ { class DJ {
tracks: Track[] = [] tracks: Track[] = []
/**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: {
constructor(public player: Euterpe | MusicPlayer, public master_bpm: number | 120) { current: number
this.beat_duration = 60 / master_bpm max: number
this.#emit_beats() next_bar_in: number
} }) => void
#emit_beats() { constructor(
this.beat.current >= 4 ? public player: Euterpe | MusicPlayer,
(this.beat.current++, this.beat.next_bar_in--) : public master_bpm: number | 120
(this.beat.current = 0, this.beat.next_bar_in = this.beat.max) ) {
this.beat_duration = 60 / master_bpm
if (this.on_beat) this.on_beat(this.beat) this.#emit_beats()
//This makes it break if BPM >= 300!!!! }
new Promise((resolve) => setTimeout(resolve, 200)).then(() => { #emit_beats() {
requestAnimationFrame(this.#emit_beats.bind(this)) this.beat.current >= 4
}) ? (this.beat.current++, this.beat.next_bar_in--)
} : ((this.beat.current = 0), (this.beat.next_bar_in = this.beat.max))
create_track(song?: Song, should_loop = false) {
this.tracks.push(new Track(this.player, song, should_loop))
}
/**
*
* @param i index of track
* @param delay how many beats in should the track start? 0 or undefined for asap, 2 = in two beats etc...
* @returns Promise<Error | self>
*/
async try_queue_track(track_i: number, delay: number) {
return new Promise((resolve, reject) => {
this.tracks[track_i].try_start(delay).then(() => resolve(this), (e) => reject(e))
})
}
/**
* Won't start playback, use try_queue_track() or try_start_track()
* @returns Promise<Error | self>
*/
async try_load_song_into_track(track_i: number, song: Song) {
return new Promise((resolve, reject) => {
this.tracks[track_i].change_song(song).then(() => resolve(this), (e) => reject(e))
})
}
/**
*
* @param i index of track
* @returns Promise<Error | self>
*/
async try_start_track(track_i: number) {
return new Promise((resolve, reject) => {
this.tracks[track_i].try_start().then(() => resolve(this), (e) => reject(e))
})
}
/**
* This function will have to restart every track, so for now implementatino pending c:
* @param new_master_bpm number in bpm
*/
set_master_bpm(new_master_bpm: number) {
this.master_bpm = new_master_bpm
this.beat_duration = 60 / this.master_bpm
}
if (this.on_beat) this.on_beat(this.beat)
//This makes it break if BPM >= 300!!!!
new Promise((resolve) => setTimeout(resolve, 200)).then(() => {
requestAnimationFrame(this.#emit_beats.bind(this))
})
}
create_track(song?: Song, should_loop = false) {
this.tracks.push(new Track(this.player, song, should_loop))
}
/**
*
* @param i index of track
* @param delay how many beats in should the track start? 0 or undefined for asap, 2 = in two beats etc...
* @returns Promise<Error | self>
*/
async try_queue_track(track_i: number, delay: number) {
return new Promise((resolve, reject) => {
this.tracks[track_i].try_start(delay).then(
() => resolve(this),
(e) => reject(e)
)
})
}
/**
* Won't start playback, use try_queue_track() or try_start_track()
* @returns Promise<Error | self>
*/
async try_load_song_into_track(track_i: number, song: Song) {
return new Promise((resolve, reject) => {
this.tracks[track_i].change_song(song).then(
() => resolve(this),
(e) => reject(e)
)
})
}
/**
*
* @param i index of track
* @returns Promise<Error | self>
*/
async try_start_track(track_i: number) {
return new Promise((resolve, reject) => {
this.tracks[track_i].try_start().then(
() => resolve(this),
(e) => reject(e)
)
})
}
/**
* This function will have to restart every track, so for now implementatino pending c:
* @param new_master_bpm number in bpm
*/
set_master_bpm(new_master_bpm: number) {
this.master_bpm = new_master_bpm
this.beat_duration = 60 / this.master_bpm
}
} }
class Track { class Track {
private audio_buffer?: AudioBuffer private audio_buffer?: AudioBuffer
private buffer_source?: AudioBufferSourceNode private buffer_source?: AudioBufferSourceNode
gain: GainNode gain: GainNode
audio_context: AudioContext | BaseAudioContext audio_context: AudioContext | BaseAudioContext
constructor(public player: MusicPlayer | Euterpe, public current_song?: Song, public should_loop?: boolean) { constructor(
this.audio_context = player.audio_context public player: MusicPlayer | Euterpe,
this.gain = this.audio_context.createGain() public current_song?: Song,
if (current_song) this.change_song(current_song).catch((e) => console.error("error during track construction - " + e)) public should_loop?: boolean
} ) {
this.audio_context = player.audio_context
this.gain = this.audio_context.createGain()
if (current_song)
this.change_song(current_song).catch((e) =>
console.error("error during track construction - " + e)
)
}
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 =
this.audio_buffer = await this.audio_context.decodeAudioData(await file.arrayBuffer()) await this.audio_context.decodeAudioData(
resolve(this) await file.arrayBuffer()
}, )
(reason) => reject(reason)) resolve(this)
}) },
} (reason) => reject(reason)
#connect() { )
if (!this.audio_buffer) throw new Error("Somehow buffer not in track even though it analyzed properly. Report this as a bug") })
this.buffer_source = this.audio_context.createBufferSource() }
this.buffer_source.buffer = this.audio_buffer! #connect() {
this.buffer_source.connect(this.gain) if (!this.audio_buffer)
this.buffer_source.loop = this.should_loop || false throw new Error(
this.gain.connect(this.player.gain) "Somehow buffer not in track even though it analyzed properly. Report this as a bug"
} )
async change_song(new_song: Song) { this.buffer_source = this.audio_context.createBufferSource()
return new Promise((resolve, reject) => { this.buffer_source.buffer = this.audio_buffer!
this.current_song = new_song this.buffer_source.connect(this.gain)
this.#prepare().then(() => { this.buffer_source.loop = this.should_loop || false
this.#connect() this.gain.connect(this.player.gain)
resolve(this) }
}, (reason) => reject(reason)) async change_song(new_song: Song) {
}) return new Promise((resolve, reject) => {
} this.current_song = new_song
/** this.#prepare().then(
* () => {
* @param delay in seconds this.#connect()
*/ resolve(this)
async try_start(delay?: number) { },
return new Promise((resolve, reject) => { (reason) => reject(reason)
if (!this.buffer_source) reject(new Error("No buffer source yet, set a song first")) )
this.buffer_source!.start(this.audio_context.currentTime + (delay || 0)) })
}) }
} /**
} *
* @param delay in seconds
*/
async try_start(delay?: number) {
return new Promise((resolve, reject) => {
if (!this.buffer_source)
reject(new Error("No buffer source yet, set a song first"))
this.buffer_source!.start(
this.audio_context.currentTime + (delay || 0)
)
})
}
}

View file

@ -1 +1 @@
import { MusicPlayerBuilder } from "@euterpe.js/player"; import { MusicPlayerBuilder } from "@euterpe.js/player"

View file

@ -1,62 +1,92 @@
import { DB, Song, Artist, Ref, RefTo, Platforms } from "@euterpe.js/music-library" import {
export const db = new DB DB,
Song,
Artist,
Ref,
RefTo,
Platforms
} from "@euterpe.js/music-library"
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(
new Artist({ "https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1"
name: "toe", )
id: 10 ]
}), ]
}),
new Artist({
name: "toe",
id: 10
})
]) ])
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
artists: [new Ref(RefTo.Artists, 2), new Ref(RefTo.Artists, 3), new Ref(RefTo.Artists, 4)], artists: [
duration: 252, new Ref(RefTo.Artists, 2),
name: "Star", new Ref(RefTo.Artists, 3),
remix_artists: [new Ref(RefTo.Artists, 5)], new Ref(RefTo.Artists, 4)
url: new URL("http://" + window.location.host + "/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3") ],
}), duration: 252,
new Song({ name: "Star",
//If you don't like guessing the IDs, then this is also a way to do it remix_artists: [new Ref(RefTo.Artists, 5)],
artists: [new Ref(RefTo.Artists, db.artists.find((a) => a.name == "Jamie xx")!.id!)], url: new URL(
duration: 331, "http://" +
name: "Sleep Sound", window.location.host +
url: new URL("http://" + window.location.host + "/Jamie xx - Sleep Sound.mp3") "/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3"
}), )
new Song({ }),
artists: [new Ref(RefTo.Artists, 1)], new Song({
duration: 75, //If you don't like guessing the IDs, then this is also a way to do it
name: "wish", artists: [
url: new URL("http://" + window.location.host + "/janz - wish.mp3") new Ref(
}), RefTo.Artists,
new Song({ db.artists.find((a) => a.name == "Jamie xx")!.id!
artists: [new Ref(RefTo.Artists, 10)], )
duration: 4 * 60 + 5, ],
name: "サニーボーイ・ラプソディ", duration: 331,
url: new URL("http://" + window.location.host + "/16.サニーボーイ・ラプソディ.ogg") name: "Sleep Sound",
}) url: new URL(
"http://" + window.location.host + "/Jamie xx - Sleep Sound.mp3"
)
}),
new Song({
artists: [new Ref(RefTo.Artists, 1)],
duration: 75,
name: "wish",
url: new URL("http://" + window.location.host + "/janz - wish.mp3")
}),
new Song({
artists: [new Ref(RefTo.Artists, 10)],
duration: 4 * 60 + 5,
name: "サニーボーイ・ラプソディ",
url: new URL(
"http://" +
window.location.host +
"/16.サニーボーイ・ラプソディ.ogg"
)
})
]) ])

View file

@ -1,129 +1,147 @@
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(
.build() document.querySelector("#audio")!,
db
).build()
add_library_to_dom() add_library_to_dom()
euterpe.try_preload_song(0).then(() => { euterpe.try_preload_song(0).then(
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() () => {
}, (e) => console.log(e + " Failed to preload")) 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)
console.log("seeked to " + e.target?.valueAsNumber) console.log("seeked to " + e.target?.valueAsNumber)
} catch { } catch {
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..
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
}) })
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
dev_queue_update() dev_queue_update()
dev_history_update() dev_history_update()
}) })
document.querySelector("#previous")?.addEventListener("click", () => { document.querySelector("#previous")?.addEventListener("click", () => {
euterpe.try_previous_song_looping().then(() => { euterpe.try_previous_song_looping().then(
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() () => {
}, (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("#next")?.addEventListener("click", () => { document.querySelector("#next")?.addEventListener("click", () => {
euterpe.try_next_song_looping().then(() => { euterpe.try_next_song_looping().then(
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() () => {
}, (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() {
const lib_dom = document.querySelector(".library-wrapper") as HTMLDivElement const lib_dom = document.querySelector(".library-wrapper") as HTMLDivElement
for (const song of euterpe.db.songs) { for (const song of euterpe.db.songs) {
const div = document.createElement("div") const div = document.createElement("div")
const p = document.createElement("p") const p = document.createElement("p")
const button_play = document.createElement("button") const button_play = document.createElement("button")
const button_queue = document.createElement("button") const button_queue = document.createElement("button")
const span = document.createElement("span") const span = document.createElement("span")
p.innerHTML = `${euterpe.format_current_song(song.id)}` p.innerHTML = `${euterpe.format_current_song(song.id)}`
button_play.innerHTML = "play" button_play.innerHTML = "play"
button_play.dataset["id"] = `${song.id}` button_play.dataset["id"] = `${song.id}`
button_play.onclick = library_play button_play.onclick = library_play
button_queue.innerHTML = "queue" button_queue.innerHTML = "queue"
button_queue.dataset["id"] = `${song.id}` button_queue.dataset["id"] = `${song.id}`
button_queue.onclick = library_queue button_queue.onclick = library_queue
div.appendChild(p) div.appendChild(p)
span.appendChild(button_play) span.appendChild(button_play)
span.appendChild(button_queue) span.appendChild(button_queue)
div.appendChild(span) div.appendChild(span)
lib_dom.appendChild(div) lib_dom.appendChild(div)
} }
} }
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(), () =>
(e) => alert(e) (document.querySelector("#text-playing")!.innerHTML =
) euterpe.format_current_song()),
(e) => alert(e)
)
} }
function library_queue(e: MouseEvent) { function library_queue(e: MouseEvent) {
const b = e.currentTarget as HTMLButtonElement const b = e.currentTarget as HTMLButtonElement
euterpe.queue_append(parseInt(b.dataset["id"]!)) euterpe.queue_append(parseInt(b.dataset["id"]!))
} }
function dev_queue_update() { function dev_queue_update() {
const p = document.querySelector("#queue-info") as HTMLParagraphElement const p = document.querySelector("#queue-info") as HTMLParagraphElement
const dev_arr = [] const dev_arr = []
for (const song of euterpe.queue) { for (const song of euterpe.queue) {
dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`) dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`)
} }
p.innerHTML = dev_arr.toString() p.innerHTML = dev_arr.toString()
} }
function dev_history_update() { function dev_history_update() {
const p = document.querySelector("#history-info") as HTMLParagraphElement const p = document.querySelector("#history-info") as HTMLParagraphElement
const dev_arr = [] const dev_arr = []
for (const song of euterpe.played_history) { for (const song of euterpe.played_history) {
dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`) dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`)
} }
p.innerHTML = dev_arr.toString() p.innerHTML = dev_arr.toString()
} }

View file

@ -6,427 +6,482 @@ export { Euterpe, EuterpeBuilder }
* Avoid Writing directly to any fields in this class! * Avoid Writing directly to any fields in this class!
*/ */
class Euterpe extends MusicPlayer { class Euterpe extends MusicPlayer {
current_song: Song | undefined current_song: Song | undefined
current_song_id = 0 current_song_id = 0
queue: Song[] = [] queue: Song[] = []
played_history: Song[] = [] played_history: Song[] = []
constructor( constructor(
public db: DB, public db: DB,
audio_context: AudioContext, audio_context: AudioContext,
audio_element: HTMLAudioElement, audio_element: HTMLAudioElement,
track: MediaElementAudioSourceNode, track: MediaElementAudioSourceNode,
gain: GainNode, gain: GainNode,
volume: number, volume: number,
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) /**
} * Use to load song on page load.
* @throws if song with ID doesn't exist
*/
async try_preload_song(id: number) {
const next = this.db.songs.find((song) => song!.id == id)
if (!next) throw new Error(`Song with id ${id} doesn't exist`)
else {
await this.try_new_song(next.url.pathname)
this.current_song = next
}
}
/** /**
* Use to load song on page load. * Won't loop back to first song if already on the last.
* @throws if song with ID doesn't exist * If queue present, uses that, if not, relies on Song ID directly from DB
*/ * @throws if on last song or song fails to start
async try_preload_song(id: number) { */
const next = this.db.songs.find((song) => song!.id == id) async try_next_song() {
if (!next) throw new Error(`Song with id ${id} doesn't exist`) let new_song: Song
else { if (this.queue.length > 0) {
await this.try_new_song(next.url.pathname) new_song = this.queue.shift()!
this.current_song = next } else {
} let id_i = this.db.songs.length
} while (this.db.songs[--id_i].id! > this.current_song_id);
const next_id = ++id_i
/** if (next_id == this.db.songs.length)
* Won't loop back to first song if already on the last. throw new Error("Won't go past the last song")
* If queue present, uses that, if not, relies on Song ID directly from DB new_song = this.db.songs.find((song) => song.id == next_id)!
* @throws if on last song or song fails to start }
*/ const url = this.options?.use_only_pathname_url
async try_next_song() { ? new_song.url.pathname
let new_song: Song : new_song.url.toString()
if (this.queue.length > 0) { await this.try_new_song(url)
new_song = this.queue.shift()! await this.try_play()
} else { if (this.current_song) this.played_history.push(this.current_song)
let id_i = this.db.songs.length; this.current_song = new_song
while (this.db.songs[--id_i].id! > this.current_song_id); this.current_song_id = new_song.id!
const next_id = ++id_i; }
if (next_id == this.db.songs.length) throw new Error("Won't go past the last song") /**
new_song = this.db.songs.find((song) => song.id == next_id)! * Will loop back to first song if already on last song,
} * If queue present, uses that, if not, relies on Song ID directly from DB
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() */
await this.try_new_song(url) async try_next_song_looping() {
await this.try_play() let new_song: Song
if (this.current_song) this.played_history.push(this.current_song) if (this.queue.length > 0) {
this.current_song = new_song new_song = this.queue.shift()!
this.current_song_id = new_song.id! } else {
} let id_i = this.db.songs.length
while (this.db.songs[--id_i].id! > this.current_song_id);
let next_id = ++id_i
/** if (next_id == this.db.songs.length) next_id = this.db.songs[0].id!
* Will loop back to first song if already on last song, new_song = this.db.songs.find((song) => song.id == next_id)!
* If queue present, uses that, if not, relies on Song ID directly from DB }
*/ const url = this.options?.use_only_pathname_url
async try_next_song_looping() { ? new_song.url.pathname
let new_song: Song : new_song.url.toString()
if (this.queue.length > 0) { await this.try_new_song(url)
new_song = this.queue.shift()! await this.try_play()
} else { if (this.current_song) this.played_history.push(this.current_song)
let id_i = this.db.songs.length; this.current_song = new_song
while (this.db.songs[--id_i].id! > this.current_song_id); this.current_song_id = new_song.id!
let next_id = ++id_i }
if (next_id == this.db.songs.length) next_id = this.db.songs[0].id! /**
new_song = this.db.songs.find((song) => song.id == next_id)! * Won't tell you if the playback was successsful & wil loop back if already on last song. Best use try_next_song_async()
} * If queue present, uses that, if not, relies on Song ID directly from DB
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() */
await this.try_new_song(url) next_song_looping() {
await this.try_play() let new_song: Song
if (this.current_song) this.played_history.push(this.current_song) if (this.queue.length > 0) {
this.current_song = new_song new_song = this.queue.shift()!
this.current_song_id = new_song.id! } else {
} let id_i = this.db.songs.length
while (this.db.songs[--id_i].id! > this.current_song_id);
let next_id = ++id_i
/** if (next_id == this.db.songs.length) next_id = this.db.songs[0].id!
* Won't tell you if the playback was successsful & wil loop back if already on last song. Best use try_next_song_async() new_song = this.db.songs.find((song) => song.id == next_id)!
* If queue present, uses that, if not, relies on Song ID directly from DB }
*/ const url = this.options?.use_only_pathname_url
next_song_looping() { ? new_song.url.pathname
let new_song: Song : new_song.url.toString()
if (this.queue.length > 0) { this.new_song(url)
new_song = this.queue.shift()! this.play()
} else { if (this.current_song) this.played_history.push(this.current_song)
let id_i = this.db.songs.length; this.current_song = new_song
while (this.db.songs[--id_i].id! > this.current_song_id); this.current_song_id = new_song.id!
let next_id = ++id_i; }
if (next_id == this.db.songs.length) next_id = this.db.songs[0].id! /**
new_song = this.db.songs.find((song) => song.id == next_id)! * 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()
} * If queue present, uses that, if not, relies on Song ID directly from DB
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() */
this.new_song(url) next_song() {
this.play() let new_song: Song
if (this.current_song) this.played_history.push(this.current_song) if (this.queue.length > 0) {
this.current_song = new_song new_song = this.queue.shift()!
this.current_song_id = new_song.id! } else {
} let id_i = this.db.songs.length
while (this.db.songs[--id_i].id! > this.current_song_id);
const next_id = ++id_i
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()
this.new_song(url)
this.play()
if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song
this.current_song_id = new_song.id!
}
/** /**
* Won't tell you if the playback was successsful, won't loop back if already on last song and won't throw error if attempted. Best use next_song_async() * 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
* If queue present, uses that, if not, relies on Song ID directly from DB */
*/ async try_specific_song(new_song_id: number) {
next_song() { const new_song = this.db.songs.find((song) => song.id! == new_song_id)
let new_song: Song if (!new_song) throw new Error(`No song with id "${new_song_id}" found`)
if (this.queue.length > 0) { else {
new_song = this.queue.shift()! this.try_new_song(new_song.url.pathname)
} else { await this.try_play()
let id_i = this.db.songs.length; if (this.current_song) this.played_history.push(this.current_song)
while (this.db.songs[--id_i].id! > this.current_song_id); this.current_song = new_song
const next_id = ++id_i; this.current_song_id = new_song.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()
this.new_song(url)
this.play()
if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song
this.current_song_id = new_song.id!
}
/** /**
* Uses safer try_play_async. Normal play / play_async will try to start the player even if the track hasn't started yet, or was previously suspended/closed * 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()
*/ */
async try_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) throw new Error(`No song with id "${new_song_id}" found`) if (!new_song) return
else { const url = this.options?.use_only_pathname_url
this.try_new_song(new_song.url.pathname) ? new_song.url.pathname
await this.try_play() : new_song.url.toString()
if (this.current_song) this.played_history.push(this.current_song) this.new_song(url)
this.current_song = new_song this.play()
this.current_song_id = new_song.id! if (this.current_song) this.played_history.push(this.current_song)
} this.current_song = new_song
} this.current_song_id = new_song.id!
}
/** /**
* Won't 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 loop back to first song if already on the last.
*/ * If played_history is present, uses that, if not, relies on Song ID directly from DB
specific_song(new_song_id: number) { * @throws if playback was unsuccessful or at first song/ can't go more previous
const new_song = this.db.songs.find((song) => song.id! == new_song_id) */
if (!new_song) return async try_previous_song() {
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() let new_song: Song
this.new_song(url) if (this.played_history.length > 0) {
this.play() new_song = this.played_history.pop()!
if (this.current_song) this.played_history.push(this.current_song) } else {
this.current_song = new_song let id_i = 0
this.current_song_id = new_song.id! while (this.db.songs[++id_i].id! < this.current_song_id);
} const next_id = --id_i
/** if (next_id == this.db.songs.length)
* Won't loop back to first song if already on the last. throw new Error("Won't roll backwards to last song")
* If played_history is present, uses that, if not, relies on Song ID directly from DB new_song = this.db.songs.find((song) => song.id == next_id)!
* @throws if playback was unsuccessful or at first song/ can't go more previous }
*/ const url = this.options?.use_only_pathname_url
async try_previous_song() { ? new_song.url.pathname
let new_song: Song : new_song.url.toString()
if (this.played_history.length > 0) { await this.try_new_song(url)
new_song = this.played_history.pop()! await this.try_play()
} else { //if (this.current_song) this.played_history.push(this.current_song)
let id_i = 0; this.current_song = new_song
while (this.db.songs[++id_i].id! < this.current_song_id); this.current_song_id = new_song.id!
const next_id = --id_i; }
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)! * Will loop back to first song if already on the last.
} * If history present, uses that, if not, relies on Song ID directly from DB
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() */
await this.try_new_song(url) async try_previous_song_looping() {
await this.try_play() let new_song: Song
//if (this.current_song) this.played_history.push(this.current_song) if (this.played_history.length > 0) {
this.current_song = new_song new_song = this.played_history.pop()!
this.current_song_id = new_song.id! } else {
} let id_i = -1
while (this.db.songs[++id_i].id! < this.current_song_id);
let next_id = --id_i
/** if (next_id == -1)
* Will loop back to first song if already on the last. next_id = this.db.songs[this.db.songs.length - 1].id!
* If history present, uses that, if not, relies on Song ID directly from DB new_song = this.db.songs.find((song) => song.id == next_id)!
*/ }
async try_previous_song_looping() { const url = this.options?.use_only_pathname_url
let new_song: Song ? new_song.url.pathname
if (this.played_history.length > 0) { : new_song.url.toString()
new_song = this.played_history.pop()! await this.try_new_song(url)
} else { await this.try_play()
let id_i = -1; //if (this.current_song) this.played_history.push(this.current_song)
while (this.db.songs[++id_i].id! < this.current_song_id); this.current_song = new_song
let next_id = --id_i; this.current_song_id = new_song.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)! * won't tell you if the play was successful, won't loop back to last song if already on the first and will throw error if attempted.
} * If history present, uses that, if not, relies on Song ID directly from DB
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() */
await this.try_new_song(url) previous_song() {
await this.try_play() let new_song: Song
//if (this.current_song) this.played_history.push(this.current_song) if (this.played_history.length > 0) {
this.current_song = new_song new_song = this.played_history.pop()!
this.current_song_id = new_song.id! } else {
} let id_i = 0
while (this.db.songs[++id_i].id! < this.current_song_id);
const next_id = -id_i
/** if (next_id == this.db.songs.length)
* won't tell you if the play was successful, won't loop back to last song if already on the first and will throw error if attempted. throw new Error("Won't go past the last song")
* If history present, uses that, if not, relies on Song ID directly from DB new_song = this.db.songs.find((song) => song.id == next_id)!
*/ }
previous_song() { const url = this.options?.use_only_pathname_url
let new_song: Song ? new_song.url.pathname
if (this.played_history.length > 0) { : new_song.url.toString()
new_song = this.played_history.pop()! this.new_song(url)
} else { this.play()
let id_i = 0; //if (this.current_song) this.played_history.push(this.current_song)
while (this.db.songs[++id_i].id! < this.current_song_id); this.current_song_id = new_song.id!
const next_id = -id_i; this.current_song = new_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)! * won't tell you if the play was successful & will loop back to last song if already on the first.
} * If queue present, uses that, if not, relies on Song ID directly from DB
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() */
this.new_song(url) previous_song_looping() {
this.play() let new_song: Song
//if (this.current_song) this.played_history.push(this.current_song) if (this.played_history.length > 0) {
this.current_song_id = new_song.id! new_song = this.played_history.pop()!
this.current_song = new_song } else {
} let id_i = 0
while (this.db.songs[++id_i].id! < this.current_song_id);
let next_id = -id_i
/** if (next_id == this.db.songs.length)
* won't tell you if the play was successful & will loop back to last song if already on the first. next_id = this.db.songs[this.db.songs.length].id!
* If queue present, uses that, if not, relies on Song ID directly from DB new_song = this.db.songs.find((song) => song.id == next_id)!
*/ }
previous_song_looping() { const url = this.options?.use_only_pathname_url
let new_song: Song ? new_song.url.pathname
if (this.played_history.length > 0) { : new_song.url.toString()
new_song = this.played_history.pop()! this.new_song(url)
} else { this.play()
let id_i = 0; //if (this.current_song) this.played_history.push(this.current_song)
while (this.db.songs[++id_i].id! < this.current_song_id); this.current_song_id = new_song.id!
let next_id = -id_i; this.current_song = new_song
}
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)! * Takes the song data from current song if no song ID is specified. Will return "ID - ID" if ID and current song doesn't exist
} * @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix)
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() */
this.new_song(url) format_current_song(id = this.current_song?.id) {
this.play() const curr_song = this.db.songs.find((song) => song.id == id)
//if (this.current_song) this.played_history.push(this.current_song) if (!curr_song) {
this.current_song_id = new_song.id! return "ID - ID"
this.current_song = new_song }
} let final_text = ""
/** for (const artist of curr_song.artists) {
* Takes the song data from current song if no song ID is specified. Will return "ID - ID" if ID and current song doesn't exist const curr_artist = artist.get(this.db) as Artist
* @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix) final_text += curr_artist.name + ", "
*/ }
format_current_song(id = this.current_song?.id) {
const curr_song = this.db.songs.find((song) => song.id == id) final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", "
if (!curr_song) { final_text += " - " + curr_song.name
return "ID - ID"
}
let final_text = ""
for (const artist of curr_song.artists) { if (curr_song.remix_artists.length > 0) {
const curr_artist = artist.get(this.db) as Artist final_text += " ("
final_text += curr_artist.name + ", "
}
final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " for (const artist of curr_song.remix_artists) {
final_text += " - " + curr_song.name const curr_artist = artist.get(this.db) as Artist
if (curr_artist.links && curr_artist.links.length > 0) {
final_text += curr_artist.name
} else {
final_text += curr_artist.name + ", "
}
}
if (curr_song.remix_artists.length > 0) { final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", "
final_text += " (" final_text += " Remix)"
}
for (const artist of curr_song.remix_artists) { return final_text
const curr_artist = artist.get(this.db) as Artist }
if (curr_artist.links && curr_artist.links.length > 0) {
final_text += curr_artist.name
} else {
final_text += curr_artist.name + ", "
}
}
final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " /**
final_text += " Remix)" * Will add to queue, if ID is undefined nothing will happen. If ID already is in queue, nothing will happen. For more control use `try_queue_add()`
} */
queue_add(id: number) {
return final_text const curr_song = this.db.songs.find((song) => song.id == id)
} if (!curr_song) return
if (this.queue.find((song) => song.id == id)) return
/** this.queue.push(curr_song)
* Will add to queue, if ID is undefined nothing will happen. If ID already is in queue, nothing will happen. For more control use `try_queue_add()` }
*/
queue_add(id: number) {
const curr_song = this.db.songs.find((song) => song.id == id)
if (!curr_song) return
if (this.queue.find((song) => song.id == id)) return
this.queue.push(curr_song)
}
/**
* Will add to queue. If ID is undefined throws error. if ID is already in queue, throws error.
*/
try_queue_add(id: number) {
const curr_song = this.db.songs.find((song) => song.id == id)
if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`)
if (this.queue.find((song) => song.id == id)) throw new Error(`Song of id "${id}" already queued`)
this.queue.push(curr_song)
}
/**
* Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. Throws error if ID doesn't exist.
*/
try_queue_append(id: number) {
const curr_song = this.db.songs.find((song) => song.id == id)
if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`)
const i = this.queue.findIndex((song) => song.id == id)
if (i != -1) this.queue.push(this.queue.splice(i, 1)[0])
else this.queue.push(curr_song)
}
/**
* Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. If ID Doesn't exist, does nothing. For more control use try_queue_append()
*/
queue_append(id: number) {
const curr_song = this.db.songs.find((song) => song.id == id)
if (!curr_song) return
const i = this.queue.findIndex((song) => song.id == id)
if (i != -1) this.queue.push(this.queue.splice(i, 1)[0])
else this.queue.push(curr_song)
}
/**
* Removes song of ID from queue and returns it. Does and returns nothing if song already not found.
*/
queue_remove(id: number) {
const i = this.queue.findIndex((song) => song.id == id)
if (i == -1) return
return this.queue.splice(i, 1)
}
/**
* Will add to queue. If ID is undefined throws error. if ID is already in queue, throws error.
*/
try_queue_add(id: number) {
const curr_song = this.db.songs.find((song) => song.id == id)
if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`)
if (this.queue.find((song) => song.id == id))
throw new Error(`Song of id "${id}" already queued`)
this.queue.push(curr_song)
}
/**
* Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. Throws error if ID doesn't exist.
*/
try_queue_append(id: number) {
const curr_song = this.db.songs.find((song) => song.id == id)
if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`)
const i = this.queue.findIndex((song) => song.id == id)
if (i != -1) this.queue.push(this.queue.splice(i, 1)[0])
else this.queue.push(curr_song)
}
/**
* Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. If ID Doesn't exist, does nothing. For more control use try_queue_append()
*/
queue_append(id: number) {
const curr_song = this.db.songs.find((song) => song.id == id)
if (!curr_song) return
const i = this.queue.findIndex((song) => song.id == id)
if (i != -1) this.queue.push(this.queue.splice(i, 1)[0])
else this.queue.push(curr_song)
}
/**
* Removes song of ID from queue and returns it. Does and returns nothing if song already not found.
*/
queue_remove(id: number) {
const i = this.queue.findIndex((song) => song.id == id)
if (i == -1) return
return this.queue.splice(i, 1)
}
} }
type BuilderOptions = { type BuilderOptions = {
use_only_pathname_url?: boolean use_only_pathname_url?: boolean
} }
class EuterpeBuilder { class EuterpeBuilder {
#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 )
* will throw if audio_element is undefined (stupid vue setup amirite?) * will throw if audio_element is undefined (stupid vue setup amirite?)
* will throw if user has not interacted with the page yet (Can't initiate AudioContext) * will throw if user has not interacted with the page yet (Can't initiate AudioContext)
*/ */
constructor(private audio_element: HTMLAudioElement, private db: DB, private options?: BuilderOptions) { constructor(
if (audio_element === undefined) throw Error("audio_element was undefined") private audio_element: HTMLAudioElement,
// ↓ For old browsers private db: DB,
const AudioContext = window.AudioContext || window.webkitAudioContext; private options?: BuilderOptions
this.#audio_context = new AudioContext() ) {
this.#track = this.#audio_context.createMediaElementSource(audio_element) if (audio_element === undefined)
this.#gain = this.#audio_context.createGain() throw Error("audio_element was undefined")
} // ↓ For old browsers
/** const AudioContext = window.AudioContext || window.webkitAudioContext
* For external use, not kept inside player after connection. this.#audio_context = new AudioContext()
* @returns {AnalyserNode} this.#track =
*/ this.#audio_context.createMediaElementSource(audio_element)
add_analyser() { this.#gain = this.#audio_context.createGain()
const analyser = this.#audio_context.createAnalyser() }
!this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser) /**
this.#prev_node = analyser * For external use, not kept inside player after connection.
return analyser * @returns {AnalyserNode}
} */
/** add_analyser() {
* For external use, not kept inside player after connection. const analyser = this.#audio_context.createAnalyser()
* @returns {StereoPannerNode} !this.#prev_node
*/ ? this.#track.connect(analyser)
add_stereo_panner_node() { : this.#prev_node.connect(analyser)
const panner = this.#audio_context.createStereoPanner() this.#prev_node = analyser
!this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner) return analyser
this.#prev_node = panner }
return panner /**
} * For external use, not kept inside player after connection.
/** * @returns {StereoPannerNode}
* For external use, not kept inside player after connection. */
* @returns {StereoPannerNode} add_stereo_panner_node() {
*/ const panner = this.#audio_context.createStereoPanner()
add_wave_shaper_node() { !this.#prev_node
const shaper = this.#audio_context.createWaveShaper() ? this.#track.connect(panner)
!this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper) : this.#prev_node.connect(panner)
this.#prev_node = shaper this.#prev_node = panner
return shaper return panner
} }
/** /**
* For additional trickery, you can connect your own node. * For external use, not kept inside player after connection.
*/ * @returns {StereoPannerNode}
connect_custom_node(node: AudioNode) { */
!this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node) add_wave_shaper_node() {
this.#prev_node = node const shaper = this.#audio_context.createWaveShaper()
} !this.#prev_node
/** ? this.#track.connect(shaper)
* Only use if you need to connect the #gain before another node, : this.#prev_node.connect(shaper)
* eg. if you want the analyser nodes output to be affected by user #gain this.#prev_node = shaper
*/ return shaper
connect_gain() { }
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) /**
this.#prev_node = this.#gain * For additional trickery, you can connect your own node.
this.#is_gain_connected = true */
} connect_custom_node(node: AudioNode) {
/** !this.#prev_node
* Finishes the build ? this.#track.connect(node)
* @returns {Euterpe} : this.#prev_node.connect(node)
*/ this.#prev_node = node
build() { }
if (!this.#is_gain_connected) { /**
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) * Only use if you need to connect the #gain before another node,
this.#prev_node = this.#gain * eg. if you want the analyser nodes output to be affected by user #gain
} */
this.#prev_node.connect(this.#audio_context.destination) connect_gain() {
return new Euterpe(this.db, this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume, undefined, this.options) !this.#prev_node
} ? this.#track.connect(this.#gain)
: this.#prev_node.connect(this.#gain)
this.#prev_node = this.#gain
this.#is_gain_connected = true
}
/**
* Finishes the build
* @returns {Euterpe}
*/
build() {
if (!this.#is_gain_connected) {
!this.#prev_node
? this.#track.connect(this.#gain)
: this.#prev_node.connect(this.#gain)
this.#prev_node = this.#gain
}
this.#prev_node.connect(this.#audio_context.destination)
return new Euterpe(
this.db,
this.#audio_context,
this.audio_element,
this.#track,
this.#gain,
this.#volume,
undefined,
this.options
)
}
} }

View file

@ -1,50 +1,74 @@
import { DB, Artist, Song, RefTo, Ref, Platforms } from "@euterpe.js/music-library"; import {
export const db = new DB DB,
Artist,
Song,
RefTo,
Ref,
Platforms
} from "@euterpe.js/music-library"
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
artists: [new Ref(RefTo.Artists, 2), new Ref(RefTo.Artists, 3), new Ref(RefTo.Artists, 4)], artists: [
duration: 252, new Ref(RefTo.Artists, 2),
name: "Star", new Ref(RefTo.Artists, 3),
remix_artists: [new Ref(RefTo.Artists, 5)], new Ref(RefTo.Artists, 4)
url: new URL("http://127.0.0.1:4200/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3") ],
}), duration: 252,
new Song({ name: "Star",
//If you don't like guessing the IDs, then this is also a way to do it remix_artists: [new Ref(RefTo.Artists, 5)],
artists: [new Ref(RefTo.Artists, db.artists.find((a) => a.name == "Jamie xx")!.id!)], url: new URL(
duration: 331, "http://127.0.0.1:4200/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3"
name: "Sleep Sound", )
url: new URL("http://127.0.0.1:4200/Jamie xx - Sleep Sound.mp3") }),
}), new Song({
new Song({ //If you don't like guessing the IDs, then this is also a way to do it
artists: [new Ref(RefTo.Artists, 1)], artists: [
duration: 75, new Ref(
name: "wish", RefTo.Artists,
url: new URL("http://127.0.0.1:4200/janz - wish.mp3") db.artists.find((a) => a.name == "Jamie xx")!.id!
}) )
]) ],
duration: 331,
name: "Sleep Sound",
url: new URL("http://127.0.0.1:4200/Jamie xx - Sleep Sound.mp3")
}),
new Song({
artists: [new Ref(RefTo.Artists, 1)],
duration: 75,
name: "wish",
url: new URL("http://127.0.0.1:4200/janz - wish.mp3")
})
])

View file

@ -1,115 +1,152 @@
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) music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then(
.then(() => { () => {
let is_seeking = false let is_seeking = false
change_current_song_text(db) 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
change_current_song_text(db) .try_new_song_async(db.songs[curr_song_id].url.pathname)
music_player.play_async().catch((err) => { console.log(err) }) .then(
}, (e) => { console.log(e) }) (s) => {
}) change_current_song_text(db)
document.querySelector("#next")?.addEventListener("click", () => { music_player.play_async().catch((err) => {
curr_song_id++ console.log(err)
if (curr_song_id > 2) curr_song_id = 0 })
music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then((s) => { },
change_current_song_text(db) (e) => {
music_player.play_async().catch((err) => { console.log(err) }) console.log(e)
}, (e) => { console.log(e) }) }
}) )
})
document.querySelector("#play")?.addEventListener("click", () => { document.querySelector("#next")?.addEventListener("click", () => {
music_player.play_async() curr_song_id++
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e)) if (curr_song_id > 2) curr_song_id = 0
}) music_player
document.querySelector("#pause")?.addEventListener("click", () => { .try_new_song_async(db.songs[curr_song_id].url.pathname)
music_player.pause() .then(
}) (s) => {
document.querySelector("#mute")?.addEventListener("click", () => { change_current_song_text(db)
music_player.mute() music_player.play_async().catch((err) => {
}) console.log(err)
document.querySelector("#unmute")?.addEventListener("click", () => { })
music_player.unmute() },
}) (e) => {
document.querySelector("#toggle-mute")?.addEventListener("click", () => { console.log(e)
music_player.mute_toggle() }
}) )
document.querySelector("#toggle-play")?.addEventListener("click", () => { })
music_player.play_toggle_async().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e))
})
document.querySelector("#volume")?.addEventListener("input", (e) => {
music_player.change_volume(e.target?.valueAsNumber)
})
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
is_seeking = true;
})
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => {
alert("Failed seeking! " + e)
})
is_seeking = false
})
// Subscriptions to AudioContext changes, eg. time..
music_player.subscribe_to_formatted_duration_time((time) => {
document.querySelector("#duration").innerHTML = time
document.querySelector("#seek").max = "" + music_player.get_current_duration()
})
music_player.subscribe_to_formatted_current_time_tick((time) => {
document.querySelector("#current").innerHTML = time
})
music_player.subscribe_to_time_tick((time) => {
if (is_seeking) return
document.querySelector("#seek").value = "" + time
})
}, (e) => console.log(e))
document.querySelector("#play")?.addEventListener("click", () => {
music_player.play_async().then(
() => {
console.log("Playing!")
},
(e) => alert("Failed to play, " + e)
)
})
document.querySelector("#pause")?.addEventListener("click", () => {
music_player.pause()
})
document.querySelector("#mute")?.addEventListener("click", () => {
music_player.mute()
})
document.querySelector("#unmute")?.addEventListener("click", () => {
music_player.unmute()
})
document
.querySelector("#toggle-mute")
?.addEventListener("click", () => {
music_player.mute_toggle()
})
document
.querySelector("#toggle-play")
?.addEventListener("click", () => {
music_player.play_toggle_async().then(
(s) => console.log("toggled play/pause"),
(e) => alert("failed to toggle pause/play!" + e)
)
})
document.querySelector("#volume")?.addEventListener("input", (e) => {
music_player.change_volume(e.target?.valueAsNumber)
})
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
is_seeking = true
})
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
music_player.try_seek_async(e.target?.valueAsNumber).then(
() => {
console.log("seeked to " + e.target?.valueAsNumber)
},
() => {
alert("Failed seeking! " + e)
}
)
is_seeking = false
})
// Subscriptions to AudioContext changes, eg. time..
music_player.subscribe_to_formatted_duration_time((time) => {
document.querySelector("#duration").innerHTML = time
document.querySelector("#seek").max =
"" + music_player.get_current_duration()
})
music_player.subscribe_to_formatted_current_time_tick((time) => {
document.querySelector("#current").innerHTML = time
})
music_player.subscribe_to_time_tick((time) => {
if (is_seeking) return
document.querySelector("#seek").value = "" + time
})
},
(e) => console.log(e)
)
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]
let final_text = "" let final_text = ""
for (const artist of curr_song.artists) { for (const artist of curr_song.artists) {
const curr_artist = artist.get(db) as Artist const curr_artist = artist.get(db) as Artist
final_text += curr_artist.name + ", " final_text += curr_artist.name + ", "
} }
final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", "
final_text += " - " + curr_song.name final_text += " - " + curr_song.name
if (curr_song.remix_artists.length > 0) { if (curr_song.remix_artists.length > 0) {
final_text += " (" final_text += " ("
for (const artist of curr_song.remix_artists) { for (const artist of curr_song.remix_artists) {
const curr_artist = artist.get(db) as Artist const curr_artist = artist.get(db) as Artist
if (curr_artist.links && curr_artist.links.length > 0) { if (curr_artist.links && curr_artist.links.length > 0) {
//returns "found a link! Spotify" //returns "found a link! Spotify"
console.log("found a link! " + Platforms[curr_artist.links[0][0]]) console.log(
"found a link! " + Platforms[curr_artist.links[0][0]]
)
const url = curr_artist.links[0][1] const url = curr_artist.links[0][1]
final_text += `<a href=${url}>${curr_artist.name}</a>, ` final_text += `<a href=${url}>${curr_artist.name}</a>, `
} else { } else {
final_text += curr_artist.name + ", " final_text += curr_artist.name + ", "
} }
} }
final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", "
final_text += " Remix)" final_text += " Remix)"
} }
elem_curr_song!.innerHTML = final_text elem_curr_song!.innerHTML = final_text
} }

View file

@ -1,299 +1,360 @@
export { export {
RefTo, RefTo,
Ref, Ref,
Song, Song,
Collection, Collection,
DB, DB,
Artist, Artist,
Platforms, Platforms,
CollectionType, CollectionType,
from_json from_json
} }
type ID = number type ID = number
enum RefTo { enum RefTo {
Artists, Artists,
Songs, Songs,
Collections Collections
} }
enum CollectionType { enum CollectionType {
Album = "Album", Album = "Album",
EP = "EP", EP = "EP",
Single = "Single", Single = "Single",
Playlist = "Playlist", Playlist = "Playlist",
Release = "Release", Release = "Release"
} }
enum Platforms { enum Platforms {
Youtube = "Youtube", Youtube = "Youtube",
Linktree = "Linktree", Linktree = "Linktree",
Bandcamp = "Bandcamp", Bandcamp = "Bandcamp",
Spotify = "Spotify", Spotify = "Spotify",
Portfolio = "Portfolio", Portfolio = "Portfolio",
BeatPort = "BeatPort", BeatPort = "BeatPort",
SoundCloud = "SoundCloud", SoundCloud = "SoundCloud",
Instagram = "Instagram", Instagram = "Instagram",
Patreon = "Patreon", Patreon = "Patreon",
Twitter = "Twitter", Twitter = "Twitter",
Facebook = "Facebook", Facebook = "Facebook"
} }
class Ref { class Ref {
constructor(public to: RefTo, public id: ID) { } constructor(public to: RefTo, public id: ID) {}
get(from: DB) { get(from: DB) {
switch (this.to) { switch (this.to) {
case RefTo.Artists: { case RefTo.Artists: {
return from.artists.find((artist) => artist.id == this.id) return from.artists.find((artist) => artist.id == this.id)
} }
case RefTo.Songs: { case RefTo.Songs: {
return from.songs.find((song) => song.id == this.id) return from.songs.find((song) => song.id == this.id)
} }
case RefTo.Collections: { case RefTo.Collections: {
return from.collections.find((col) => col.id == this.id) return from.collections.find((col) => col.id == this.id)
} }
} }
} }
} }
function ref_from_json(ref: any): Ref { function ref_from_json(ref: any): Ref {
return new Ref(ref.to, ref.id) return new Ref(ref.to, ref.id)
} }
interface SongConstructor { interface SongConstructor {
name: string name: string
artists?: Ref[] artists?: Ref[]
url: URL url: URL
duration?: number duration?: number
publish_date?: Date publish_date?: Date
remix_artists?: Ref[] remix_artists?: Ref[]
in_collection?: Ref in_collection?: Ref
cover?: URL cover?: URL
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 {
name: string name: string
artists: Ref[] artists: Ref[]
url: URL url: URL
duration?: number duration?: number
remix_artists: Ref[] remix_artists: Ref[]
publish_date?: Date publish_date?: Date
in_collection?: Ref in_collection?: Ref
cover?: URL cover?: URL
bpm?: number bpm?: number
key?: string key?: string
fft_data?: number[] fft_data?: number[]
metadata: any[] metadata: any[]
/** /**
* The ID is always there, don't worry :) * The ID is always there, don't worry :)
*/ */
id?: ID id?: ID
constructor(data: SongConstructor) { constructor(data: SongConstructor) {
this.name = data.name this.name = data.name
this.artists = data.artists || [] this.artists = data.artists || []
this.url = data.url this.url = data.url
this.duration = data.duration this.duration = data.duration
this.publish_date = data.publish_date this.publish_date = data.publish_date
this.remix_artists = data.remix_artists || [] this.remix_artists = data.remix_artists || []
this.in_collection = data.in_collection this.in_collection = data.in_collection
this.cover = data.cover this.cover = data.cover
this.bpm = data.bpm this.bpm = data.bpm
this.key = data.key this.key = data.key
this.fft_data = data.fft_data this.fft_data = data.fft_data
this.id = data.id this.id = data.id
this.metadata = data.metadata || [] this.metadata = data.metadata || []
} }
} }
interface ArtistConstructor { interface ArtistConstructor {
name: string, name: string
pfp?: URL pfp?: URL
songs?: Ref[] songs?: Ref[]
collections?: Ref[] collections?: Ref[]
links?: [Platforms, URL][] links?: [Platforms, URL][]
id?: ID id?: ID
metadata?: any[] metadata?: any[]
} }
class Artist { class Artist {
name = "" name = ""
pfp?: URL pfp?: URL
songs: Ref[] songs: Ref[]
collections: Ref[] collections: Ref[]
links?: [Platforms, URL][] links?: [Platforms, URL][]
metadata: any[] metadata: any[]
/** /**
* The ID is always there, don't worry :) * The ID is always there, don't worry :)
*/ */
id?: ID id?: ID
constructor(data: ArtistConstructor) { constructor(data: ArtistConstructor) {
this.name = data.name this.name = data.name
this.pfp = data.pfp this.pfp = data.pfp
this.songs = data.songs || [] this.songs = data.songs || []
this.collections = data.collections || [] this.collections = data.collections || []
this.links = data.links this.links = data.links
this.id = data.id this.id = data.id
this.metadata = data.metadata || [] this.metadata = data.metadata || []
} }
} }
interface CollectionConstructor { interface CollectionConstructor {
artists: Ref[] artists: Ref[]
songs: Ref[] songs: Ref[]
cover?: URL cover?: URL
duration?: number duration?: number
publish_date?: Date publish_date?: Date
id?: ID id?: ID
metadata?: any[] metadata?: any[]
name?: string name?: string
type?: CollectionType type?: CollectionType
} }
class Collection { class Collection {
name?: string name?: string
type?: CollectionType type?: CollectionType
artists: Ref[] artists: Ref[]
songs: Ref[] songs: Ref[]
cover?: URL cover?: URL
duration?: number duration?: number
publish_date?: Date publish_date?: Date
metadata: any[] metadata: any[]
/** /**
* The ID is always there, don't worry :) * The ID is always there, don't worry :)
*/ */
id?: ID id?: ID
constructor(data: CollectionConstructor) { constructor(data: CollectionConstructor) {
this.artists = data.artists this.artists = data.artists
this.songs = data.songs this.songs = data.songs
this.cover = data.cover this.cover = data.cover
this.duration = data.duration this.duration = data.duration
this.publish_date = data.publish_date this.publish_date = data.publish_date
this.id = data.id this.id = data.id
this.name = data.name this.name = data.name
this.metadata = data.metadata ? data.metadata : [] this.metadata = data.metadata ? data.metadata : []
} }
} }
class DB { class DB {
artists: Artist[] = [] artists: Artist[] = []
songs: Song[] = [] songs: Song[] = []
collections: Collection[] = [] collections: Collection[] = []
add(song: Song[]): void add(song: Song[]): void
add(artist: Artist[]): void add(artist: Artist[]): void
add(collection: Collection[]): void add(collection: Collection[]): void
add(mix: (Song | Artist | Collection)[]): void add(mix: (Song | Artist | Collection)[]): void
add(stuff: Artist[] | Collection[] | Song[] | (Song | Artist | Collection)[]) { add(
/** All of this adds refrences to the other side of whatever is being added. stuff: Artist[] | Collection[] | Song[] | (Song | Artist | Collection)[]
* eg. adding song with refrence to artist, adds refrence of song to artist ) {
* and adds incremental ids /** All of this adds refrences to the other side of whatever is being added.
*/ * eg. adding song with refrence to artist, adds refrence of song to artist
let inputs * and adds incremental ids
if (typeof stuff[Symbol.iterator] != "function") { */
inputs = [stuff] let inputs
} else { if (typeof stuff[Symbol.iterator] != "function") {
inputs = stuff inputs = [stuff]
} } else {
for (const input of inputs) { inputs = stuff
if (input instanceof Artist) { }
const artist = input as Artist for (const input of inputs) {
if (!artist.id) artist.id = this.artists.length if (input instanceof Artist) {
const artist = input as Artist
if (!artist.id) artist.id = this.artists.length
for (const song_ref of artist.songs) { for (const song_ref of artist.songs) {
const curr_song = song_ref.get(this) as Song const curr_song = song_ref.get(this) as Song
curr_song?.artists.push(new Ref(RefTo.Artists, artist.id)) curr_song?.artists.push(new Ref(RefTo.Artists, artist.id))
} }
for (const col_ref of artist.collections) { for (const col_ref of artist.collections) {
const curr_col = col_ref.get(this) as Collection const curr_col = col_ref.get(this) as Collection
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) {
const col = input as Collection
if (!col.id) col.id = this.collections.length
else if (input instanceof Collection) { for (const song_ref of col.songs) {
const col = input as Collection const curr_song = song_ref.get(this) as Song
if (!col.id) col.id = this.collections.length curr_song.in_collection = new Ref(RefTo.Collections, col.id)
}
for (const artist_ref of col.artists) {
const curr_artist = artist_ref.get(this) as Artist
curr_artist.collections.push(
new Ref(RefTo.Collections, col.id)
)
}
this.collections.push(col)
} else if (input instanceof Song) {
const song = input as Song
if (!song.id) song.id = this.songs.length
for (const song_ref of col.songs) { if (song.in_collection) {
const curr_song = song_ref.get(this) as Song const curr_col = song.in_collection.get(this) as Collection
curr_song.in_collection = new Ref(RefTo.Collections, col.id) curr_col.songs.push(new Ref(RefTo.Songs, song.id))
} song.artists.forEach((artist) =>
for (const artist_ref of col.artists) { curr_col.artists.push(
const curr_artist = artist_ref.get(this) as Artist new Ref(RefTo.Artists, artist.get(this)!.id!)
curr_artist.collections.push(new Ref(RefTo.Collections, col.id)) )
} )
this.collections.push(col) song.remix_artists.forEach((artist) =>
} curr_col.artists.push(
new Ref(RefTo.Artists, artist.get(this)!.id!)
)
)
}
else if (input instanceof Song) { for (const artist_ref of song.artists) {
const song = input as Song const curr_artist = artist_ref.get(this) as Artist
if (!song.id) song.id = this.songs.length curr_artist.songs.push(new Ref(RefTo.Songs, song.id))
}
if (song.in_collection) {
const curr_col = song.in_collection.get(this) as Collection
curr_col.songs.push(new Ref(RefTo.Songs, song.id))
song.artists.forEach((artist) => curr_col.artists.push(new Ref(RefTo.Artists, artist.get(this)!.id!)))
song.remix_artists.forEach((artist) => curr_col.artists.push(new Ref(RefTo.Artists, artist.get(this)!.id!)))
}
for (const artist_ref of song.artists) {
const curr_artist = artist_ref.get(this) as Artist
curr_artist.songs.push(new Ref(RefTo.Songs, song.id))
}
for (const artist_ref of song.remix_artists) {
const curr_artist = artist_ref.get(this) as Artist
curr_artist.songs.push(new Ref(RefTo.Songs, song.id))
}
this.songs.push(song)
}
}
this.songs.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!)
}
for (const artist_ref of song.remix_artists) {
const curr_artist = artist_ref.get(this) as Artist
curr_artist.songs.push(new Ref(RefTo.Songs, song.id))
}
this.songs.push(song)
}
}
this.songs.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!)
}
} }
function from_json(db_stringified: { artists?: any, songs?: any, collections?: any }): DB { function from_json(db_stringified: {
const db = new DB artists?: any
if (db_stringified.artists) { songs?: any
for (const artist of db_stringified.artists) { collections?: any
if (artist.songs) artist.songs = artist.songs.map((e: any) => ref_from_json(e)) }): DB {
if (artist.collections) artist.collections = artist.collections.map((e: any) => ref_from_json(e)) const db = new DB()
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 (db_stringified.artists) {
if (artist.publish_date) artist.publish_date = new Date(JSON.parse(artist.publish_date)) for (const artist of db_stringified.artists) {
if (artist.id) artist.id = artist.id as ID if (artist.songs)
try { if (artist.pfp) artist.pfp = new URL(artist.pfp) } artist.songs = artist.songs.map((e: any) => ref_from_json(e))
catch (e) { console.error(e), console.error("failed to parse artist URL") } if (artist.collections)
db.artists.push(artist) artist.collections = artist.collections.map((e: any) =>
} ref_from_json(e)
} )
if (db_stringified.songs) { if (artist.links)
for (const song of db_stringified.songs) { artist.links = artist.links.map((e: any) => {
try { if (song.url) song.url = new URL(song.url) } catch (e) { console.error("failed to parse song.url" + e) } try {
if (song.artists) song.artists = song.artists.map((e: any) => ref_from_json(e)) ;[e[0] as Platforms, new URL(e[1])]
if (song.remix_artists) song.remix_artists = song.remix_artists.map((e: any) => ref_from_json(e)) } catch (e) {
if (song.in_collection) song.in_collection = ref_from_json(song.in_collection) console.log(e)
try { if (song.cover) song.cover = new URL(song.cover) } }
catch (e) { 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)) } if (artist.publish_date)
catch (e) { console.error(e), console.error("Failed to song cover url") } artist.publish_date = new Date(JSON.parse(artist.publish_date))
if (song.id) song.id = song.id as ID if (artist.id) artist.id = artist.id as ID
db.songs.push(song) try {
} if (artist.pfp) artist.pfp = new URL(artist.pfp)
} } catch (e) {
if (db_stringified.collections) { console.error(e), console.error("failed to parse artist URL")
for (const collection of db_stringified.collections) { }
if (collection.artists) collection.artists = collection.artists.map((e: any) => ref_from_json(e)) db.artists.push(artist)
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) }
try { if (collection.publish_date) collection.publish_date = new Date(JSON.parse(collection.publish_date)) } if (db_stringified.songs) {
catch (e) { console.error(e), console.error("Failed to parse date") } for (const song of db_stringified.songs) {
try { if (collection.cover) collection.cover = new URL(collection.cover) } try {
catch (e) { console.error(e), console.error("failed to parse collection cover url") } if (song.url) song.url = new URL(song.url)
if (collection.id) collection.id = collection.id as ID } catch (e) {
db.collections.push(collection) console.error("failed to parse song.url" + e)
} }
} if (song.artists)
return db 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.in_collection)
song.in_collection = ref_from_json(song.in_collection)
try {
if (song.cover) song.cover = new URL(song.cover)
} catch (e) {
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
db.songs.push(song)
}
}
if (db_stringified.collections) {
for (const collection of db_stringified.collections) {
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.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)
)
} catch (e) {
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
db.collections.push(collection)
}
}
return db
} }
// const db = new DB // const db = new DB
// db.add( // db.add(
@ -312,4 +373,4 @@ function from_json(db_stringified: { artists?: any, songs?: any, collections?: a
// console.dir(db, { depth: null }) // console.dir(db, { depth: null })
// const res = db.artists[0].songs[0].get(db) as Song // const res = db.artists[0].songs[0].get(db) as Song
// console.log(`${db.artists[0].name} has song ${db.songs[0].name}? : ${res.name} is there!`) // console.log(`${db.artists[0].name} has song ${db.songs[0].name}? : ${res.name} is there!`)

View file

@ -1,58 +1,85 @@
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")) music_player
.then(() => { .try_new_song(
let is_seeking = false encodeURI(
document.querySelector("#play")?.addEventListener("click", () => { "http://" +
//const analyser_node = music_player_builder.add_analyser() window.location.host +
music_player.try_play() "/nuphory - NVISION (EXTENDED MIX).ogg"
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e)) )
}) )
document.querySelector("#pause")?.addEventListener("click", () => { .then(
music_player.pause() () => {
}) let is_seeking = false
document.querySelector("#mute")?.addEventListener("click", () => { document.querySelector("#play")?.addEventListener("click", () => {
music_player.mute() //const analyser_node = music_player_builder.add_analyser()
}) music_player.try_play().then(
document.querySelector("#unmute")?.addEventListener("click", () => { () => {
music_player.unmute() console.log("Playing!")
}) },
document.querySelector("#toggle-mute")?.addEventListener("click", () => { (e) => alert("Failed to play, " + e)
music_player.mute_toggle() )
}) })
document.querySelector("#toggle-play")?.addEventListener("click", () => { document.querySelector("#pause")?.addEventListener("click", () => {
music_player.try_play_toggle().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e)) music_player.pause()
}) })
document.querySelector("#volume")?.addEventListener("input", (e) => { document.querySelector("#mute")?.addEventListener("click", () => {
music_player.change_volume(e.target?.valueAsNumber) music_player.mute()
}) })
document.querySelector("#seek")?.addEventListener("mousedown", (e) => { document.querySelector("#unmute")?.addEventListener("click", () => {
is_seeking = true; music_player.unmute()
}) })
document.querySelector("#seek")?.addEventListener("mouseup", (e) => { document
try { .querySelector("#toggle-mute")
music_player.try_seek(e.target?.valueAsNumber) ?.addEventListener("click", () => {
console.log("seeked to " + e.target?.valueAsNumber) music_player.mute_toggle()
} catch (e) { })
alert("Failed seeking! " + e) document
} .querySelector("#toggle-play")
is_seeking = false ?.addEventListener("click", () => {
}) music_player.try_play_toggle().then(
// Subscriptions to AudioContext changes, eg. time.. (s) => console.log("toggled play/pause"),
music_player.on_duration_formatted((time) => { (e) => alert("failed to toggle pause/play!" + e)
document.querySelector("#duration")!.innerHTML = time )
document.querySelector("#seek")!.max = "" + music_player.current_song_duration })
}) document
music_player.on_time_tick_formatted((time) => { .querySelector("#volume")
document.querySelector("#current")!.innerHTML = time ?.addEventListener("input", (e) => {
}) music_player.change_volume(e.target?.valueAsNumber)
music_player.on_time_tick((time) => { })
if (is_seeking) return document
document.querySelector("#seek")!.value = "" + time .querySelector("#seek")
}) ?.addEventListener("mousedown", (e) => {
is_seeking = true
}, (e) => console.log(e)) })
document
.querySelector("#seek")
?.addEventListener("mouseup", (e) => {
try {
music_player.try_seek(e.target?.valueAsNumber)
console.log("seeked to " + e.target?.valueAsNumber)
} catch (e) {
alert("Failed seeking! " + e)
}
is_seeking = false
})
// Subscriptions to AudioContext changes, eg. time..
music_player.on_duration_formatted((time) => {
document.querySelector("#duration")!.innerHTML = time
document.querySelector("#seek")!.max =
"" + music_player.current_song_duration
})
music_player.on_time_tick_formatted((time) => {
document.querySelector("#current")!.innerHTML = time
})
music_player.on_time_tick((time) => {
if (is_seeking) return
document.querySelector("#seek")!.value = "" + time
})
},
(e) => console.log(e)
)

View file

@ -1,436 +1,489 @@
export enum SubscribeEvents { export enum SubscribeEvents {
CurrentTimeTick, CurrentTimeTick,
FormattedDurationTick, FormattedDurationTick,
FormattedCurrentTimeTick, FormattedCurrentTimeTick
} }
class PubSub { class PubSub {
//el = event listener //el = event listener
el_current_time_tick: Array<(data: any) => void> = [] el_current_time_tick: Array<(data: any) => void> = []
el_formatted_duration_tick: Array<(data: any) => void> = [] el_formatted_duration_tick: Array<(data: any) => void> = []
el_formatted_current_time_tick: Array<(data: any) => void> = [] el_formatted_current_time_tick: Array<(data: any) => void> = []
subscribe(event_name: SubscribeEvents, func: (data: any) => void) { subscribe(event_name: SubscribeEvents, func: (data: any) => void) {
switch (event_name) { switch (event_name) {
case SubscribeEvents.CurrentTimeTick: { case SubscribeEvents.CurrentTimeTick: {
this.el_current_time_tick.push(func) this.el_current_time_tick.push(func)
break break
} }
case SubscribeEvents.FormattedDurationTick: { case SubscribeEvents.FormattedDurationTick: {
this.el_formatted_duration_tick.push(func) this.el_formatted_duration_tick.push(func)
break break
} }
case SubscribeEvents.FormattedCurrentTimeTick: { case SubscribeEvents.FormattedCurrentTimeTick: {
this.el_formatted_current_time_tick.push(func) this.el_formatted_current_time_tick.push(func)
break break
} }
} }
} }
unsubscribe(event_name: SubscribeEvents, func: (data: any) => void) { unsubscribe(event_name: SubscribeEvents, func: (data: any) => void) {
switch (event_name) { switch (event_name) {
case SubscribeEvents.CurrentTimeTick: { case SubscribeEvents.CurrentTimeTick: {
if (this.el_current_time_tick.includes(func)) { if (this.el_current_time_tick.includes(func)) {
this.el_current_time_tick.splice(this.el_current_time_tick.indexOf(func), 1) this.el_current_time_tick.splice(
} this.el_current_time_tick.indexOf(func),
break 1
} )
case SubscribeEvents.FormattedDurationTick: { }
if (this.el_formatted_duration_tick.includes(func)) { break
this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1) }
} case SubscribeEvents.FormattedDurationTick: {
break if (this.el_formatted_duration_tick.includes(func)) {
} this.el_formatted_duration_tick.splice(
case SubscribeEvents.FormattedCurrentTimeTick: { this.el_formatted_duration_tick.indexOf(func),
if (this.el_formatted_duration_tick.includes(func)) { 1
this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1) )
} }
break break
} }
} case SubscribeEvents.FormattedCurrentTimeTick: {
} if (this.el_formatted_duration_tick.includes(func)) {
emit(event_name: SubscribeEvents, data: any) { this.el_formatted_duration_tick.splice(
switch (event_name) { this.el_formatted_duration_tick.indexOf(func),
case SubscribeEvents.CurrentTimeTick: { 1
this.el_current_time_tick.forEach((func) => { )
func(data) }
}) break
break }
} }
case SubscribeEvents.FormattedDurationTick: { }
this.el_formatted_duration_tick.forEach((func) => { emit(event_name: SubscribeEvents, data: any) {
func(data) switch (event_name) {
}) case SubscribeEvents.CurrentTimeTick: {
break this.el_current_time_tick.forEach((func) => {
} func(data)
case SubscribeEvents.FormattedCurrentTimeTick: { })
this.el_formatted_current_time_tick.forEach((func) => { break
func(data) }
}) case SubscribeEvents.FormattedDurationTick: {
break this.el_formatted_duration_tick.forEach((func) => {
} func(data)
} })
} break
}
case SubscribeEvents.FormattedCurrentTimeTick: {
this.el_formatted_current_time_tick.forEach((func) => {
func(data)
})
break
}
}
}
} }
/* For old browsers */ /* For old browsers */
declare global { declare global {
interface Window { interface Window {
webkitAudioContext: typeof AudioContext webkitAudioContext: typeof AudioContext
} }
} }
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
}
mute_toggle() { mute_toggle() {
if (this.gain.gain.value == 0) { if (this.gain.gain.value == 0) {
this.unmute() this.unmute()
} else { } else {
this.mute() this.mute()
} }
} }
mute() { mute() {
this.#volume_cache = this.gain.gain.value this.#volume_cache = this.gain.gain.value
/* Gentler mute, doesn't pop /* Gentler mute, doesn't pop
gain.gain.linearRampToValueAtTime( gain.gain.linearRampToValueAtTime(
0, 0,
audio_context.currentTime + 0.1 audio_context.currentTime + 0.1
);*/ );*/
this.volume = this.gain.gain.value = 0 this.volume = this.gain.gain.value = 0
} }
unmute() { unmute() {
this.volume = this.gain.gain.value = this.#volume_cache this.volume = this.gain.gain.value = this.#volume_cache
} }
change_volume(volume_i: number) { change_volume(volume_i: number) {
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_async. 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 also resume playback if player is paused (by finishing the song etc)
* @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) {
if (this.audio_context.state !== "running") { if (this.audio_context.state !== "running") {
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 : " +
if (this.audio_element.paused) await this.try_play() this.audio_context.state
this.audio_element.currentTime = new_time )
} }
if (this.audio_element.paused) await this.try_play()
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_async or seek_async 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_async. 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() {
if (this.audio_context.state !== "running") { if (this.audio_context.state !== "running") {
await this.audio_context.resume() await this.audio_context.resume()
} }
if (this.audio_element.paused) { if (this.audio_element.paused) {
try { try {
await this.audio_element.play() await this.audio_element.play()
this.is_playing = true this.is_playing = true
} catch (e) { } catch (e) {
this.is_playing = false this.is_playing = false
throw e throw e
} }
} else { } else {
this.audio_element.pause() this.audio_element.pause()
this.is_playing = false this.is_playing = false
} }
} }
/** /**
* Unsafe, can just fail. Use try_play_toggle unless you don't care about the result. * Unsafe, can just fail. Use try_play_toggle unless you don't care about the result.
*/ */
play_toggle() { play_toggle() {
if (this.audio_element.paused) { if (this.audio_element.paused) {
this.is_playing = true this.is_playing = true
this.audio_element.play().catch((r) => { this.audio_element.play().catch((r) => {
this.is_playing = false this.is_playing = false
throw r throw r
}) })
} else { } else {
this.is_playing = false this.is_playing = false
this.audio_element.pause() this.audio_element.pause()
} }
} }
/** /**
* Safer play. Normal play will try to start the player even if the track hasn't started yet, or was previously suspended/closed * Safer play. Normal play 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() { async try_play() {
if (this.is_playing) return if (this.is_playing) return
if (this.audio_context.state !== "running") { if (this.audio_context.state !== "running") {
await this.audio_context.resume() await this.audio_context.resume()
} }
if (this.audio_element.paused) { if (this.audio_element.paused) {
try { try {
await this.audio_element.play() await this.audio_element.play()
this.is_playing = true this.is_playing = true
} catch (e) { } catch (e) {
this.is_playing = false this.is_playing = false
throw e throw e
} }
} }
} }
/** /**
* 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_async or try_play_async unless you don't care about the result.
*/ */
play() { play() {
if (this.is_playing) return if (this.is_playing) return
this.audio_element.play().catch(() => { this.audio_element.play().catch(() => {
this.is_playing = false this.is_playing = false
}) })
} }
/** /**
* Safe technically. Even if audioContext is suspended or closed it will pretend that it paused. * Safe technically. Even if audioContext is suspended or closed it will pretend that it paused.
*/ */
pause() { pause() {
this.audio_element.pause() this.audio_element.pause()
this.is_playing = false this.is_playing = false
} }
/** /**
* 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. Need to call try_play_async() 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) {
if (this.audio_context.state !== "running") { if (this.audio_context.state !== "running") {
try { try {
await this.audio_context.resume() await this.audio_context.resume()
} catch (e) { } catch (e) {
console.log("loading new song - couldn't resume context before hand", e) console.log(
} "loading new song - couldn't resume context before hand",
} e
return new Promise<void>((resolve, reject) => { )
this.audio_element.src = this.current_song_path = path }
//Found out today about this. Such a nice new way to mass remove event listeners! }
const controller = new AbortController(); return new Promise<void>((resolve, reject) => {
this.audio_element.src = this.current_song_path = path
//Found out today about this. Such a nice new way to mass remove event listeners!
const controller = new AbortController()
this.audio_element.addEventListener("canplaythrough", function canplay_listener() { this.audio_element.addEventListener(
controller.abort() "canplaythrough",
}, { signal: controller.signal }) function canplay_listener() {
controller.abort()
},
{ signal: controller.signal }
)
this.audio_element.addEventListener("error", function error_listener() { this.audio_element.addEventListener(
controller.abort("new src error") "error",
}, { signal: controller.signal }) function error_listener() {
controller.abort("new src error")
},
{ signal: controller.signal }
)
this.audio_element.addEventListener("stalled", function stalled_listener() { this.audio_element.addEventListener(
controller.abort("new src stalled") "stalled",
}, { signal: controller.signal }) function stalled_listener() {
controller.abort("new src stalled")
},
{ 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", (r) => {
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")
resolve() reject(new Error(controller.signal.reason))
}) 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_async() unless you don't care about the result
new_song(path: string) { */
this.audio_element.src = this.current_song_path = path new_song(path: string) {
this.current_song_duration = this.audio_element.duration this.audio_element.src = this.current_song_path = path
} this.current_song_duration = this.audio_element.duration
/** }
* Will parse the duration of the song to make it easy to display in UI /**
* If somethings undefined it returns "0:00" * Will parse the duration of the song to make it easy to display in UI
*/ * If somethings undefined it returns "0:00"
get_formatted_duration() { */
const dur = this.audio_element.duration get_formatted_duration() {
this.current_song_duration = this.audio_element.duration const dur = this.audio_element.duration
this.current_song_duration = this.audio_element.duration
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
* If somethings undefined it returns "0:00" * If somethings undefined it returns "0:00"
*/ */
get_formatted_current_time() { get_formatted_current_time() {
const curr = this.audio_element.currentTime const curr = this.audio_element.currentTime
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))
if (this.audio_element.ended) this.is_playing = false if (this.audio_element.ended) this.is_playing = false
if (this.audio_element.paused) this.is_playing == false if (this.audio_element.paused) this.is_playing == false
// if use reactively changes volume directly // if use reactively changes volume directly
this.gain.gain.value = this.volume this.gain.gain.value = this.volume
this.time = this.audio_element.currentTime this.time = this.audio_element.currentTime
if (this.#pub_sub.el_current_time_tick.length == 0) cancelAnimationFrame(request_id) if (this.#pub_sub.el_current_time_tick.length == 0)
this.#pub_sub.emit(SubscribeEvents.CurrentTimeTick, this.time) cancelAnimationFrame(request_id)
} this.#pub_sub.emit(SubscribeEvents.CurrentTimeTick, this.time)
#emit_duration_fmt() { }
const request_id = requestAnimationFrame(this.#emit_duration_fmt.bind(this)) #emit_duration_fmt() {
const time = this.get_formatted_duration() const request_id = requestAnimationFrame(
if (this.#pub_sub.el_formatted_duration_tick.length == 0) cancelAnimationFrame(request_id) this.#emit_duration_fmt.bind(this)
this.#pub_sub.emit(SubscribeEvents.FormattedDurationTick, time) )
} const time = this.get_formatted_duration()
#emit_time_fmt() { if (this.#pub_sub.el_formatted_duration_tick.length == 0)
const request_id = requestAnimationFrame(this.#emit_time_fmt.bind(this)) cancelAnimationFrame(request_id)
const time = this.get_formatted_current_time() this.#pub_sub.emit(SubscribeEvents.FormattedDurationTick, time)
if (this.#pub_sub.el_formatted_current_time_tick.length == 0) cancelAnimationFrame(request_id) }
this.#pub_sub.emit(SubscribeEvents.FormattedCurrentTimeTick, time) #emit_time_fmt() {
} const request_id = requestAnimationFrame(this.#emit_time_fmt.bind(this))
/** const time = this.get_formatted_current_time()
* Will give current time every animation frame if (this.#pub_sub.el_formatted_current_time_tick.length == 0)
*/ cancelAnimationFrame(request_id)
on_time_tick(callback: (data: any) => void) { this.#pub_sub.emit(SubscribeEvents.FormattedCurrentTimeTick, time)
this.#pub_sub.subscribe(SubscribeEvents.CurrentTimeTick, callback) }
this.#emit_time() /**
} * Will give current time every animation frame
*/
on_time_tick(callback: (data: any) => void) {
this.#pub_sub.subscribe(SubscribeEvents.CurrentTimeTick, callback)
this.#emit_time()
}
/** /**
* Will give formatted current time via get_formatted_current_time() every animation frame * Will give formatted current time via get_formatted_current_time() every animation frame
*/ */
on_time_tick_formatted(callback: (data: any) => void) { on_time_tick_formatted(callback: (data: any) => void) {
this.#pub_sub.subscribe(SubscribeEvents.FormattedCurrentTimeTick, callback) this.#pub_sub.subscribe(
this.#emit_time_fmt() SubscribeEvents.FormattedCurrentTimeTick,
} callback
/** )
* Will give formatted duration time via get_formatted_duration() every animation frame this.#emit_time_fmt()
*/ }
on_duration_formatted(callback: (data: any) => void) { /**
this.#pub_sub.subscribe(SubscribeEvents.FormattedDurationTick, callback) * Will give formatted duration time via get_formatted_duration() every animation frame
this.#emit_duration_fmt() */
} on_duration_formatted(callback: (data: any) => void) {
this.#pub_sub.subscribe(SubscribeEvents.FormattedDurationTick, callback)
this.#emit_duration_fmt()
}
} }
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 )
* will throw if audio_element is undefined (stupid vue setup amirite?) * will throw if audio_element is undefined (stupid vue setup amirite?)
* will throw if user has not interacted with the page yet (Can't initiate AudioContext) * will throw if user has not interacted with the page yet (Can't initiate AudioContext)
*/ */
constructor(private audio_element: HTMLAudioElement) { constructor(private audio_element: HTMLAudioElement) {
if (audio_element === undefined) throw Error("audio_element was undefined") if (audio_element === undefined)
// ↓ For old browsers throw Error("audio_element was undefined")
const AudioContext = window.AudioContext || window.webkitAudioContext; // ↓ For old browsers
this.#audio_context = new AudioContext() const AudioContext = window.AudioContext || window.webkitAudioContext
this.#track = this.#audio_context.createMediaElementSource(audio_element) this.#audio_context = new AudioContext()
this.#gain = this.#audio_context.createGain() this.#track =
} this.#audio_context.createMediaElementSource(audio_element)
/** this.#gain = this.#audio_context.createGain()
* For external use, not kept inside player after connection. }
* @returns {AnalyserNode} /**
*/ * For external use, not kept inside player after connection.
add_analyser() { * @returns {AnalyserNode}
const analyser = this.#audio_context.createAnalyser() */
!this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser) add_analyser() {
this.#prev_node = analyser const analyser = this.#audio_context.createAnalyser()
return analyser !this.#prev_node
} ? this.#track.connect(analyser)
/** : this.#prev_node.connect(analyser)
* For external use, not kept inside player after connection. this.#prev_node = analyser
* @returns {StereoPannerNode} return analyser
*/ }
add_stereo_panner_node() { /**
const panner = this.#audio_context.createStereoPanner() * For external use, not kept inside player after connection.
!this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner) * @returns {StereoPannerNode}
this.#prev_node = panner */
return panner add_stereo_panner_node() {
} const panner = this.#audio_context.createStereoPanner()
/** !this.#prev_node
* For external use, not kept inside player after connection. ? this.#track.connect(panner)
* @returns {StereoPannerNode} : this.#prev_node.connect(panner)
*/ this.#prev_node = panner
add_wave_shaper_node() { return panner
const shaper = this.#audio_context.createWaveShaper() }
!this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper) /**
this.#prev_node = shaper * For external use, not kept inside player after connection.
return shaper * @returns {StereoPannerNode}
} */
/** add_wave_shaper_node() {
* For additional trickery, you can connect your own node. const shaper = this.#audio_context.createWaveShaper()
*/ !this.#prev_node
connect_custom_node(node: AudioNode) { ? this.#track.connect(shaper)
!this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node) : this.#prev_node.connect(shaper)
this.#prev_node = node this.#prev_node = shaper
} return shaper
/** }
* Only use if you need to connect the #gain before another node, /**
* eg. if you want the analyser nodes output to be affected by user #gain * For additional trickery, you can connect your own node.
*/ */
connect_gain() { connect_custom_node(node: AudioNode) {
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) !this.#prev_node
this.#prev_node = this.#gain ? this.#track.connect(node)
this.#is_gain_connected = true : this.#prev_node.connect(node)
} this.#prev_node = node
/** }
* Finishes the build /**
* @returns {Euterpe} * Only use if you need to connect the #gain before another node,
*/ * eg. if you want the analyser nodes output to be affected by user #gain
build() { */
if (!this.#is_gain_connected) { connect_gain() {
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) !this.#prev_node
this.#prev_node = this.#gain ? this.#track.connect(this.#gain)
} : this.#prev_node.connect(this.#gain)
this.#prev_node.connect(this.#audio_context.destination) this.#prev_node = this.#gain
return new MusicPlayer(this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume) this.#is_gain_connected = true
} }
} /**
* Finishes the build
* @returns {Euterpe}
*/
build() {
if (!this.#is_gain_connected) {
!this.#prev_node
? this.#track.connect(this.#gain)
: this.#prev_node.connect(this.#gain)
this.#prev_node = this.#gain
}
this.#prev_node.connect(this.#audio_context.destination)
return new MusicPlayer(
this.#audio_context,
this.audio_element,
this.#track,
this.#gain,
this.#volume
)
}
}

View file

@ -1,4 +1,15 @@
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
fs.writeFile('songs_list.ts', `export const songs = ` + JSON.stringify(songs), 'utf8', () => { 1 + 1 }) .create()
.path("../public/samples")
.ext(["ogg"])
.findSync()
fs.writeFile(
"songs_list.ts",
`export const songs = ` + JSON.stringify(songs),
"utf8",
() => {
1 + 1
}
)

View file

@ -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("\\") ? "\\" : "/"
@ -13,22 +13,27 @@ export function generate_db() {
for (let i = 0; i < songs.length; i++) { for (let i = 0; i < songs.length; i++) {
const song = songs[i] const song = songs[i]
const last_i = song.lastIndexOf(path_char) const last_i = song.lastIndexOf(path_char)
const collection_name = song.slice(song.slice(0, last_i).lastIndexOf(path_char) + 1, last_i) const collection_name = song.slice(
song.slice(0, last_i).lastIndexOf(path_char) + 1,
last_i
)
/* /*
const foreforelast_i = song.slice(0, forelast_i - 1) const foreforelast_i = song.slice(0, forelast_i - 1)
const foreforeforelast_i = song.slice(0, foreforelast_i - 1).lastIndexOf("\\") const foreforeforelast_i = song.slice(0, foreforelast_i - 1).lastIndexOf("\\")
*/ */
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([
name: collection_name, new Collection({
songs: [], name: collection_name,
artists: [], songs: [],
})]) 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! })
} }
@ -39,11 +44,15 @@ export function generate_db() {
const last_i = song.song.lastIndexOf(path_char) const last_i = song.song.lastIndexOf(path_char)
const name = song.song.slice(last_i + 1) const name = song.song.slice(last_i + 1)
const song_url = song.song.slice(song.song.indexOf(`public${path_char}`) + 7) const song_url = song.song.slice(
song.song.indexOf(`public${path_char}`) + 7
)
const db_song = new Song({ const db_song = new Song({
name: name.slice(0, name.lastIndexOf(".")), name: name.slice(0, name.lastIndexOf(".")),
artists: [], artists: [],
url: new URL(`${window.location.href}${song_url}`.replaceAll("\\", "/")), url: new URL(
`${window.location.href}${song_url}`.replaceAll("\\", "/")
),
duration: 0, duration: 0,
remix_artists: [], remix_artists: [],
in_collection: new Ref(RefTo.Collections, song.collection_id) in_collection: new Ref(RefTo.Collections, song.collection_id)

View file

@ -1,8 +1,14 @@
import { DB, from_json } from "@euterpe.js/music-library" 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
@ -17,126 +23,163 @@ audioContextAnalyser.smoothingTimeConstant = 0
const analyserBufferLength = audioContextAnalyser.frequencyBinCount const analyserBufferLength = audioContextAnalyser.frequencyBinCount
const FFTDataArray = new Float32Array(analyserBufferLength) 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()
download(JSON.stringify(result.db), "db.json", "text/plain") download(JSON.stringify(result.db), "db.json", "text/plain")
}) })
document.getElementById("create-svg")!.addEventListener("click", (ev) => { document.getElementById("create-svg")!.addEventListener("click", (ev) => {
audioContext.resume() audioContext.resume()
svg() svg()
}) })
document.getElementById("upload")!.addEventListener("change", (ev) => { 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
for (const song of new_db.songs) { for (const song of new_db.songs) {
if (song.fft_data) { if (song.fft_data) {
for (let i = 0; i < song.fft_data.length; i++) { for (let i = 0; i < song.fft_data.length; i++) {
if (song.fft_data[i] === null || song.fft_data[i] === undefined) song.fft_data[i] = -Infinity if (
} song.fft_data[i] === null ||
} song.fft_data[i] === undefined
} )
result = { db: new_db, analyzer_node: audioContextAnalyser } song.fft_data[i] = -Infinity
} }
}
}
result = { db: new_db, analyzer_node: audioContextAnalyser }
}
}) })
async function svg() { async function svg() {
if (!result) { if (!result) {
alert("not analyzed yet!") alert("not analyzed yet!")
return return
} }
console.log("Creating svgs...") console.log("Creating svgs...")
const canvas_wrapper = document.querySelector(".canvas-wrapper") as HTMLElement const canvas_wrapper = document.querySelector(
".canvas-wrapper"
) as HTMLElement
const waveform_canvas = document.querySelector("#waveform-canvas")?.cloneNode() as SVGSVGElement const waveform_canvas = document
.querySelector("#waveform-canvas")
?.cloneNode() as SVGSVGElement
canvas_wrapper.childNodes.forEach((c) => c.remove()) canvas_wrapper.childNodes.forEach((c) => c.remove())
canvas_wrapper.appendChild(waveform_canvas) canvas_wrapper.appendChild(waveform_canvas)
for (const song of result.db.songs) {
console.log("creating waveform for -> " + song.name)
const curr_waveform_canvas = waveform_canvas.cloneNode() as SVGSVGElement
waveform_canvas.parentElement?.append(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_time_smoothing(0.8)
.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 })
waveform_visual.draw_once()
// await new Promise<void>((done) => setTimeout(() => done(), 500))
// @ts-ignore
song.metadata[0] = curr_waveform_canvas.children[0].getAttribute("d")
song.fft_data = []
}
waveform_canvas.remove()
console.dir(result.db, { depth: null })
download(JSON.stringify(result.db), "db.json", "text/plain")
for (const song of result.db.songs) {
console.log("creating waveform for -> " + song.name)
const curr_waveform_canvas =
waveform_canvas.cloneNode() as SVGSVGElement
waveform_canvas.parentElement?.append(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: 0.9,
fft_offset_i: -65
})
.set_fft_time_smoothing(0.8)
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
const waveform_visual = waveform_visual_builder.build(
ShapeType.Waveform,
true,
{
fft_data: new Float32Array(new Float64Array(song.fft_data!)),
orientation: WaveformOrientation.Horizontal,
shape_type: WaveformShape.LineLike
}
)
waveform_visual.draw_once()
// await new Promise<void>((done) => setTimeout(() => done(), 500))
// @ts-ignore
song.metadata[0] = curr_waveform_canvas.children[0].getAttribute("d")
song.fft_data = []
}
waveform_canvas.remove()
console.dir(result.db, { depth: null })
download(JSON.stringify(result.db), "db.json", "text/plain")
} }
async function analyze(): Promise<AnalyzeReturn> { async function analyze(): Promise<AnalyzeReturn> {
console.clear() console.clear()
const audioEl = document.querySelector("#audio") as HTMLAudioElement const audioEl = document.querySelector("#audio") as HTMLAudioElement
console.log("analysing...") console.log("analysing...")
const samplingRate = 100 const samplingRate = 100
// db.songs.splice(0, 10) // db.songs.splice(0, 10)
// db.songs.splice(2) // db.songs.splice(2)
console.log(db) console.log(db)
for (const song of db.songs) { for (const song of db.songs) {
// const song = db.songs[db.songs.length - 1] // const song = db.songs[db.songs.length - 1]
console.log(`Analyzing ${song.name}, ${db.songs.indexOf(song) + 1}/${db.songs.length}`) console.log(
//if not loaded yet keep trying `Analyzing ${song.name}, ${db.songs.indexOf(song) + 1}/${
audioEl.src = song.url.href db.songs.length
await awaitLoad(audioEl) }`
song.duration = audioEl.duration )
let currentFFTData = [] //if not loaded yet keep trying
for (let curSecond = 0; curSecond < song.duration; curSecond += song.duration / samplingRate) { audioEl.src = song.url.href
console.log("working...") await awaitLoad(audioEl)
audioEl.currentTime = curSecond song.duration = audioEl.duration
await audioEl.play() let currentFFTData = []
await new Promise<void>((done) => setTimeout(() => done(), 100)) for (
audioContextAnalyser.getFloatFrequencyData(FFTDataArray) let curSecond = 0;
let volume = 0 curSecond < song.duration;
FFTDataArray.forEach((element) => { curSecond += song.duration / samplingRate
volume += element ) {
}) console.log("working...")
currentFFTData.push(Math.round((volume / FFTDataArray.length) * 100) / 100) audioEl.currentTime = curSecond
} await audioEl.play()
song.fft_data = currentFFTData await new Promise<void>((done) => setTimeout(() => done(), 100))
console.log(song.fft_data) audioContextAnalyser.getFloatFrequencyData(FFTDataArray)
} let volume = 0
console.log("Analyzation finished!") FFTDataArray.forEach((element) => {
const result: AnalyzeReturn = { analyzer_node: audioContextAnalyser, db: db } volume += element
return result })
currentFFTData.push(
Math.round((volume / FFTDataArray.length) * 100) / 100
)
}
song.fft_data = currentFFTData
console.log(song.fft_data)
}
console.log("Analyzation finished!")
const result: AnalyzeReturn = {
analyzer_node: audioContextAnalyser,
db: db
}
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) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
audioEl.addEventListener("loadeddata", function () { audioEl.addEventListener("loadeddata", function () {
if (audioEl.readyState >= 4) { if (audioEl.readyState >= 4) {
resolve() resolve()
} }
}) })
}) })
} }

View file

@ -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("\\"))
@ -35,7 +39,8 @@ function generate_new_sounds_ogg(file, currentExtention) {
//Adds 25ms of delay to all samples //Adds 25ms of delay to all samples
command += `-af 'adelay=25:all=true' ` command += `-af 'adelay=25:all=true' `
//So the demo is HQ //So the demo is HQ
if (file.includes("demo")) command += `-c:a libopus -b:a 256k '${file}.ogg'"` if (file.includes("demo"))
command += `-c:a libopus -b:a 256k '${file}.ogg'"`
else command += `-c:a libopus -b:a 96k '${file}.ogg'"` else command += `-c:a libopus -b:a 96k '${file}.ogg'"`
exec(command) exec(command)
console.log(command) console.log(command)
@ -54,7 +59,11 @@ function generate_new_sounds_mp3(file, currentExtention) {
exec(command) exec(command)
// console.log(command) // console.log(command)
} }
function generate_new_video_sizes_mp4(file, currentExtention, width_resolutions) { function generate_new_video_sizes_mp4(
file,
currentExtention,
width_resolutions
) {
const path = file.substring(0, file.lastIndexOf("\\")) const path = file.substring(0, file.lastIndexOf("\\"))
file = file.substring(file.lastIndexOf("\\") + 1) file = file.substring(file.lastIndexOf("\\") + 1)
@ -72,12 +81,16 @@ 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)
} }
}) })
} }
function generate_new_video_sizes_webm(file, currentExtention, width_resolutions) { function generate_new_video_sizes_webm(
file,
currentExtention,
width_resolutions
) {
const path = file.substring(0, file.lastIndexOf("\\")) const path = file.substring(0, file.lastIndexOf("\\"))
file = file.substring(file.lastIndexOf("\\") + 1) file = file.substring(file.lastIndexOf("\\") + 1)
@ -95,36 +108,42 @@ 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) {
current_media = [current_media.substring(0, current_media.lastIndexOf(".")), current_media.substring(current_media.lastIndexOf(".") + 1)] current_media = [
current_media.substring(0, current_media.lastIndexOf(".")),
current_media.substring(current_media.lastIndexOf(".") + 1)
]
if (current_media[1] == "wav") { if (current_media[1] == "wav") {
console.log(`${current_media[0]}.${current_media[1]}\n`) console.log(`${current_media[0]}.${current_media[1]}\n`)
generate_new_sounds_ogg(`${current_media[0]}`, `${current_media[1]}`) generate_new_sounds_ogg(
generate_new_sounds_mp3(`${current_media[0]}`, `${current_media[1]}`) `${current_media[0]}`,
`${current_media[1]}`
)
generate_new_sounds_mp3(
`${current_media[0]}`,
`${current_media[1]}`
)
} }
/* /*
@ -147,6 +166,3 @@ for (let i = 0; i < dirs.length; i++) {
} }
} }
} }

View file

@ -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"
]

View file

@ -1,43 +1,82 @@
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
*/ */
const trapnation_visual_builder = new AudioVisualBuilder(trapnation_analyser_node, document.querySelector("#trapnation-canvas") as SVGSVGElement) const trapnation_visual_builder = new AudioVisualBuilder(
//Because the to_fft_range is so low, it needs more FFT data. trapnation_analyser_node,
.set_fft_size(8192) document.querySelector("#trapnation-canvas") as SVGSVGElement
//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 }) //Because the to_fft_range is so low, it needs more FFT data.
.set_fft_time_smoothing(0.6) .set_fft_size(8192)
//If not using typescript enums, CatmullRom = number 2 //Tells the Visualiser how to parse data which mutates our initial shape
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom) .set_fft_data_tresholds({
const trapnation_visual = trapnation_visual_builder.build(ShapeType.Circle, false) to_fft_range_i: 3,
point_count_i: 40,
fft_multiplier_i: 1.5,
fft_offset_i: 150
})
.set_fft_time_smoothing(0.6)
//If not using typescript enums, CatmullRom = number 2
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
const 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(
.set_fft_data_tresholds({ point_count_i: 50, fft_multiplier_i: 2, fft_offset_i: -100 }) bar_analyser_node,
.set_fft_time_smoothing(0.8) document.querySelector("#bar-canvas") as SVGSVGElement
.set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular) )
.set_fft_data_tresholds({
point_count_i: 50,
fft_multiplier_i: 2,
fft_offset_i: -100
})
.set_fft_time_smoothing(0.8)
.set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular)
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(
.set_fft_data_tresholds({ point_count_i: 100, fft_multiplier_i: 1, fft_offset_i: -80 }) bar_analyser_node,
.set_fft_time_smoothing(0.8) waveform_canvas
.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 }) .set_fft_data_tresholds({
point_count_i: 100,
fft_multiplier_i: 1,
fft_offset_i: -80
})
.set_fft_time_smoothing(0.8)
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
const waveform_visual = waveform_visual_builder.build(
ShapeType.Waveform,
true,
{
fft_data: new Float32Array(fft_data.fft_data),
orientation: WaveformOrientation.Horizontal,
shape_type: WaveformShape.LineLike
}
)
trapnation_visual.draw() trapnation_visual.draw()
bar_visual.draw() bar_visual.draw()
@ -84,70 +123,106 @@ 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
const resX = convert_range(x, [0, rect.width], [0, waveform_canvas.viewBox.baseVal.width + 40]) const resX = convert_range(
const polygon = `polygon(0 0, ${resX}px 0, ${resX}px 100%, 0 100%)` x,
document.documentElement.style.setProperty("--clip-seek-path", polygon) [0, rect.width],
[0, waveform_canvas.viewBox.baseVal.width + 40]
)
const polygon = `polygon(0 0, ${resX}px 0, ${resX}px 100%, 0 100%)`
document.documentElement.style.setProperty("--clip-seek-path", polygon)
}) })
waveform_canvas.addEventListener("mouseleave", (e) => { 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")) music_player
.then(() => { .try_new_song_async(
let is_seeking = false encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg")
document.querySelector("#play")?.addEventListener("click", () => { )
music_player.play_async() .then(
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e)) () => {
}) let is_seeking = false
document.querySelector("#pause")?.addEventListener("click", () => { document.querySelector("#play")?.addEventListener("click", () => {
music_player.pause() music_player.play_async().then(
}) () => {
document.querySelector("#mute")?.addEventListener("click", () => { console.log("Playing!")
music_player.mute() },
}) (e) => alert("Failed to play, " + e)
document.querySelector("#unmute")?.addEventListener("click", () => { )
music_player.unmute() })
}) document.querySelector("#pause")?.addEventListener("click", () => {
document.querySelector("#toggle-mute")?.addEventListener("click", () => { music_player.pause()
music_player.mute_toggle() })
}) document.querySelector("#mute")?.addEventListener("click", () => {
document.querySelector("#toggle-play")?.addEventListener("click", () => { music_player.mute()
music_player.play_toggle_async().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e)) })
}) document.querySelector("#unmute")?.addEventListener("click", () => {
document.querySelector("#volume")?.addEventListener("input", (e) => { music_player.unmute()
music_player.change_volume(e.target?.valueAsNumber) })
}) document
document.querySelector("#seek")?.addEventListener("mousedown", (e) => { .querySelector("#toggle-mute")
is_seeking = true; ?.addEventListener("click", () => {
}) music_player.mute_toggle()
document.querySelector("#seek")?.addEventListener("mouseup", (e) => { })
music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => { document
alert("Failed seeking! " + e) .querySelector("#toggle-play")
}) ?.addEventListener("click", () => {
is_seeking = false music_player.play_toggle_async().then(
}) (s) => console.log("toggled play/pause"),
// Subscriptions to AudioContext changes, eg. time.. (e) => alert("failed to toggle pause/play!" + e)
music_player.on_duration_formatted((time) => { )
duration_element.innerHTML = time })
seek_element.max = "" + music_player.current_song_duration document
}) .querySelector("#volume")
music_player.on_time_tick_formatted((time) => { ?.addEventListener("input", (e) => {
current_time_element.innerHTML = time music_player.change_volume(e.target?.valueAsNumber)
}) })
music_player.on_time_tick((time) => { document
if (is_seeking) return .querySelector("#seek")
seek_element.value = "" + time ?.addEventListener("mousedown", (e) => {
const x = `${time / music_player.current_song_duration * 100}%` is_seeking = true
const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)` })
document.documentElement.style.setProperty("--clip-time-path", polygon) document
}) .querySelector("#seek")
?.addEventListener("mouseup", (e) => {
}, (e) => console.log(e)) music_player.try_seek_async(e.target?.valueAsNumber).then(
() => {
console.log("seeked to " + e.target?.valueAsNumber)
},
() => {
alert("Failed seeking! " + e)
}
)
is_seeking = false
})
// Subscriptions to AudioContext changes, eg. time..
music_player.on_duration_formatted((time) => {
duration_element.innerHTML = time
seek_element.max = "" + music_player.current_song_duration
})
music_player.on_time_tick_formatted((time) => {
current_time_element.innerHTML = time
})
music_player.on_time_tick((time) => {
if (is_seeking) return
seek_element.value = "" + time
const x = `${
(time / music_player.current_song_duration) * 100
}%`
const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)`
document.documentElement.style.setProperty(
"--clip-time-path",
polygon
)
})
},
(e) => console.log(e)
)

View file

@ -1,14 +1,17 @@
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,
-96.32, -99.23, -103.13, -85.57, -98.17, -103.27, -107.5, -83.62, -95.23, -97.12, -94.78, -93.68, -95.02, -97.16, -96.32, -99.23, -103.13, -85.57, -98.17,
-95.93, -101.42, -97.83, -102.42, -111.74, -101.38, -106.8, -111.05, -88.04, -90.88, -97.67, -103.27, -107.5, -83.62, -95.23, -97.12, -94.78, -95.93, -101.42,
-96.31, -96.69, -102.15, -102.03, -100.51, -107.14, -101.48, -101.6, -106.62, -73.94, -97.83, -102.42, -111.74, -101.38, -106.8, -111.05, -88.04, -90.88,
-79.53, -92.74, -96.08, -96.26, -100.35, -99.13, -102.03, -107.4, -93.57, -102.31, -102.3, -97.67, -96.31, -96.69, -102.15, -102.03, -100.51, -107.14, -101.48,
-109.04, -81.85, -92.79, -100.06, -95.79, -96.49, -99.89, -100.27, -102.69, -107.35, -101.6, -106.62, -73.94, -79.53, -92.74, -96.08, -96.26, -100.35,
-103.94, -104.64, -104.3, -78.82, -84.2, -95.29, -92.57, -93.47, -98.08, -98.9, -101.56, -99.13, -102.03, -107.4, -93.57, -102.31, -102.3, -109.04, -81.85,
-109.38, -102.01, -102.51, -104.83, -72.18, -76.52, -91.69, -99.97, -96.63, -98.61, -76.97, -92.79, -100.06, -95.79, -96.49, -99.89, -100.27, -102.69, -107.35,
-90.41, -100.38, -106.77, -102.83, -104.46, -108.59, -80.97, -88.05, -100.77, -79.64, -72.3, -103.94, -104.64, -104.3, -78.82, -84.2, -95.29, -92.57, -93.47, -98.08,
-87.96, -92.89, -93.03 -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
]
}

File diff suppressed because it is too large Load diff

81
test.js
View file

@ -1,47 +1,50 @@
class AudioContexthehe { class AudioContexthehe {
state = "suspended" state = "suspended"
constructor() { } constructor() {}
resume() { resume() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.state = "running" this.state = "running"
resolve() resolve()
}) })
} }
} }
class AudioElementHehe { class AudioElementHehe {
constructor() { } constructor() {}
play() { play() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log("playing!") console.log("playing!")
resolve() resolve()
}) })
} }
pause() { pause() {
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()
function try_play_toggle_async() { function try_play_toggle_async() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (audio_context.state !== "running") { if (audio_context.state !== "running") {
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(
is_playing = true (s) => {
resolve(s) is_playing = true
}, (r) => { resolve(s)
is_playing = false },
reject(r) (r) => {
}) is_playing = false
} else { reject(r)
audio_element.pause() }
is_playing = false )
resolve(null) } else {
} audio_element.pause()
}) is_playing = false
} resolve(null)
}
})
}

View file

@ -1,42 +1,42 @@
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,
"moduleResolution": "node", "moduleResolution": "node",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"importHelpers": true, "importHelpers": true,
"target": "es2015", "target": "es2015",
"module": "esnext", "module": "esnext",
"lib": [ "lib": [
"es2020", "es2020",
"dom" "dom"
], ],
"skipLibCheck": true, "skipLibCheck": true,
"skipDefaultLibCheck": true, "skipDefaultLibCheck": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@euterpe.js/dj": [ "@euterpe.js/dj": [
"packages/dj/src/index.ts" "packages/dj/src/index.ts"
], ],
"@euterpe.js/euterpe": [ "@euterpe.js/euterpe": [
"packages/euterpe/src/index.ts" "packages/euterpe/src/index.ts"
], ],
"@euterpe.js/music-library": [ "@euterpe.js/music-library": [
"packages/music-library/src/index.ts" "packages/music-library/src/index.ts"
], ],
"@euterpe.js/player": [ "@euterpe.js/player": [
"packages/player/src/index.ts" "packages/player/src/index.ts"
], ],
"@euterpe.js/visualizer": [ "@euterpe.js/visualizer": [
"packages/visualizer/src/index.ts" "packages/visualizer/src/index.ts"
] ]
} }
}, },
"exclude": [ "exclude": [
"node_modules", "node_modules",
"tmp" "tmp"
] ]
} }