fix formatting
This commit is contained in:
parent
b57321350d
commit
ed2d865f2d
24 changed files with 3217 additions and 2495 deletions
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"babelrcRoots": [
|
"babelrcRoots": [
|
||||||
"*"
|
"*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
148
nx.json
148
nx.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
85
package.json
85
package.json
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
import { MusicPlayerBuilder } from "@euterpe.js/player";
|
import { MusicPlayerBuilder } from "@euterpe.js/player"
|
||||||
|
|
|
@ -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"
|
||||||
|
)
|
||||||
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!`)
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1 +1,33 @@
|
||||||
export const songs = ["../public/samples/FX/01 HTS Boom Kicks.ogg","../public/samples/FX/02 HTS Verbclaps.ogg","../public/samples/FX/03 HTS Noisesweep.ogg","../public/samples/FX/04 HTS Combined FX.ogg","../public/samples/bass/01 HTS Arp Bass.ogg","../public/samples/bass/02 HTS Hard Bass.ogg","../public/samples/bass/03 HTS Break Bass.ogg","../public/samples/bass/04 HTS Sub Bass.ogg","../public/samples/demos/01 demo 1 - Violet Delta & Eyhz.ogg","../public/samples/demos/02 demo 2 - Walras.ogg","../public/samples/demos/03 demo 3 - G4TE-16.ogg","../public/samples/demos/04 demo 4 - shadeux.ogg","../public/samples/demos/05 demo 5 - crowit.ogg","../public/samples/demos/06 demo 6 - sh0wtime.ogg","../public/samples/demos/07 demo 7 - nuphory & Luna Lenta.ogg","../public/samples/drums/01 HTS Rides and Hats.ogg","../public/samples/drums/02 HTS Claps and Hats.ogg","../public/samples/drums/03 HTS Club Snares.ogg","../public/samples/drums/04 HTS Buildup Snares.ogg","../public/samples/kicks/01 HTS Trancekick.ogg","../public/samples/kicks/02 HTS Sizzle Layer.ogg","../public/samples/kicks/03 HTS Transients.ogg","../public/samples/kicks/04 HTS Kick Combined.ogg","../public/samples/loops/01 HTS Loop CH.ogg","../public/samples/loops/02 HTS Loop OH.ogg","../public/samples/loops/03 HTS Perc Loop.ogg","../public/samples/loops/04 HTS Full Loop.ogg","../public/samples/synths/01 HTS Leads and Bass 01.ogg","../public/samples/synths/02 HTS Leads and Pads 01.ogg","../public/samples/synths/03 HTS Leads and Bass 02.ogg","../public/samples/synths/04 HTS Leads and Pads 02.ogg"]
|
export const songs = [
|
||||||
|
"../public/samples/FX/01 HTS Boom Kicks.ogg",
|
||||||
|
"../public/samples/FX/02 HTS Verbclaps.ogg",
|
||||||
|
"../public/samples/FX/03 HTS Noisesweep.ogg",
|
||||||
|
"../public/samples/FX/04 HTS Combined FX.ogg",
|
||||||
|
"../public/samples/bass/01 HTS Arp Bass.ogg",
|
||||||
|
"../public/samples/bass/02 HTS Hard Bass.ogg",
|
||||||
|
"../public/samples/bass/03 HTS Break Bass.ogg",
|
||||||
|
"../public/samples/bass/04 HTS Sub Bass.ogg",
|
||||||
|
"../public/samples/demos/01 demo 1 - Violet Delta & Eyhz.ogg",
|
||||||
|
"../public/samples/demos/02 demo 2 - Walras.ogg",
|
||||||
|
"../public/samples/demos/03 demo 3 - G4TE-16.ogg",
|
||||||
|
"../public/samples/demos/04 demo 4 - shadeux.ogg",
|
||||||
|
"../public/samples/demos/05 demo 5 - crowit.ogg",
|
||||||
|
"../public/samples/demos/06 demo 6 - sh0wtime.ogg",
|
||||||
|
"../public/samples/demos/07 demo 7 - nuphory & Luna Lenta.ogg",
|
||||||
|
"../public/samples/drums/01 HTS Rides and Hats.ogg",
|
||||||
|
"../public/samples/drums/02 HTS Claps and Hats.ogg",
|
||||||
|
"../public/samples/drums/03 HTS Club Snares.ogg",
|
||||||
|
"../public/samples/drums/04 HTS Buildup Snares.ogg",
|
||||||
|
"../public/samples/kicks/01 HTS Trancekick.ogg",
|
||||||
|
"../public/samples/kicks/02 HTS Sizzle Layer.ogg",
|
||||||
|
"../public/samples/kicks/03 HTS Transients.ogg",
|
||||||
|
"../public/samples/kicks/04 HTS Kick Combined.ogg",
|
||||||
|
"../public/samples/loops/01 HTS Loop CH.ogg",
|
||||||
|
"../public/samples/loops/02 HTS Loop OH.ogg",
|
||||||
|
"../public/samples/loops/03 HTS Perc Loop.ogg",
|
||||||
|
"../public/samples/loops/04 HTS Full Loop.ogg",
|
||||||
|
"../public/samples/synths/01 HTS Leads and Bass 01.ogg",
|
||||||
|
"../public/samples/synths/02 HTS Leads and Pads 01.ogg",
|
||||||
|
"../public/samples/synths/03 HTS Leads and Bass 02.ogg",
|
||||||
|
"../public/samples/synths/04 HTS Leads and Pads 02.ogg"
|
||||||
|
]
|
||||||
|
|
|
@ -1,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)
|
||||||
|
)
|
||||||
|
|
|
@ -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
81
test.js
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue