Compare commits

..

1 commit

Author SHA1 Message Date
51f135b6fa init rust lib 2023-12-22 14:05:46 +01:00
57 changed files with 3068 additions and 3496 deletions

View file

@ -1,13 +1,12 @@
{ {
"parser": "typescript", "parser": "typescript",
"trailingComma": "none", "trailingComma": "none",
"useTabs": true, "useTabs": true,
"tabWidth": 4, "tabWidth": 4,
"semi": false, "semi": false,
"singleQuote": false, "singleQuote": false,
"endOfLine": "lf", "endOfLine": "lf",
"bracketSpacing": true, "bracketSpacing": true,
"bracketSameLine": false, "bracketSameLine": false,
"arrowParens": "always", "arrowParens": "always"
"printWidth": 150
} }

View file

@ -1 +0,0 @@
./packages/euterpe/README.md

22
README.md Normal file
View file

@ -0,0 +1,22 @@
# Euterpe
The sole mono-repo for all things Euterpe.
## Understand this workspace
Run `npx nx graph` to see a diagram of the dependencies of the projects.
All programs go like this: `npx nx {package script} {package}`
## Test this workspace
Run `npx nx server player-web-test` to see the music player in a minimal demo.
Run `npx nx server visualizer-web-test` to see the music player in a minimal demo.
## Build
Run `npx nx build player` to build the player.
Run `npx nx build visualizer` to build the player.
## Publish
First build, then run `npm run publish-[package]`(scripts)

View file

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

148
nx.json
View file

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

View file

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

View file

@ -1,73 +1,74 @@
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.decodeAudioData(buffer).then((audio_buffer) => {
this.audio_buffer = audio_buffer this.audio_buffer = audio_buffer
}) })
}) })
}) });
} catch (e) { } catch (e) {
console.error(new Error("Failed to preprocess DJSong. " + e)) console.error(new Error("Failed to preprocess DJSong. " + e))
} }
} }
public async analyze(url: URL, audio_context: AudioContext) { public async analyze(url: URL, audio_context: AudioContext) {
this.audio_buffer = await audio_context.decodeAudioData(await (await fetch(url)).arrayBuffer()) 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" ? inputs = dj_songs : inputs = [dj_songs]
for (const input of inputs) { for (const input of inputs) {
if (input instanceof DJSong) { if (input instanceof DJSong) {
const song = input as DJSong const song = input as DJSong
if (!song.id) song.id = this.songs.length 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) => 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!))) song.remix_artists.forEach((artist) => curr_col.artists.push(new Ref(RefTo.Artists, artist.get(this)!.id!)))
} }
for (const artist_ref of song.artists) { for (const artist_ref of song.artists) {
const curr_artist = artist_ref.get(this) as Artist const curr_artist = artist_ref.get(this) as Artist
curr_artist.songs.push(new Ref(RefTo.Songs, song.id)) curr_artist.songs.push(new Ref(RefTo.Songs, song.id))
} }
for (const artist_ref of song.remix_artists) { for (const artist_ref of song.remix_artists) {
const curr_artist = artist_ref.get(this) as Artist const curr_artist = artist_ref.get(this) as Artist
curr_artist.songs.push(new Ref(RefTo.Songs, song.id)) curr_artist.songs.push(new Ref(RefTo.Songs, song.id))
} }
this.songs.push(song)
}
}
}
this.songs.push(song)
}
}
}
} }

View file

@ -1,134 +1,125 @@
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: { current: number, max: number, next_bar_in: number }) => void
constructor(public player: Euterpe | MusicPlayer, public master_bpm: number | 120) { constructor(public player: Euterpe | MusicPlayer, public master_bpm: number | 120) {
this.beat_duration = 60 / master_bpm this.beat_duration = 60 / master_bpm
this.#emit_beats() this.#emit_beats()
} }
#emit_beats() { #emit_beats() {
this.beat.current >= 4 ? (this.beat.current++, this.beat.next_bar_in--) : ((this.beat.current = 0), (this.beat.next_bar_in = this.beat.max)) this.beat.current >= 4 ?
(this.beat.current++, this.beat.next_bar_in--) :
(this.beat.current = 0, this.beat.next_bar_in = this.beat.max)
if (this.on_beat) this.on_beat(this.beat)
//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
}
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(public player: MusicPlayer | Euterpe, public current_song?: Song, public should_loop?: boolean) {
this.audio_context = player.audio_context this.audio_context = player.audio_context
this.gain = this.audio_context.createGain() this.gain = this.audio_context.createGain()
if (current_song) this.change_song(current_song).catch((e) => console.error("error during track construction - " + e)) 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).then( fetch(this.current_song!.url)
async (file) => { .then(
this.audio_buffer = await this.audio_context.decodeAudioData(await file.arrayBuffer()) async (file) => {
resolve(this) this.audio_buffer = await this.audio_context.decodeAudioData(await file.arrayBuffer())
}, resolve(this)
(reason) => reject(reason) },
) (reason) => reject(reason))
}) })
} }
#connect() { #connect() {
if (!this.audio_buffer) throw new Error("Somehow buffer not in track even though it analyzed properly. Report this as a bug") 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 = this.audio_context.createBufferSource()
this.buffer_source.buffer = this.audio_buffer! this.buffer_source.buffer = this.audio_buffer!
this.buffer_source.connect(this.gain) this.buffer_source.connect(this.gain)
this.buffer_source.loop = this.should_loop || false this.buffer_source.loop = this.should_loop || false
this.gain.connect(this.player.gain) this.gain.connect(this.player.gain)
} }
async change_song(new_song: Song) { async change_song(new_song: Song) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.current_song = new_song this.current_song = new_song
this.#prepare().then( this.#prepare().then(() => {
() => { this.#connect()
this.#connect() resolve(this)
resolve(this) }, (reason) => reject(reason))
}, })
(reason) => reject(reason) }
) /**
}) *
} * @param delay in seconds
/** */
* async try_start(delay?: number) {
* @param delay in seconds return new Promise((resolve, reject) => {
*/ if (!this.buffer_source) reject(new Error("No buffer source yet, set a song first"))
async try_start(delay?: number) { this.buffer_source!.start(this.audio_context.currentTime + (delay || 0))
return new Promise((resolve, reject) => { })
if (!this.buffer_source) reject(new Error("No buffer source yet, set a song first")) }
this.buffer_source!.start(this.audio_context.currentTime + (delay || 0))
})
}
} }

View file

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

View file

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

View file

@ -1,140 +1,129 @@
import { db } from "./db" import { db } from "./db";
import { EuterpeBuilder } from "@euterpe.js/euterpe" import { EuterpeBuilder } from "@euterpe.js/euterpe";
let is_seeking = false let is_seeking = false
// document.addEventListener("click", start, { once: true }) // document.addEventListener("click", start, { once: true })
const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db, { const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db)
auto_play_after_seek: true, .build()
auto_play_after_changing_song: true,
use_only_pathname_url: true
}).build()
add_library_to_dom() add_library_to_dom()
euterpe.try_preload_song(0).then(() => {
document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()
}, (e) => console.log(e + " Failed to preload"))
document.querySelector("#seek")?.addEventListener("mouseup", (e) => { document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
try { try {
euterpe.try_seek(e.target?.valueAsNumber) euterpe.try_seek(e.target?.valueAsNumber)
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 song and AudioContext changes, eg. time, name.. // 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()
}) })
euterpe.on_song_change((_, song_name) => {
document.querySelector("#text-playing")!.innerHTML = song_name
})
//preload after setting all listeners to make sure you capture the song update!
euterpe.try_preload_song(0).catch((e) => console.log(e + " Failed to preload"))
document.querySelector("#previous")?.addEventListener("click", () => { document.querySelector("#previous")?.addEventListener("click", () => {
euterpe.try_previous_song_looping().catch((e) => alert(e + "Failed to change song")) euterpe.try_previous_song_looping().then(() => {
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().catch((e) => alert(e + "Failed to change song")) euterpe.try_next_song_looping().then(() => {
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()), () => document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song(),
(e) => alert(e) (e) => alert(e)
) )
} }
function library_queue(e: MouseEvent) { function library_queue(e: MouseEvent) {
const b = e.currentTarget as HTMLButtonElement const b = e.currentTarget as HTMLButtonElement
euterpe.queue_append(parseInt(b.dataset["id"]!)) euterpe.queue_append(parseInt(b.dataset["id"]!))
} }
function dev_queue_update() { function dev_queue_update() {
const p = document.querySelector("#queue-info") as HTMLParagraphElement const p = document.querySelector("#queue-info") as HTMLParagraphElement
const dev_arr = [] const dev_arr = []
for (const song of euterpe.queue) { for (const song of euterpe.queue) {
dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`) dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`)
} }
p.innerHTML = dev_arr.toString() p.innerHTML = dev_arr.toString()
} }
function dev_history_update() { function dev_history_update() {
const p = document.querySelector("#history-info") as HTMLParagraphElement const p = document.querySelector("#history-info") as HTMLParagraphElement
const dev_arr = [] const dev_arr = []
for (const song of euterpe.played_history) { for (const song of euterpe.played_history) {
dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`) dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`)
} }
p.innerHTML = dev_arr.toString() p.innerHTML = dev_arr.toString()
} }

View file

@ -2,16 +2,13 @@
Fully featured AudioContext music player for the web. Fully featured AudioContext music player for the web.
## Euterpe in production:
- Hypertrance ( [site](https://hypertrance.eu/), [repository](https://github.com/nuphory/hypertrance.eu) )
Features: Features:
- "Local" library/database for songs, collections, artists, waveforms, artist links and much more! - "Local" library/Database for songs, collections etc.
- Queue and history - Queue
- History
- Easy way to create Vector based audio visuals - Easy way to create Vector based audio visuals
- Library automatization based on folder/file structure, preprocessing and encoding media files for all platforms
- Safe. Provides wrappers for all functions that are either unsafe or don't give a success return. (very Rust inspired, yes.) - Safe. Provides wrappers for all functions that are either unsafe or don't give a success return. (very Rust inspired, yes.)
- Frontend library agnostic - Async / Await or simple funcions.
## How to use: ## How to use:
@ -22,155 +19,4 @@ Since this package is just a compilation of our smaller modules, you can read in
- [Euterpe Visualizer](https://www.npmjs.com/package/@euterpe.js/visualizer) - [Euterpe Visualizer](https://www.npmjs.com/package/@euterpe.js/visualizer)
- [Euterpe Music Library](https://www.npmjs.com/package/@euterpe.js/music-library) - [Euterpe Music Library](https://www.npmjs.com/package/@euterpe.js/music-library)
You can further check out how to automate database creation from folder structure, auto encode media for all platforms and create waveform svgs for songs here:
- [Euterpe Preprocessor](https://www.npmjs.com/package/@euterpe.js/preprocessor)
This module builds on those, and further adds functions for playing backwards, forwards and managing the queue. This module builds on those, and further adds functions for playing backwards, forwards and managing the queue.
First we create a database with our songs
`db.ts`
```ts
import { DB, Song, Artist, Ref, RefTo, Platforms } from "@euterpe.js/music-library"
export const db = new DB
db.add([
//The IDs are added incrementally & are 0 based., so first artists ID added is 0, next 1 etc...
//You can specify the ID manually if you want
new Artist({
name: "Machinedrum",
}),
new Artist({
name: "Tanerélle",
}),
new Artist({
name: "Mono/Poly",
}),
new Artist({
name: "IMANU",
links: [
[Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]
]
}),
])
db.add([
new Song({
//Refrences are constructed as such. This allows to get to the artist from either collection or song
artists: [new Ref(RefTo.Artists, 2), new Ref(RefTo.Artists, 3), new Ref(RefTo.Artists, 4)],
duration: 252,
name: "Star",
remix_artists: [new Ref(RefTo.Artists, 5)],
url: new URL("http://" + window.location.host + "/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3")
}),
])
```
Then we build our Euterpe player and assign the db to it. Then it's just a matter of creating event listeners to the dom and binding them to Euterpes functions.
`main.ts`
```ts
import { db } from "./db";
import { EuterpeBuilder } from "@euterpe.js/euterpe"
let is_seeking = false
const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db)
.build()
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
try {
euterpe.try_seek(e.target?.valueAsNumber)
} catch {
alert("Failed seeking! " + e)
}
is_seeking = false
})
euterpe.on_song_change((_, song_name) => {
document.querySelector("#text-playing")!.innerHTML = song_name
})
document.querySelector("#previous")?.addEventListener("click", () => {
euterpe.try_previous_song_looping().catch((e) => alert(e + "Failed to change song"))
})
document.querySelector("#next")?.addEventListener("click", () => {
euterpe.try_next_song_looping().catch((e) => alert(e + "Failed to change song"))
})
document.querySelector("#mute")?.addEventListener("click", () => {
euterpe.mute()
})
document.querySelector("#unmute")?.addEventListener("click", () => {
euterpe.unmute()
})
document.querySelector("#toggle-play")?.addEventListener("click", () => {
euterpe.try_play_toggle().catch((e) => alert("failed to toggle pause/play!" + e))
})
document.querySelector("#volume")?.addEventListener("input", (e) => {
euterpe.change_volume(e.target?.valueAsNumber)
})
//disables time updates so the time slider doesn't slip away from user
document.querySelector("#seek")?.addEventListener("mousedown", () => {
is_seeking = true
})
```
Then we can set up listeners to Euterpes events to keep the UI up todate as well
`main.ts`
```ts
//...
// Subscriptions to song and AudioContext changes, eg. time, name..
euterpe.on_duration_formatted((time) => {
document.querySelector("#duration")!.innerHTML = time
document.querySelector("#seek")!.max = "" + euterpe.current_song_duration
})
euterpe.on_time_tick_formatted((time) => {
document.querySelector("#current")!.innerHTML = time
})
euterpe.on_time_tick((time) => {
if (is_seeking) return
document.querySelector("#seek")!.value = "" + time
dev_queue_update()
dev_history_update()
})
euterpe.on_song_change((_, song_name) => {
document.querySelector("#text-playing")!.innerHTML = song_name
})
//preload after setting all listeners to make sure you capture the song update!
euterpe.try_preload_song(0).catch((e) => console.log(e + " Failed to preload"))
//..
function dev_queue_update() {
const p = document.querySelector("#queue-info") as HTMLParagraphElement
const dev_arr = []
for (const song of euterpe.queue) {
dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`)
}
p.innerHTML = dev_arr.toString()
}
function dev_history_update() {
const p = document.querySelector("#history-info") as HTMLParagraphElement
const dev_arr = []
for (const song of euterpe.played_history) {
dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`)
}
p.innerHTML = dev_arr.toString()
}
```
and it's done!
For vizualizer demo, or how to use the core parts of the Euterpe libraries separately, check out the individual repos readmes.

View file

@ -1,33 +1,33 @@
{ {
"name": "@euterpe.js/euterpe", "name": "@euterpe.js/euterpe",
"version": "2.1.0", "version": "2.0.1",
"type": "module", "type": "module",
"description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!", "description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!",
"main": "./src/index.js", "main": "./src/index.js",
"author": { "author": {
"name": "Djkáťo", "name": "Djkáťo",
"email": "djkatovfx@gmail.com" "email": "djkatovfx@gmail.com"
}, },
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/euterpe-js/euterpe-source.git" "url": "git+https://github.com/euterpe-js/euterpe-source.git"
}, },
"homepage": "https://github.com/euterpe-js/euterpe-source/tree/master/packages/euterpe#readme", "homepage": "https://github.com/euterpe-js/euterpe-source/tree/master/packages/euterpe#readme",
"keywords": [ "keywords": [
"audio", "audio",
"library", "library",
"music-database", "music-database",
"audio-player", "audio-player",
"webaudio", "webaudio",
"database", "database",
"db" "db"
], ],
"exports": { "exports": {
".": { ".": {
"types": "./src/index.d.ts", "types": "./src/index.d.ts",
"import": "./src/index.js", "import": "./src/index.js",
"require": "./src/lib/euterpe.js" "require": "./src/lib/euterpe.js"
} }
} }
} }

View file

@ -6,471 +6,427 @@ 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[] = []
#sub_list: Array<(song_id: number, song_name: string) => void> = [] 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)
audio_element.addEventListener("ended", async () => { super(audio_context, audio_element, track, gain, volume, current_song_path)
try { }
await this.try_next_song()
} catch (e) {
audio_element.currentTime = 0
this.pause()
}
})
}
/** /**
* Unsafe, throws error if failed. Use try_seek or seek unless you don't care about the result. * Use to load song on page load.
*/ * @throws if song with ID doesn't exist
override seek(new_time: number) { */
super.seek(new_time) async try_preload_song(id: number) {
if (this.options?.auto_play_after_seek) this.play() 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
}
}
/** /**
* Safer seek. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed. * Won't loop back to first song if already on the last.
* won't resume playback by default unless `EuterpeBuilderOptions.auto_play_after_seek == true` * If queue present, uses that, if not, relies on Song ID directly from DB
* @throws if "Can't seek - Audiocontext is not running" * @throws if on last song or song fails to start
*/ */
override async try_seek(new_time: number) { async try_next_song() {
await super.try_seek(new_time) let new_song: Song
if (this.options?.auto_play_after_seek) this.play() if (this.queue.length > 0) {
} new_song = this.queue.shift()!
} 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) throw new Error("Won't go past the last song")
* Use to load song on page load. new_song = this.db.songs.find((song) => song.id == next_id)!
* @throws if song with ID doesn't exist }
*/ const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
async try_preload_song(id: number) { await this.try_new_song(url)
const next = this.db.songs.find((song) => song!.id == id) await this.try_play()
if (!next) throw new Error(`Song with id ${id} doesn't exist`) if (this.current_song) this.played_history.push(this.current_song)
else { this.current_song = new_song
this.current_song = next this.current_song_id = new_song.id!
this.#emit_on_song_change() }
await this.try_new_song(next.url.pathname)
}
}
/** /**
* Won't loop back to first song if already on the last. * 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 * 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_next_song_looping() {
async try_next_song() { let new_song: Song
let new_song: Song if (this.queue.length > 0) {
if (this.queue.length > 0) { new_song = this.queue.shift()!
new_song = this.queue.shift()! } else {
} else { let id_i = this.db.songs.length;
let id_i = this.db.songs.length while (this.db.songs[--id_i].id! > this.current_song_id);
while (this.db.songs[--id_i].id! > this.current_song_id); let next_id = ++id_i
const next_id = ++id_i
if (next_id == this.db.songs.length) throw new Error("Won't go past the last song") if (next_id == this.db.songs.length) next_id = this.db.songs[0].id!
new_song = this.db.songs.find((song) => song.id == next_id)! new_song = this.db.songs.find((song) => song.id == next_id)!
} }
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
await this.try_new_song(url) await this.try_new_song(url)
if (this.options?.auto_play_after_changing_song) await this.try_play() await this.try_play()
if (this.current_song) this.played_history.push(this.current_song) if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song this.current_song = new_song
this.current_song_id = new_song.id! this.current_song_id = new_song.id!
this.#emit_on_song_change() }
}
/** /**
* Will loop back to first song if already on last song, * 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 * If queue present, uses that, if not, relies on Song ID directly from DB
*/ */
async try_next_song_looping() { next_song_looping() {
let new_song: Song let new_song: Song
if (this.queue.length > 0) { if (this.queue.length > 0) {
new_song = this.queue.shift()! new_song = this.queue.shift()!
} else { } else {
let id_i = this.db.songs.length let id_i = this.db.songs.length;
while (this.db.songs[--id_i].id! > this.current_song_id); while (this.db.songs[--id_i].id! > this.current_song_id);
let next_id = ++id_i let next_id = ++id_i;
if (next_id == this.db.songs.length) next_id = this.db.songs[0].id! if (next_id == this.db.songs.length) next_id = this.db.songs[0].id!
new_song = this.db.songs.find((song) => song.id == next_id)! new_song = this.db.songs.find((song) => song.id == next_id)!
} }
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
await this.try_new_song(url) this.new_song(url)
if (this.options?.auto_play_after_changing_song) await this.try_play() this.play()
if (this.current_song) this.played_history.push(this.current_song) if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song this.current_song = new_song
this.current_song_id = new_song.id! this.current_song_id = new_song.id!
this.#emit_on_song_change() }
}
/** /**
* Won't tell you if the playback was successsful & will loop back if already on last song. Best use try_next_song() * 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 * If queue present, uses that, if not, relies on Song ID directly from DB
*/ */
next_song_looping() { next_song() {
let new_song: Song let new_song: Song
if (this.queue.length > 0) { if (this.queue.length > 0) {
new_song = this.queue.shift()! new_song = this.queue.shift()!
} else { } else {
let id_i = this.db.songs.length let id_i = this.db.songs.length;
while (this.db.songs[--id_i].id! > this.current_song_id); while (this.db.songs[--id_i].id! > this.current_song_id);
let next_id = ++id_i 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!
}
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)! * 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
} */
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() async try_specific_song(new_song_id: number) {
this.new_song(url) const new_song = this.db.songs.find((song) => song.id! == new_song_id)
if (this.options?.auto_play_after_changing_song) this.play() if (!new_song) throw new Error(`No song with id "${new_song_id}" found`)
if (this.current_song) this.played_history.push(this.current_song) else {
this.current_song = new_song this.try_new_song(new_song.url.pathname)
this.current_song_id = new_song.id! await this.try_play()
this.#emit_on_song_change() 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() * 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()
* If queue present, uses that, if not, relies on Song ID directly from DB */
*/ 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) return
if (this.queue.length > 0) { const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
new_song = this.queue.shift()! this.new_song(url)
} else { this.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)
if (this.options?.auto_play_after_changing_song) this.play()
if (this.current_song) this.played_history.push(this.current_song)
this.current_song = new_song
this.current_song_id = new_song.id!
this.#emit_on_song_change()
}
/** /**
* Uses safer try_play. Normal play / play will try to start the player even if the track hasn't started yet, or was previously suspended/closed * 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
async try_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) throw new Error(`No song with id "${new_song_id}" found`) async try_previous_song() {
else { let new_song: Song
this.try_new_song(new_song.url.pathname) if (this.played_history.length > 0) {
if (this.options?.auto_play_after_changing_song) await this.try_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);
this.#emit_on_song_change() const next_id = --id_i;
}
}
/** if (next_id == this.db.songs.length) throw new Error("Won't roll backwards to last song")
* Won't throw an error if new ID not found. Won't tell you if the play was successful, best use specific_song() or try_specific_song() new_song = this.db.songs.find((song) => song.id == next_id)!
*/ }
specific_song(new_song_id: number) { const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
const new_song = this.db.songs.find((song) => song.id! == new_song_id) await this.try_new_song(url)
if (!new_song) return await this.try_play()
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() //if (this.current_song) this.played_history.push(this.current_song)
this.new_song(url) this.current_song = new_song
if (this.options?.auto_play_after_changing_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!
this.#emit_on_song_change()
}
/** /**
* Won't loop back to first song if already on the last. * Will 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 * If history present, uses that, if not, relies on Song ID directly from DB
* @throws if playback was unsuccessful or at first song/ can't go more previous */
*/ async try_previous_song_looping() {
async try_previous_song() { let new_song: Song
let new_song: Song if (this.played_history.length > 0) {
if (this.played_history.length > 0) { new_song = this.played_history.pop()!
new_song = this.played_history.pop()! } else {
} else { let id_i = -1;
let id_i = 0 while (this.db.songs[++id_i].id! < this.current_song_id);
while (this.db.songs[++id_i].id! < this.current_song_id); let next_id = --id_i;
const next_id = --id_i
if (next_id == this.db.songs.length) throw new Error("Won't roll backwards to last song") if (next_id == -1) next_id = this.db.songs[this.db.songs.length - 1].id!
new_song = this.db.songs.find((song) => song.id == next_id)! new_song = this.db.songs.find((song) => song.id == next_id)!
} }
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
await this.try_new_song(url) await this.try_new_song(url)
if (this.options?.auto_play_after_changing_song) await this.try_play() await this.try_play()
this.current_song = new_song //if (this.current_song) this.played_history.push(this.current_song)
this.current_song_id = new_song.id! this.current_song = new_song
this.#emit_on_song_change() this.current_song_id = new_song.id!
} }
/** /**
* Will loop back to first song if already on the last. * 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 * If history present, uses that, if not, relies on Song ID directly from DB
*/ */
async try_previous_song_looping() { previous_song() {
let new_song: Song let new_song: Song
if (this.played_history.length > 0) { if (this.played_history.length > 0) {
new_song = this.played_history.pop()! new_song = this.played_history.pop()!
} else { } else {
let id_i = -1 let id_i = 0;
while (this.db.songs[++id_i].id! < this.current_song_id); while (this.db.songs[++id_i].id! < this.current_song_id);
let next_id = --id_i const next_id = -id_i;
if (next_id == -1) next_id = this.db.songs[this.db.songs.length - 1].id! if (next_id == this.db.songs.length) throw new Error("Won't go past the last song")
new_song = this.db.songs.find((song) => song.id == next_id)! new_song = this.db.songs.find((song) => song.id == next_id)!
} }
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
await this.try_new_song(url) this.new_song(url)
if (this.options?.auto_play_after_changing_song) await this.try_play() this.play()
this.current_song = new_song //if (this.current_song) this.played_history.push(this.current_song)
this.current_song_id = new_song.id! this.current_song_id = new_song.id!
this.#emit_on_song_change() this.current_song = new_song
} }
/** /**
* 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. * won't tell you if the play was successful & will loop back to last song if already on the first.
* If history present, uses that, if not, relies on Song ID directly from DB * If queue present, uses that, if not, relies on Song ID directly from DB
*/ */
previous_song() { previous_song_looping() {
let new_song: Song let new_song: Song
if (this.played_history.length > 0) { if (this.played_history.length > 0) {
new_song = this.played_history.pop()! new_song = this.played_history.pop()!
} else { } else {
let id_i = 0 let id_i = 0;
while (this.db.songs[++id_i].id! < this.current_song_id); while (this.db.songs[++id_i].id! < this.current_song_id);
const next_id = -id_i let next_id = -id_i;
if (next_id == this.db.songs.length) throw new Error("Won't go past the last song") if (next_id == this.db.songs.length) next_id = this.db.songs[this.db.songs.length].id!
new_song = this.db.songs.find((song) => song.id == next_id)! new_song = this.db.songs.find((song) => song.id == next_id)!
} }
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString()
this.new_song(url) this.new_song(url)
if (this.options?.auto_play_after_changing_song) this.play() this.play()
this.current_song_id = new_song.id! //if (this.current_song) this.played_history.push(this.current_song)
this.#emit_on_song_change() this.current_song_id = new_song.id!
this.current_song = new_song this.current_song = new_song
} }
/** /**
* won't tell you if the play was successful & will loop back to last song if already on the first. * 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
* If queue present, uses that, if not, relies on Song ID directly from DB * @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix)
*/ */
previous_song_looping() { format_current_song(id = this.current_song?.id) {
let new_song: Song
if (this.played_history.length > 0) {
new_song = this.played_history.pop()!
} 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) next_id = this.db.songs[this.db.songs.length].id! const curr_song = this.db.songs.find((song) => song.id == id)
new_song = this.db.songs.find((song) => song.id == next_id)! if (!curr_song) {
} return "ID - ID"
const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() }
this.new_song(url) let final_text = ""
if (this.options?.auto_play_after_changing_song) this.play()
this.current_song_id = new_song.id!
this.current_song = new_song
this.#emit_on_song_change()
}
/** 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)
if (!curr_song) {
return "ID - ID"
}
let final_text = ""
for (const artist of curr_song.artists) { final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", "
const curr_artist = artist.get(this.db) as Artist final_text += " - " + curr_song.name
final_text += curr_artist.name + ", "
}
final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " if (curr_song.remix_artists.length > 0) {
final_text += " - " + curr_song.name final_text += " ("
if (curr_song.remix_artists.length > 0) { for (const artist of curr_song.remix_artists) {
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 + ", "
}
}
for (const artist of curr_song.remix_artists) { final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", "
const curr_artist = artist.get(this.db) as Artist final_text += " Remix)"
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 ", " return final_text
final_text += " Remix)" }
}
return final_text /**
} * Will add to queue, if ID is undefined nothing will happen. If ID already is in queue, nothing will happen. For more control use `try_queue_add()`
*/
queue_add(id: number) {
const curr_song = this.db.songs.find((song) => song.id == id)
if (!curr_song) return
if (this.queue.find((song) => song.id == id)) return
this.queue.push(curr_song)
}
/** /**
* Will add to queue, if ID is undefined nothing will happen. If ID already is in queue, nothing will happen. For more control use `try_queue_add()` * Will add to queue. If ID is undefined throws error. if ID is already in queue, throws error.
*/ */
queue_add(id: number) { try_queue_add(id: number) {
const curr_song = this.db.songs.find((song) => song.id == id) const curr_song = this.db.songs.find((song) => song.id == id)
if (!curr_song) return if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`)
if (this.queue.find((song) => song.id == id)) return if (this.queue.find((song) => song.id == id)) throw new Error(`Song of id "${id}" already queued`)
this.queue.push(curr_song) 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)
}
on_song_change(callback: (song_id: number, song_name: string) => void) {
this.#sub_list.push(callback)
}
#emit_on_song_change() {
for (const func of this.#sub_list) {
func(this.current_song_id, this.format_current_song(this.current_song_id))
}
}
} }
type BuilderOptions = { type BuilderOptions = {
use_only_pathname_url?: boolean use_only_pathname_url?: boolean
auto_play_after_seek?: boolean
auto_play_after_changing_song?: boolean
} }
class EuterpeBuilder { class EuterpeBuilder {
#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(private audio_element: HTMLAudioElement, private db: DB, private options?: BuilderOptions) {
if (audio_element === undefined) throw Error("audio_element was undefined") if (audio_element === undefined) throw Error("audio_element was undefined")
// ↓ For old browsers // ↓ For old browsers
const AudioContext = window.AudioContext || window.webkitAudioContext const AudioContext = window.AudioContext || window.webkitAudioContext;
this.#audio_context = new AudioContext() this.#audio_context = new AudioContext()
this.#track = this.#audio_context.createMediaElementSource(audio_element) this.#track = this.#audio_context.createMediaElementSource(audio_element)
this.#gain = this.#audio_context.createGain() this.#gain = this.#audio_context.createGain()
} }
/** /**
* For external use, not kept inside player after connection. * For external use, not kept inside player after connection.
* @returns {AnalyserNode} * @returns {AnalyserNode}
*/ */
add_analyser() { add_analyser() {
const analyser = this.#audio_context.createAnalyser() const analyser = this.#audio_context.createAnalyser()
!this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser) !this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser)
this.#prev_node = analyser this.#prev_node = analyser
return analyser return analyser
} }
/** /**
* For external use, not kept inside player after connection. * For external use, not kept inside player after connection.
* @returns {StereoPannerNode} * @returns {StereoPannerNode}
*/ */
add_stereo_panner_node() { add_stereo_panner_node() {
const panner = this.#audio_context.createStereoPanner() const panner = this.#audio_context.createStereoPanner()
!this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner) !this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner)
this.#prev_node = panner this.#prev_node = panner
return panner return panner
} }
/** /**
* For external use, not kept inside player after connection. * For external use, not kept inside player after connection.
* @returns {StereoPannerNode} * @returns {StereoPannerNode}
*/ */
add_wave_shaper_node() { add_wave_shaper_node() {
const shaper = this.#audio_context.createWaveShaper() const shaper = this.#audio_context.createWaveShaper()
!this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper) !this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper)
this.#prev_node = shaper this.#prev_node = shaper
return shaper return shaper
} }
/** /**
* For additional trickery, you can connect your own node. * For additional trickery, you can connect your own node.
*/ */
connect_custom_node(node: AudioNode) { connect_custom_node(node: AudioNode) {
!this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node) !this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node)
this.#prev_node = node this.#prev_node = node
} }
/** /**
* Only use if you need to connect the #gain before another node, * Only use if you need to connect the #gain before another node,
* eg. if you want the analyser nodes output to be affected by user #gain * eg. if you want the analyser nodes output to be affected by user #gain
*/ */
connect_gain() { connect_gain() {
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain)
this.#prev_node = this.#gain this.#prev_node = this.#gain
this.#is_gain_connected = true this.#is_gain_connected = true
} }
/** /**
* Finishes the build * Finishes the build
* @returns {Euterpe} * @returns {Euterpe}
*/ */
build() { build() {
if (!this.#is_gain_connected) { if (!this.#is_gain_connected) {
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain)
this.#prev_node = this.#gain this.#prev_node = this.#gain
} }
this.#prev_node.connect(this.#audio_context.destination) 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) return new Euterpe(this.db, this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume, undefined, this.options)
} }
} }

View file

@ -1,49 +1,50 @@
import { DB, Artist, Song, RefTo, Ref, Platforms } from "@euterpe.js/music-library" import { DB, Artist, Song, RefTo, Ref, Platforms } from "@euterpe.js/music-library";
export const db = new DB() export const db = new DB
db.add([ db.add([
//The IDs are added incrementally & are 0 based., so first artists ID added is 0, next 1 etc... //The IDs are added incrementally & are 0 based., so first artists ID added is 0, next 1 etc...
//You can specify the ID manually if you want //You can specify the ID manually if you want
new Artist({ new Artist({
name: "Jamie xx" name: "Jamie xx",
}), }),
new Artist({ new Artist({
name: "janz" name: "janz",
}), }),
new Artist({ new Artist({
name: "Machinedrum" name: "Machinedrum",
}), }),
new Artist({ new Artist({
name: "Tanerélle" name: "Tanerélle",
}), }),
new Artist({ new Artist({
name: "Mono/Poly" name: "Mono/Poly",
}), }),
new Artist({ new Artist({
name: "IMANU", name: "IMANU",
links: [[Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]] links: [
}) [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: [new Ref(RefTo.Artists, 2), new Ref(RefTo.Artists, 3), new Ref(RefTo.Artists, 4)],
duration: 252, duration: 252,
name: "Star", name: "Star",
remix_artists: [new Ref(RefTo.Artists, 5)], remix_artists: [new Ref(RefTo.Artists, 5)],
url: new URL("http://127.0.0.1:4200/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3") url: new URL("http://127.0.0.1:4200/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3")
}), }),
new Song({ new Song({
//If you don't like guessing the IDs, then this is also a way to do it //If you don't like guessing the IDs, then this is also a way to do it
artists: [new Ref(RefTo.Artists, db.artists.find((a) => a.name == "Jamie xx")!.id!)], artists: [new Ref(RefTo.Artists, db.artists.find((a) => a.name == "Jamie xx")!.id!)],
duration: 331, duration: 331,
name: "Sleep Sound", name: "Sleep Sound",
url: new URL("http://127.0.0.1:4200/Jamie xx - Sleep Sound.mp3") url: new URL("http://127.0.0.1:4200/Jamie xx - Sleep Sound.mp3")
}), }),
new Song({ new Song({
artists: [new Ref(RefTo.Artists, 1)], artists: [new Ref(RefTo.Artists, 1)],
duration: 75, duration: 75,
name: "wish", name: "wish",
url: new URL("http://127.0.0.1:4200/janz - wish.mp3") url: new URL("http://127.0.0.1:4200/janz - wish.mp3")
}) })
]) ])

View file

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

View file

@ -62,17 +62,17 @@ let curr_song_id = 1;
document.querySelector("#previous")?.addEventListener("click", () => { document.querySelector("#previous")?.addEventListener("click", () => {
curr_song_id-- curr_song_id--
if (curr_song_id < 0) curr_song_id = 2 if (curr_song_id < 0) curr_song_id = 2
music_player.try_new_song(db.songs[curr_song_id].url.pathname).then((s) => { music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then((s) => {
change_current_song_text(db) change_current_song_text(db)
music_player.try_play().catch((err) => { console.log(err) }) music_player.play_async().catch((err) => { console.log(err) })
}, (e) => { console.log(e) }) }, (e) => { console.log(e) })
}) })
document.querySelector("#next")?.addEventListener("click", () => { document.querySelector("#next")?.addEventListener("click", () => {
curr_song_id++ curr_song_id++
if (curr_song_id > 2) curr_song_id = 0 if (curr_song_id > 2) curr_song_id = 0
music_player.try_new_song(db.songs[curr_song_id].url.pathname).then((s) => { music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then((s) => {
change_current_song_text(db) change_current_song_text(db)
music_player.try_play().catch((err) => { console.log(err) }) music_player.play_async().catch((err) => { console.log(err) })
}, (e) => { console.log(e) }) }, (e) => { console.log(e) })
}) })
``` ```

View file

@ -1,33 +1,33 @@
{ {
"name": "@euterpe.js/music-library", "name": "@euterpe.js/music-library",
"version": "1.0.12", "version": "1.0.11",
"type": "module", "type": "module",
"description": "A simple music library, acting as a Local DB as JS Object. Contains everything a person would need to store their music data for website playback.", "description": "A simple music library, acting as a Local DB as JS Object. Contains everything a person would need to store their music data for website playback.",
"main": "./src/index.js", "main": "./src/index.js",
"author": { "author": {
"name": "Djkáťo", "name": "Djkáťo",
"email": "djkatovfx@gmail.com" "email": "djkatovfx@gmail.com"
}, },
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/euterpe-js/euterpe-source.git" "url": "git+https://github.com/euterpe-js/euterpe-source.git"
}, },
"homepage": "https://github.com/euterpe-js/euterpe-source/tree/master/packages/music-library#readme", "homepage": "https://github.com/euterpe-js/euterpe-source/tree/master/packages/music-library#readme",
"keywords": [ "keywords": [
"audio", "audio",
"library", "library",
"music-database", "music-database",
"audio-player", "audio-player",
"webaudio", "webaudio",
"database", "database",
"db" "db"
], ],
"exports": { "exports": {
".": { ".": {
"types": "./src/index.d.ts", "types": "./src/index.d.ts",
"import": "./src/index.js", "import": "./src/index.js",
"require": "./src/lib/music-library.js" "require": "./src/lib/music-library.js"
} }
} }
} }

View file

@ -1,309 +1,299 @@
export { RefTo, Ref, Song, Collection, DB, Artist, Platforms, CollectionType, from_json } export {
RefTo,
Ref,
Song,
Collection,
DB,
Artist,
Platforms,
CollectionType,
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(stuff: Artist[] | Collection[] | Song[] | (Song | Artist | Collection)[]) {
/** All of this adds refrences to the other side of whatever is being added. /** 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 * eg. adding song with refrence to artist, adds refrence of song to artist
* and adds incremental ids * and adds incremental ids
*/ */
let inputs let inputs
if (typeof stuff[Symbol.iterator] != "function") { if (typeof stuff[Symbol.iterator] != "function") {
inputs = [stuff] inputs = [stuff]
} else { } else {
inputs = stuff inputs = stuff
} }
for (const input of inputs) { for (const input of inputs) {
if (input instanceof Artist) { if (input instanceof Artist) {
const artist = input as Artist const artist = input as Artist
if (!artist.id) artist.id = this.artists.length 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
for (const song_ref of col.songs) { else if (input instanceof Collection) {
const curr_song = song_ref.get(this) as Song const col = input as Collection
curr_song.in_collection = new Ref(RefTo.Collections, col.id) if (!col.id) col.id = this.collections.length
}
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
if (song.in_collection) { for (const song_ref of col.songs) {
const curr_col = song.in_collection.get(this) as Collection const curr_song = song_ref.get(this) as Song
curr_col.songs.push(new Ref(RefTo.Songs, song.id)) curr_song.in_collection = new Ref(RefTo.Collections, col.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 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)
}
for (const artist_ref of song.artists) { else if (input instanceof Song) {
const curr_artist = artist_ref.get(this) as Artist const song = input as Song
curr_artist.songs.push(new Ref(RefTo.Songs, song.id)) if (!song.id) song.id = this.songs.length
}
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: { artists?: any, songs?: any, collections?: any }): DB {
const db = new DB() const db = new DB
if (db_stringified.artists) { if (db_stringified.artists) {
for (const artist of db_stringified.artists) { for (const artist of db_stringified.artists) {
if (artist.songs) artist.songs = artist.songs.map((e: any) => ref_from_json(e)) if (artist.songs) artist.songs = artist.songs.map((e: any) => ref_from_json(e))
if (artist.collections) artist.collections = artist.collections.map((e: any) => ref_from_json(e)) if (artist.collections) artist.collections = artist.collections.map((e: any) => ref_from_json(e))
if (artist.links) if (artist.links) artist.links = artist.links.map((e: any) => { try { [e[0] as Platforms, new URL(e[1])] } catch (e) { console.log(e) } })
artist.links = artist.links.map((e: any) => { if (artist.publish_date) artist.publish_date = new Date(JSON.parse(artist.publish_date))
try { if (artist.id) artist.id = artist.id as ID
;[e[0] as Platforms, new URL(e[1])] try { if (artist.pfp) artist.pfp = new URL(artist.pfp) }
} catch (e) { catch (e) { console.error(e), console.error("failed to parse artist URL") }
console.log(e) db.artists.push(artist)
} }
}) }
if (artist.publish_date) artist.publish_date = new Date(JSON.parse(artist.publish_date)) if (db_stringified.songs) {
if (artist.id) artist.id = artist.id as ID for (const song of db_stringified.songs) {
try { try { if (song.url) song.url = new URL(song.url) } catch (e) { console.error("failed to parse song.url" + e) }
if (artist.pfp) artist.pfp = new URL(artist.pfp) if (song.artists) song.artists = song.artists.map((e: any) => ref_from_json(e))
} catch (e) { if (song.remix_artists) song.remix_artists = song.remix_artists.map((e: any) => ref_from_json(e))
console.error(e), console.error("failed to parse artist URL") if (song.in_collection) song.in_collection = ref_from_json(song.in_collection)
} try { if (song.cover) song.cover = new URL(song.cover) }
db.artists.push(artist) 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 (db_stringified.songs) { if (song.id) song.id = song.id as ID
for (const song of db_stringified.songs) { db.songs.push(song)
try { }
if (song.url) song.url = new URL(song.url) }
} catch (e) { if (db_stringified.collections) {
console.error("failed to parse song.url" + e) for (const collection of db_stringified.collections) {
} if (collection.artists) collection.artists = collection.artists.map((e: any) => ref_from_json(e))
if (song.artists) song.artists = song.artists.map((e: any) => ref_from_json(e)) if (collection.songs) collection.songs = collection.songs.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 (collection.type) collection.type = collection.type.map((e: any) => e as CollectionType)
if (song.in_collection) song.in_collection = ref_from_json(song.in_collection) try { if (collection.publish_date) collection.publish_date = new Date(JSON.parse(collection.publish_date)) }
try { catch (e) { console.error(e), console.error("Failed to parse date") }
if (song.cover) song.cover = new URL(song.cover) try { if (collection.cover) collection.cover = new URL(collection.cover) }
} catch (e) { catch (e) { console.error(e), console.error("failed to parse collection cover url") }
console.error(e), console.error("failed to parse artist URL") if (collection.id) collection.id = collection.id as ID
} db.collections.push(collection)
try { }
if (song.publish_date) song.publish_date = new Date(JSON.parse(song.publish_date)) }
} catch (e) { return db
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(

View file

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

View file

@ -5,12 +5,13 @@ A simple, safe AudioContext web music player.
##### Full demo at [github link](https://github.com/euterpe-js/euterpe-source/tree/master/packages/player-web-test) ##### Full demo at [github link](https://github.com/euterpe-js/euterpe-source/tree/master/packages/player-web-test)
All we need to do is import the player builder and build it Euterpe player is very Rust inspired, meaning there's always a safer function. `play()`,`play_async()` and `try_play_async()` for example. The goal is to provide the developer with knowledge of what happened when they ran the function, so they can decide what to do if it failed or succeeded.
```ts ```js
import { MusicPlayerBuilder } from "@euterpe/player"; import { MusicPlayerBuilder } from "@euterpe/player";
const audio_el = document.querySelector("#audio") const audio_el = document.querySelector("#audio")
const music_player_builder = MusicPlayerBuilder(audio_el) const music_player_builder = MusicPlayerBuilder(audio_el)
music_player_builder.start()
// Builder allows for attaching custom nodes if necessary, eg. // Builder allows for attaching custom nodes if necessary, eg.
const panning_node = music_player_builder.add_stereo_panner_node() const panning_node = music_player_builder.add_stereo_panner_node()
@ -21,24 +22,24 @@ waves_shaper_node.oversample = '4x'
const music_player = music_player_builder.build() const music_player = music_player_builder.build()
//Next we add a song URL to the Audio Element, //Next we add a song URL to the Audio Element,
music_player.try_new_song(encodeURI("my_song.ogg")) music_player.try_new_song_async(encodeURI("my_song.ogg"))
//and wait for the user input to resume the AudioContext .then(() => {
document.querySelector("#play")?.addEventListener("click", () => { //and wait for the user input to resume the AudioContext
music_player.try_play() document.querySelector("#play")?.addEventListener("click", () => {
.then( music_player.play_async()
//Easily follow up with what to do next .then(
() => { console.log("Playing!") }, //Easily follow up with what to do next
(e) => alert("Failed to play, " + e) () => { console.log("Playing!") },
) (e) => alert("Failed to play, " + e)
}) )
})
})
``` ```
It's quite easy to give user the control in UI It's quite easy to give user the control in UI
```js
```ts
// Play when user clicks a <button></button> // Play when user clicks a <button></button>
document.querySelector("#play-button")?.addEventListener("click", () => { document.querySelector("#play-button")?.addEventListener("click", () => {
music_player.try_play() music_player.play_async()
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e)) .then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
}) })
// Mute when user clicks another <button></button> // Mute when user clicks another <button></button>
@ -52,8 +53,7 @@ document.querySelector("#volume")?.addEventListener("input", (e) => {
``` ```
Euterpe Player also provides functions to easily track the status of playback. It does this via Subscription/Publisher pattern which publishes every frame ( Using `requestAnimationFrame()`). This allows for always up todate values reflecting on the UI. Euterpe Player also provides functions to easily track the status of playback. It does this via Subscription/Publisher pattern which publishes every frame ( Using `requestAnimationFrame()`). This allows for always up todate values reflecting on the UI.
```js
```ts
// Subscriptions to AudioContext changes, eg. time.. // Subscriptions to AudioContext changes, eg. time..
music_player.on_duration_formatted((time) => { music_player.on_duration_formatted((time) => {
//time == "4:53, "15:59", "1756:15:59"... //time == "4:53, "15:59", "1756:15:59"...

View file

@ -1,6 +1,6 @@
{ {
"name": "@euterpe.js/player", "name": "@euterpe.js/player",
"version": "2.1.0", "version": "2.0.1",
"type": "module", "type": "module",
"description": "A simple, safe AudioContext web music player", "description": "A simple, safe AudioContext web music player",
"main": "./src/index.js", "main": "./src/index.js",

View file

@ -1,445 +1,436 @@
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), 1)
} }
break break
} }
case SubscribeEvents.FormattedDurationTick: { case SubscribeEvents.FormattedDurationTick: {
if (this.el_formatted_duration_tick.includes(func)) { if (this.el_formatted_duration_tick.includes(func)) {
this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1) this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1)
} }
break break
} }
case SubscribeEvents.FormattedCurrentTimeTick: { case SubscribeEvents.FormattedCurrentTimeTick: {
if (this.el_formatted_duration_tick.includes(func)) { if (this.el_formatted_duration_tick.includes(func)) {
this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1) this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1)
} }
break break
} }
} }
} }
emit(event_name: SubscribeEvents, data: any) { emit(event_name: SubscribeEvents, data: any) {
switch (event_name) { switch (event_name) {
case SubscribeEvents.CurrentTimeTick: { case SubscribeEvents.CurrentTimeTick: {
this.el_current_time_tick.forEach((func) => { this.el_current_time_tick.forEach((func) => {
func(data) func(data)
}) })
break break
} }
case SubscribeEvents.FormattedDurationTick: { case SubscribeEvents.FormattedDurationTick: {
this.el_formatted_duration_tick.forEach((func) => { this.el_formatted_duration_tick.forEach((func) => {
func(data) func(data)
}) })
break break
} }
case SubscribeEvents.FormattedCurrentTimeTick: { case SubscribeEvents.FormattedCurrentTimeTick: {
this.el_formatted_current_time_tick.forEach((func) => { this.el_formatted_current_time_tick.forEach((func) => {
func(data) func(data)
}) })
break 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. 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 not resume playback * 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 : " + 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 or seek 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. 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 or try_play 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 and change audio dom elements url. Need to call try_play() 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) => { return new Promise<void>((resolve, reject) => {
this.audio_element.src = this.current_song_path = path this.audio_element.src = this.current_song_path = path
//Found out today about this. Such a nice new way to mass remove event listeners! //Found out today about this. Such a nice new way to mass remove event listeners!
const controller = new AbortController() const controller = new AbortController();
this.audio_element.addEventListener( this.audio_element.addEventListener("canplaythrough", function canplay_listener() {
"canplaythrough", controller.abort()
function canplay_listener() { }, { signal: controller.signal })
controller.abort()
},
{ signal: controller.signal }
)
this.audio_element.addEventListener( this.audio_element.addEventListener("error", function error_listener() {
"error", controller.abort("new src error")
function error_listener() { }, { signal: controller.signal })
controller.abort("new src error")
},
{ signal: controller.signal }
)
this.audio_element.addEventListener( this.audio_element.addEventListener("stalled", function stalled_listener() {
"stalled", controller.abort("new src stalled")
function stalled_listener() { }, { signal: controller.signal })
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", () => { 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") reject(new Error(controller.signal.reason))
resolve() resolve()
}) })
this.is_playing = false
})
}
/**
* Won't tell if you if the song actually got loaded or if it failed. For a safer version use try_new_song_async() unless you don't care about the result
*/
new_song(path: string) {
this.audio_element.src = this.current_song_path = path
this.current_song_duration = this.audio_element.duration
}
/**
* Will parse the duration of the song to make it easy to display in UI
* If somethings undefined it returns "0:00"
*/
get_formatted_duration() {
const dur = this.audio_element.duration
this.current_song_duration = this.audio_element.duration
this.is_playing = false if (dur == 0 || !dur) return "0:00"
})
}
/**
* Won't tell if you if the song actually got loaded or if it failed. For a safer version use try_new_song() unless you don't care about the result
*/
new_song(path: string) {
this.audio_element.src = this.current_song_path = path
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"
*/
get_formatted_duration() {
const dur = this.audio_element.duration
this.current_song_duration = this.audio_element.duration
if (dur == 0 || !dur) return "0:00" // ~ is Bitwise NOT, equivalent to Math.floor()
const hrs = ~~(dur / 3600);
const mins = ~~((dur % 3600) / 60);
const secs = ~~dur % 60;
// ~ is Bitwise NOT, equivalent to Math.floor() let ret = ""
const hrs = ~~(dur / 3600) if (hrs > 0) {
const mins = ~~((dur % 3600) / 60) ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
const secs = ~~dur % 60 }
let ret = "" ret += "" + mins + ":" + (secs < 10 ? "0" : "");
if (hrs > 0) { ret += "" + secs;
ret += "" + hrs + ":" + (mins < 10 ? "0" : "") return ret;
} }
/**
* Will parse the current time of the song to make it easy to display in UI
* If somethings undefined it returns "0:00"
*/
get_formatted_current_time() {
const curr = this.audio_element.currentTime
ret += "" + mins + ":" + (secs < 10 ? "0" : "") if (curr == 0 || !curr) return "0:00"
ret += "" + secs // ~~ is Bitwise OR, equivalent to Math.floor()
return ret const hrs = ~~(curr / 3600);
} const mins = ~~((curr % 3600) / 60);
/** const secs = ~~curr % 60;
* Will parse the current time of the song to make it easy to display in UI
* If somethings undefined it returns "0:00"
*/
get_formatted_current_time() {
const curr = this.audio_element.currentTime
if (curr == 0 || !curr) return "0:00" let ret = ""
// ~~ is Bitwise OR, equivalent to Math.floor() if (hrs > 0) {
const hrs = ~~(curr / 3600) ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
const mins = ~~((curr % 3600) / 60) }
const secs = ~~curr % 60
let ret = "" ret += "" + mins + ":" + (secs < 10 ? "0" : "");
if (hrs > 0) { ret += "" + secs;
ret += "" + hrs + ":" + (mins < 10 ? "0" : "") return ret;
} }
#emit_time() {
const request_id = requestAnimationFrame(this.#emit_time.bind(this))
if (this.audio_element.ended) this.is_playing = false
if (this.audio_element.paused) this.is_playing == false
// if use reactively changes volume directly
this.gain.gain.value = this.volume
ret += "" + mins + ":" + (secs < 10 ? "0" : "") this.time = this.audio_element.currentTime
ret += "" + secs if (this.#pub_sub.el_current_time_tick.length == 0) cancelAnimationFrame(request_id)
return ret this.#pub_sub.emit(SubscribeEvents.CurrentTimeTick, this.time)
} }
#emit_time() { #emit_duration_fmt() {
const request_id = requestAnimationFrame(this.#emit_time.bind(this)) const request_id = requestAnimationFrame(this.#emit_duration_fmt.bind(this))
if (this.audio_element.ended) this.is_playing = false const time = this.get_formatted_duration()
if (this.audio_element.paused) this.is_playing == false if (this.#pub_sub.el_formatted_duration_tick.length == 0) cancelAnimationFrame(request_id)
// if use reactively changes volume directly this.#pub_sub.emit(SubscribeEvents.FormattedDurationTick, time)
this.gain.gain.value = this.volume }
#emit_time_fmt() {
const request_id = requestAnimationFrame(this.#emit_time_fmt.bind(this))
const time = this.get_formatted_current_time()
if (this.#pub_sub.el_formatted_current_time_tick.length == 0) cancelAnimationFrame(request_id)
this.#pub_sub.emit(SubscribeEvents.FormattedCurrentTimeTick, time)
}
/**
* Will give current time every animation frame
*/
on_time_tick(callback: (data: any) => void) {
this.#pub_sub.subscribe(SubscribeEvents.CurrentTimeTick, callback)
this.#emit_time()
}
this.time = this.audio_element.currentTime /**
if (this.#pub_sub.el_current_time_tick.length == 0) cancelAnimationFrame(request_id) * Will give formatted current time via get_formatted_current_time() every animation frame
this.#pub_sub.emit(SubscribeEvents.CurrentTimeTick, this.time) */
} on_time_tick_formatted(callback: (data: any) => void) {
#emit_duration_fmt() { this.#pub_sub.subscribe(SubscribeEvents.FormattedCurrentTimeTick, callback)
const request_id = requestAnimationFrame(this.#emit_duration_fmt.bind(this)) this.#emit_time_fmt()
const time = this.get_formatted_duration() }
if (this.#pub_sub.el_formatted_duration_tick.length == 0) cancelAnimationFrame(request_id) /**
this.#pub_sub.emit(SubscribeEvents.FormattedDurationTick, time) * Will give formatted duration time via get_formatted_duration() every animation frame
} */
#emit_time_fmt() { on_duration_formatted(callback: (data: any) => void) {
const request_id = requestAnimationFrame(this.#emit_time_fmt.bind(this)) this.#pub_sub.subscribe(SubscribeEvents.FormattedDurationTick, callback)
const time = this.get_formatted_current_time() this.#emit_duration_fmt()
if (this.#pub_sub.el_formatted_current_time_tick.length == 0) cancelAnimationFrame(request_id) }
this.#pub_sub.emit(SubscribeEvents.FormattedCurrentTimeTick, time)
}
/**
* Will give current time every animation frame
*/
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
*/
on_time_tick_formatted(callback: (data: any) => void) {
this.#pub_sub.subscribe(SubscribeEvents.FormattedCurrentTimeTick, callback)
this.#emit_time_fmt()
}
/**
* Will give formatted duration time via get_formatted_duration() every animation frame
*/
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) throw Error("audio_element was undefined")
// ↓ For old browsers // ↓ For old browsers
const AudioContext = window.AudioContext || window.webkitAudioContext const AudioContext = window.AudioContext || window.webkitAudioContext;
this.#audio_context = new AudioContext() this.#audio_context = new AudioContext()
this.#track = this.#audio_context.createMediaElementSource(audio_element) this.#track = this.#audio_context.createMediaElementSource(audio_element)
this.#gain = this.#audio_context.createGain() this.#gain = this.#audio_context.createGain()
} }
/** /**
* For external use, not kept inside player after connection. * For external use, not kept inside player after connection.
* @returns {AnalyserNode} * @returns {AnalyserNode}
*/ */
add_analyser() { add_analyser() {
const analyser = this.#audio_context.createAnalyser() const analyser = this.#audio_context.createAnalyser()
!this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser) !this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser)
this.#prev_node = analyser this.#prev_node = analyser
return analyser return analyser
} }
/** /**
* For external use, not kept inside player after connection. * For external use, not kept inside player after connection.
* @returns {StereoPannerNode} * @returns {StereoPannerNode}
*/ */
add_stereo_panner_node() { add_stereo_panner_node() {
const panner = this.#audio_context.createStereoPanner() const panner = this.#audio_context.createStereoPanner()
!this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner) !this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner)
this.#prev_node = panner this.#prev_node = panner
return panner return panner
} }
/** /**
* For external use, not kept inside player after connection. * For external use, not kept inside player after connection.
* @returns {StereoPannerNode} * @returns {StereoPannerNode}
*/ */
add_wave_shaper_node() { add_wave_shaper_node() {
const shaper = this.#audio_context.createWaveShaper() const shaper = this.#audio_context.createWaveShaper()
!this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper) !this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper)
this.#prev_node = shaper this.#prev_node = shaper
return shaper return shaper
} }
/** /**
* For additional trickery, you can connect your own node. * For additional trickery, you can connect your own node.
*/ */
connect_custom_node(node: AudioNode) { connect_custom_node(node: AudioNode) {
!this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node) !this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node)
this.#prev_node = node this.#prev_node = node
} }
/** /**
* Only use if you need to connect the #gain before another node, * Only use if you need to connect the #gain before another node,
* eg. if you want the analyser nodes output to be affected by user #gain * eg. if you want the analyser nodes output to be affected by user #gain
*/ */
connect_gain() { connect_gain() {
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain)
this.#prev_node = this.#gain this.#prev_node = this.#gain
this.#is_gain_connected = true this.#is_gain_connected = true
} }
/** /**
* Finishes the build * Finishes the build
* @returns {Euterpe} * @returns {Euterpe}
*/ */
build() { build() {
if (!this.#is_gain_connected) { if (!this.#is_gain_connected) {
!this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain)
this.#prev_node = this.#gain this.#prev_node = this.#gain
} }
this.#prev_node.connect(this.#audio_context.destination) this.#prev_node.connect(this.#audio_context.destination)
return new MusicPlayer(this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume) return new MusicPlayer(this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume)
} }
} }

View file

@ -1,3 +1,4 @@
/target
# Logs # Logs
logs logs
*.log *.log
@ -24,7 +25,7 @@ dist-ssr
*.sw? *.sw?
# public samples # public samples
public/samples/* # public/samples/*
public/media/* # public/media/*
src/song_list.ts # src/song_list.ts
src/db.js # src/db.js

View file

@ -0,0 +1,11 @@
import { LoaderConfig, T } from "hyperimport";
export default {
buildCommand: ["rustc", "--crate-type", "cdylib", "/home/djkato/Code PF/euterpe-preprocessor/src/lib.rs", "--out-dir", "build/lib.rs"],
outDir: "build/lib.rs",
symbols: {
get_fft_data: {
args: [],
returns: T.cstring
},
}
} satisfies LoaderConfig.Main;

View file

@ -0,0 +1 @@
1697374218769

View file

@ -0,0 +1,4 @@
declare module "*/lib.rs" {
const symbols: import("bun:ffi").ConvertFns<typeof import("./config.ts").default.symbols>;
export = symbols;
}

161
packages/preprocessor/Cargo.lock generated Normal file
View file

@ -0,0 +1,161 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "crossbeam-deque"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [
"cfg-if",
]
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "euterpe-preprocessor"
version = "0.1.0"
dependencies = [
"anyhow",
"hound",
"rayon",
"walkdir",
]
[[package]]
name = "hound"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
[[package]]
name = "memoffset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
[[package]]
name = "rayon"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View file

@ -0,0 +1,12 @@
[package]
name = "euterpe-preprocessor"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
hound = "3.5.1"
rayon = "1.8.0"
walkdir = "2.4.0"

View file

@ -0,0 +1,14 @@
# How to use
To extend the Rust library, make sure the functions you're exporting have `#[no_mangle]` derived
and they `extern "C"`. To use, just import `{func} from "./lib.rs"`.
To make sure args and return types match, go to `@types/lib.rs/config.ts`
and change the types of args and returns.
```
bun i
bun dev
```
```
open http://localhost:3000
```

Binary file not shown.

BIN
packages/preprocessor/bun.lockb Executable file

Binary file not shown.

View file

@ -0,0 +1,4 @@
preload = ["./node_modules/hyperimport/preload.ts"]
[hyperimport]
loaders = ["rs"]

View file

@ -1,75 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FFT generator</title>
</head>
<body>
<audio id="audio"></audio>
<div id="app"></div>
<script type="module" src="/src/main.ts">
</script>
<div class="button-wrapper">
<button id="analyze">Analyze!</button>
<button id="create-svg">Create svgs!</button>
<div class="button-wrapper">
<div>
<input id="upload" type="file" />
<p>Upload DB</p>
</div>
<a id="download" href="">Download DB</a>
</div>
</div>
<div class="canvas-wrapper">
<svg id="waveform-canvas" viewBox="0 0 500 500" preserveAspectRatio="none"></svg>
</div>
<style>
body {
width: 100%;
min-height: 100vh;
padding: 0;
margin: 0;
/*
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
*/
}
.button-wrapper {
display: flex;
width: 100%;
height: 4rem;
justify-content: center;
align-items: center;
}
.button-wrapper>* {
padding: 1.2rem 2rem;
width: 100%;
margin: 0 auto;
}
svg {
width: 100%;
height: 200px;
border: 1px solid black;
grid-column-start: content;
}
.canvas-wrapper {
display: grid;
grid-template-columns: 0.5fr [content] 1fr 0.5fr;
column-gap: 20px;
justify-items: stretch;
align-items: stretch;
}
</style>
</body>
</html>

View file

@ -1,22 +1,15 @@
{ {
"name": "@euterpe.js/preprocessor", "name": "euterpe-preprocessor",
"private": true, "module": "src/index.ts",
"version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"crawl": "cd src && node crawler.mjs", "dev": "bun run --hot src/index.ts"
"process": "npm run crawl && vite",
"media-process": "cd src && node media_process.js",
"serve": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^5.0.2",
"vite": "^4.4.0"
}, },
"dependencies": { "dependencies": {
"@euterpe.js/music-library": "*", "hono": "^3.7.2",
"filehound": "^1.17.6" "hyperimport": "^0.1.0"
},
"devDependencies": {
"bun-types": "^0.6.2"
} }
} }

View file

@ -1,70 +1,70 @@
{ {
"name": "@euterpe.js/preprocessor", "name": "@euterpe.js/preprocessor",
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application", "projectType": "application",
"sourceRoot": "packages/preprocessor/src", "sourceRoot": "packages/preprocessor/src",
"tags": [], "tags": [],
"targets": { "targets": {
"build": { "build": {
"executor": "@nx/vite:build", "executor": "bun:build",
"outputs": [ "outputs": [
"{options.outputPath}" "{options.outputPath}"
], ],
"defaultConfiguration": "production", "defaultConfiguration": "production",
"options": { "options": {
"outputPath": "dist/packages/preprocessor" "outputPath": "dist/packages/preprocessor"
}, },
"configurations": { "configurations": {
"development": { "development": {
"mode": "development" "mode": "development"
}, },
"production": { "production": {
"mode": "production" "mode": "production"
} }
} }
}, },
"serve": { "serve": {
"executor": "@nx/vite:dev-server", "executor": "bun:dev",
"defaultConfiguration": "development", "defaultConfiguration": "development",
"options": { "options": {
"buildTarget": "preprocessor:build" "buildTarget": "preprocessor:build"
}, },
"configurations": { "configurations": {
"development": { "development": {
"buildTarget": "preprocessor:build:development", "buildTarget": "preprocessor:build:development",
"hmr": true "hmr": true
}, },
"production": { "production": {
"buildTarget": "preprocessor:build:production", "buildTarget": "preprocessor:build:production",
"hmr": false "hmr": false
} }
} }
}, },
"preview": { "preview": {
"executor": "@nx/vite:preview-server", "executor": "bun:start",
"defaultConfiguration": "development", "defaultConfiguration": "development",
"options": { "options": {
"buildTarget": "preprocessor:build" "buildTarget": "preprocessor:build"
}, },
"configurations": { "configurations": {
"development": { "development": {
"buildTarget": "preprocessor:build:development" "buildTarget": "preprocessor:build:development"
}, },
"production": { "production": {
"buildTarget": "preprocessor:build:production" "buildTarget": "preprocessor:build:production"
} }
} }
}, },
"lint": { "lint": {
"executor": "@nx/linter:eslint", "executor": "@nx/linter:eslint",
"outputs": [ "outputs": [
"{options.outputFile}" "{options.outputFile}"
], ],
"options": { "options": {
"lintFilePatterns": [ "lintFilePatterns": [
"packages/preprocessor/**/*.ts" "packages/preprocessor/**/*.ts"
] ]
} }
} }
} }
} }

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FFT generator</title>
</head>
<body>
<audio id="audio"></audio>
<div id="app"></div>
<script type="module" src="main.ts">
</script>
<div class="button-wrapper">
<button id="regenerate">Regenerate</button>
<div>
<input id="upload" type="file" />
<p>Upload DB</p>
</div>
<a id="download" href="">Download DB</a>
</div>
<div class="canvas-wrapper">
<svg id="waveform-canvas" viewBox="0 0 500 500" preserveAspectRatio="none"></svg>
</div>
<style>
body {
width: 100%;
min-height: 100vh;
padding: 0;
margin: 0;
/*
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
*/
}
.button-wrapper {
display: flex;
width: 100%;
height: 4rem;
justify-content: center;
align-items: center;
}
.button-wrapper>* {
padding: 1.2rem 2rem;
width: 100%;
margin: 0 auto;
}
svg {
width: 100%;
height: 200px;
border: 1px solid black;
grid-column-start: content;
}
.canvas-wrapper {
display: grid;
grid-template-columns: 0.5fr [content] 1fr 0.5fr;
column-gap: 20px;
justify-items: stretch;
align-items: stretch;
}
</style>
</body>
</html>

View file

@ -0,0 +1,33 @@
document.getElementById("regenerate").addEventListener("click", async (ev) => {
result = await analyze()
download(JSON.stringify(result.db), "db.json", "text/plain")
})
document.getElementById("upload").addEventListener("change", (ev) => {
/*
audioContext.resume()
const fileReader = new FileReader()
fileReader.readAsText(ev.target.files[0])
fileReader.onload = event => {
let str = JSON.parse(event.target.result)
let new_db = from_json(str)
//-infinity get stringified to null, undo that
for (const song of new_db.songs) {
if (song.fft_data) {
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
}
}
}
result = { db: new_db, analyzer_node: audioContextAnalyser }
}
*/
})
function download(content, fileName, contentType) {
var a = document.querySelector("#download");
var file = new Blob([content], { type: contentType });
a.href = URL.createObjectURL(file);
a.download = fileName;
// a.click();
}

View file

@ -1,6 +0,0 @@
import filehound from "filehound"
import fs from "fs"
const songs = filehound.create().path("../public/samples").ext(["ogg"]).findSync()
fs.writeFile("songs_list.ts", `export const songs = ` + JSON.stringify(songs), "utf8", () => {
1 + 1
})

View file

@ -1,57 +0,0 @@
import { Collection, Ref, RefTo, Song, DB } from "@euterpe.js/music-library"
import { songs } from "./songs_list"
export function generate_db() {
console.log(songs)
// construct db
let db = new DB()
let collections: string[] = new Array()
let new_songs = []
const path_char = songs[0].includes("\\") ? "\\" : "/"
//create collections by folder names
for (let i = 0; i < songs.length; i++) {
const song = songs[i]
const last_i = song.lastIndexOf(path_char)
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 foreforeforelast_i = song.slice(0, foreforelast_i - 1).lastIndexOf("\\")
*/
if (!collections.includes(collection_name)) {
console.log(`creating collection ${collection_name}`)
db.add([
new Collection({
name: collection_name,
songs: [],
artists: []
})
])
collections.push(collection_name)
}
let col = db.collections.find((col) => col.name == collection_name)!
let col_id = col.id
new_songs.push({ song: song, collection_id: col_id! })
}
//create songs
for (let i = 0; i < new_songs.length; i++) {
let song = new_songs[i]
const last_i = song.song.lastIndexOf(path_char)
const name = song.song.slice(last_i + 1)
const song_url = song.song.slice(song.song.indexOf(`public${path_char}`) + 7)
const db_song = new Song({
name: name.slice(0, name.lastIndexOf(".")),
artists: [],
url: new URL(`${window.location.href}${song_url}`.replaceAll("\\", "/")),
duration: 0,
remix_artists: [],
in_collection: new Ref(RefTo.Collections, song.collection_id)
})
db.add([db_song])
}
console.log(db)
return db
}

View file

@ -0,0 +1,14 @@
import { Hono } from 'hono'
import { get_fft_data } from "./lib.rs"
import { serveStatic } from 'hono/cloudflare-workers'
const app = new Hono()
app.use("/media", serveStatic({ root: "./public/media" }))
app.use("/samples", serveStatic({ root: "./public/samples" }))
app.use('/', serveStatic({ root: "./client" }))
app.get("/api/generate", (c) => {
let res = get_fft_data()
return c.json({ res })
})
export default app

View file

@ -0,0 +1,104 @@
use std::{
ffi::{c_char, CString},
path::PathBuf,
};
use anyhow::{bail, Context};
use walkdir::WalkDir;
// Actually returns JSON but ye...
#[no_mangle]
pub extern "C" fn get_fft_data() -> *mut c_char {
let files = crawl_and_analyze(PathBuf::from("../public/media"));
let SVG::from("");
return CString::new("Hello!").unwrap().into_raw();
}
struct File {
pub svg: Option<SVG>,
pub samples: Vec<i16>,
pub path_buf: PathBuf,
}
struct Points {
pub x: i16,
pub y: i16,
}
struct ViewBox {
pub h_0: i16,
pub h_max: i16,
pub w_0: i16,
pub w_max: i16,
}
impl ViewBox {
pub fn default() -> Self {
ViewBox {
h_0: 0,
w_0: 0,
h_max: 500,
w_max: 500,
}
}
}
struct SVG {
view_box: ViewBox,
points: Points,
path: Option<String>,
}
impl SVG {
fn from(samples: Vec<i16>) -> Self {}
fn mutate_points(&mut self) {}
fn catmull_rom_smooth(&mut self) {}
fn normalize(&mut self) {
// Set lowest sample value to 0, max to 1 and make it linear instead of logarythmic
}
}
fn crawl_and_analyze(root: PathBuf) -> anyhow::Result<Vec<PathBuf>> {
// Find all WAV files, return their paths
let file_paths = crawl(root)?;
let files = file_paths.into_iter().map(|p| File {
samples: get_file_samples(p).unwrap(),
svg: None,
path_buf: p,
});
bail!("")
}
fn crawl(root: PathBuf) -> anyhow::Result<Vec<PathBuf>> {
let mut file_paths = vec![];
for path in WalkDir::new(root).max_depth(5) {
if let Ok(path) = path {
if path.file_type().is_file() {
if let Some(ext) = path.path().extension() {
if ext == "wav" {
file_paths.push(path.path().to_path_buf());
}
}
}
}
}
Ok(file_paths)
}
fn get_file_samples(path: PathBuf) -> anyhow::Result<Vec<i16>> {
// Read wav file, return its samples
bail!("")
}
/*
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}*/

View file

@ -1,150 +0,0 @@
import { DB, from_json } from "@euterpe.js/music-library"
import { generate_db } from "./generate_db"
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType, WaveformOrientation, WaveformShape } from "@euterpe.js/visualizer"
let result: AnalyzeReturn | undefined
let db = generate_db()
//Create all audio nodes
const audioEl = document.querySelector("#audio") as HTMLAudioElement
const audioContext = new AudioContext()
const track = audioContext.createMediaElementSource(audioEl)
const gain = audioContext.createGain()
gain.gain.value = 0
const audioContextAnalyser = audioContext.createAnalyser()
audioContextAnalyser.fftSize = 32
audioContextAnalyser.smoothingTimeConstant = 0
const analyserBufferLength = audioContextAnalyser.frequencyBinCount
const FFTDataArray = new Float32Array(analyserBufferLength)
//Connect all audio Nodes
track.connect(audioContextAnalyser).connect(gain).connect(audioContext.destination)
document.getElementById("analyze")!.addEventListener("click", async (ev) => {
audioContext.resume()
result = await analyze()
download(JSON.stringify(result.db), "db.json", "text/plain")
})
document.getElementById("create-svg")!.addEventListener("click", (ev) => {
audioContext.resume()
svg()
})
document.getElementById("upload")!.addEventListener("change", (ev) => {
audioContext.resume()
const fileReader = new FileReader()
fileReader.readAsText(ev.target.files[0])
fileReader.onload = (event) => {
let str = JSON.parse(event.target.result)
let new_db = from_json(str)
//-infinity get stringified to null, undo that
for (const song of new_db.songs) {
if (song.fft_data) {
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
}
}
}
result = { db: new_db, analyzer_node: audioContextAnalyser }
}
})
async function svg() {
if (!result) {
alert("not analyzed yet!")
return
}
console.log("Creating svgs...")
const canvas_wrapper = document.querySelector(".canvas-wrapper") as HTMLElement
const waveform_canvas = document.querySelector("#waveform-canvas")?.cloneNode() as SVGSVGElement
canvas_wrapper.childNodes.forEach((c) => c.remove())
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: 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> {
console.clear()
const audioEl = document.querySelector("#audio") as HTMLAudioElement
console.log("analysing...")
const samplingRate = 100
// db.songs.splice(0, 10)
// db.songs.splice(2)
console.log(db)
for (const song of db.songs) {
// const song = db.songs[db.songs.length - 1]
console.log(`Analyzing ${song.name}, ${db.songs.indexOf(song) + 1}/${db.songs.length}`)
//if not loaded yet keep trying
audioEl.src = song.url.href
await awaitLoad(audioEl)
song.duration = audioEl.duration
let currentFFTData = []
for (let curSecond = 0; curSecond < song.duration; curSecond += song.duration / samplingRate) {
console.log("working...")
audioEl.currentTime = curSecond
await audioEl.play()
await new Promise<void>((done) => setTimeout(() => done(), 100))
audioContextAnalyser.getFloatFrequencyData(FFTDataArray)
let volume = 0
FFTDataArray.forEach((element) => {
volume += element
})
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) {
var a = document.querySelector("#download") as HTMLAnchorElement
var file = new Blob([content], { type: contentType })
a.href = URL.createObjectURL(file)
a.download = fileName
// a.click();
}
type AnalyzeReturn = {
analyzer_node: AnalyserNode
db: DB
}
function awaitLoad(audioEl: HTMLAudioElement) {
return new Promise<void>((resolve, reject) => {
audioEl.addEventListener("loadeddata", function () {
if (audioEl.readyState >= 4) {
resolve()
}
})
})
}

View file

@ -1,150 +0,0 @@
/**
* TODO:
* -add back -metadata
*
*/
import filehound from "filehound"
import { execSync, exec } from "child_process"
import { fstat, unlinkSync } from "fs"
function generate_new_photo_sizes(file, currentExtention) {
const path = file.substring(0, file.lastIndexOf("\\"))
file = file.substring(file.lastIndexOf("\\") + 1)
let command = ""
command += `cd "${path}" && konsole -e "`
command += `ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -quality 90 -compression_level 6 "${file}_ogw.webp" `
command += ` -vf scale=1000:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_1000p.webp"`
command += ` -vf scale=800:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_800p.webp"`
command += ` -vf scale=500:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_500p.webp"`
command += ` -vf scale=320:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_320p.webp"`
command += ` && exit"`
exec(command)
}
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(
`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) {
const path = file.substring(0, file.lastIndexOf("\\"))
file = file.substring(file.lastIndexOf("\\") + 1)
let command = ""
command += `konsole -e "`
command += `ffmpeg -y -i '${file}.${currentExtention}' `
//Adds 25ms of delay to all samples
command += `-af 'adelay=25:all=true' `
//So the demo is HQ
if (file.includes("demo")) command += `-c:a libopus -b:a 256k '${file}.ogg'"`
else command += `-c:a libopus -b:a 96k '${file}.ogg'"`
exec(command)
console.log(command)
}
function generate_new_sounds_mp3(file, currentExtention) {
const path = file.substring(0, file.lastIndexOf("\\"))
file = file.substring(file.lastIndexOf("\\") + 1)
let command = ""
command += `konsole -e "`
command += `ffmpeg -y -i '${file}.${currentExtention}' `
//Adds 25ms of delay to all samples
command += `-af 'adelay=25:all=true' `
if (file.includes("demo")) command += `-b:a 256k '${file}.mp3'"`
else command += `-b:a 160k '${file}.mp3'"`
exec(command)
// console.log(command)
}
function generate_new_video_sizes_mp4(file, currentExtention, width_resolutions) {
const path = file.substring(0, file.lastIndexOf("\\"))
file = file.substring(file.lastIndexOf("\\") + 1)
let command = ""
command += `cd "${path}" && `
command += `del ffmpeg2pass-0.log && `
command += `ffmpeg -y -i "${file}.${currentExtention}" `
command += `-vcodec libx264 -g 240 -b:v 3M `
command += `-pass 1 -f mp4 NUL && exit`
exec(command).once("exit", () => {
for (const resolution of width_resolutions) {
let res_command = ""
res_command += `konsole -e "`
res_command += `cd "${path}" && `
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 += '&& exit"'
exec(res_command)
}
})
}
function generate_new_video_sizes_webm(file, currentExtention, width_resolutions) {
const path = file.substring(0, file.lastIndexOf("\\"))
file = file.substring(file.lastIndexOf("\\") + 1)
let command = ""
command += `cd "${path}" && `
command += `del ffmpeg2pass-0.log && `
command += `ffmpeg -y -i "${file}.${currentExtention}" `
command += `-vcodec libvpx-vp9 -cpu-used 0 -deadline good -quality good -g 240 -crf 42 -b:v 0 -c:a libopus -row-mt 1 -tile-rows 2 -tile-columns 4 -threads 16 -auto-alt-ref 6 `
command += `-pass 1 -f webm NUL && exit`
exec(command).once("exit", () => {
for (const resolution of width_resolutions) {
let res_command = ""
res_command += `konsole -e "`
res_command += `cd "${path}" && `
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 += '&& exit"'
exec(res_command)
}
})
}
let dirs = filehound.create().path("../public").directory().findSync()
console.log(dirs)
for (let i = 0; i < dirs.length; i++) {
//gets current name file+ext
let current_folder_files = filehound.create().path(`${dirs[i]}`).findSync()
if (current_folder_files[0] != undefined) {
//if previous encode was cancelled and 2pass log not removed, remove it :)
if (current_folder_files[0].includes("ffmpeg2pass-0.log")) {
try {
unlinkSync(`${dirs[i]}/ffmpeg2pass-0.log`)
} catch (err) {}
current_folder_files = current_folder_files.slice(1)
}
for (let current_media of current_folder_files) {
current_media = [current_media.substring(0, current_media.lastIndexOf(".")), current_media.substring(current_media.lastIndexOf(".") + 1)]
if (current_media[1] == "wav") {
console.log(`${current_media[0]}.${current_media[1]}\n`)
generate_new_sounds_ogg(`${current_media[0]}`, `${current_media[1]}`)
generate_new_sounds_mp3(`${current_media[0]}`, `${current_media[1]}`)
}
/*
if (current_media[1] == "webm" || current_media[1] == "mov" || current_media[1] == "avi" || current_media[1] == "mp4") {
if (/\_\d*p/.test(current_media[0])) continue
// console.log(`Video: ${current_media[0]}.${current_media[1]}\n`)
// generate_new_video_sizes_webm(`${current_media[0]}`, `${current_media[1]}`, [1440, 1080, 720, 480])
// generate_new_video_sizes_mp4(`${current_media[0]}`, `${current_media[1]}`, [1440, 1080, 720, 480])
}
if (current_media[1] == "png" || current_media[1] == "jpg") {
console.log(`.\\${current_media[0]}.${current_media[1]}\n`)
generate_new_photo_sizes(`.\\${current_media[0]}`, `${current_media[1]}`)
}
if (current_media[1] == "gif") {
console.log(`.\\${current_media[0]}.${current_media[1]}\n`)
generate_new_anim_photo_sizes(`.\\${current_media[0]}`, `${current_media[1]}`)
}*/
}
}
}

View file

@ -1,33 +0,0 @@
export const songs = [
"../public/samples/FX/01 HTS Boom Kicks.ogg",
"../public/samples/FX/02 HTS Verbclaps.ogg",
"../public/samples/FX/03 HTS Noisesweep.ogg",
"../public/samples/FX/04 HTS Combined FX.ogg",
"../public/samples/bass/01 HTS Arp Bass.ogg",
"../public/samples/bass/02 HTS Hard Bass.ogg",
"../public/samples/bass/03 HTS Break Bass.ogg",
"../public/samples/bass/04 HTS Sub Bass.ogg",
"../public/samples/demos/01 demo 1 - Violet Delta & Eyhz.ogg",
"../public/samples/demos/02 demo 2 - Walras.ogg",
"../public/samples/demos/03 demo 3 - G4TE-16.ogg",
"../public/samples/demos/04 demo 4 - shadeux.ogg",
"../public/samples/demos/05 demo 5 - crowit.ogg",
"../public/samples/demos/06 demo 6 - sh0wtime.ogg",
"../public/samples/demos/07 demo 7 - nuphory & Luna Lenta.ogg",
"../public/samples/drums/01 HTS Rides and Hats.ogg",
"../public/samples/drums/02 HTS Claps and Hats.ogg",
"../public/samples/drums/03 HTS Club Snares.ogg",
"../public/samples/drums/04 HTS Buildup Snares.ogg",
"../public/samples/kicks/01 HTS Trancekick.ogg",
"../public/samples/kicks/02 HTS Sizzle Layer.ogg",
"../public/samples/kicks/03 HTS Transients.ogg",
"../public/samples/kicks/04 HTS Kick Combined.ogg",
"../public/samples/loops/01 HTS Loop CH.ogg",
"../public/samples/loops/02 HTS Loop OH.ogg",
"../public/samples/loops/03 HTS Perc Loop.ogg",
"../public/samples/loops/04 HTS Full Loop.ogg",
"../public/samples/synths/01 HTS Leads and Bass 01.ogg",
"../public/samples/synths/02 HTS Leads and Pads 01.ogg",
"../public/samples/synths/03 HTS Leads and Bass 02.ogg",
"../public/samples/synths/04 HTS Leads and Pads 02.ogg"
]

View file

@ -1 +0,0 @@
/// <reference types="vite/client" />

View file

@ -1,30 +1,30 @@
{ {
"extends": "../../tsconfig.base.json", "compilerOptions": {
"files": [], "esModuleInterop": true,
"compilerOptions": { "jsxImportSource": "hono/jsx",
"target": "ESNext", "jsx": "react-jsx",
"useDefineForClassFields": true, "lib": [
"module": "ESNext", "ESNext"
"lib": [ ],
"ESNext", "module": "esnext",
"DOM" "target": "esnext",
], "moduleResolution": "bundler",
"moduleResolution": "Node", "moduleDetection": "force",
"strict": true, "allowImportingTsExtensions": true,
"resolveJsonModule": true, "noEmit": true,
"isolatedModules": true, "composite": true,
"esModuleInterop": true, "strict": true,
"noEmit": true, "downlevelIteration": true,
"noUnusedLocals": true, "skipLibCheck": true,
"noUnusedParameters": true, "allowSyntheticDefaultImports": true,
"noImplicitReturns": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": true, "allowJs": true,
"types": [ "types": [
"vite/client" "bun-types" // add Bun global
] ],
}, "include": [
"include": [ "src",
"src", "src/media_process.js"
"src/media_process.js" ]
] }
} }

View file

@ -1,33 +0,0 @@
/// <reference types="vitest" />
import { defineConfig } from "vite"
import viteTsConfigPaths from "vite-tsconfig-paths"
export default defineConfig({
cacheDir: "../../node_modules/.vite/preprocessor",
server: {
port: 4201,
host: "localhost"
},
preview: {
port: 4300,
host: "localhost"
},
plugins: [
viteTsConfigPaths({
root: "."
})
]
// Uncomment this if you are using workers.
// worker: {
// plugins: [
// viteTsConfigPaths({
// root: '../../',
// }),
// ],
// },
})

View file

@ -1,64 +1,43 @@
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(0.5) music_player.change_volume(.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(trapnation_analyser_node, document.querySelector("#trapnation-canvas") as SVGSVGElement)
//Because the to_fft_range is so low, it needs more FFT data. //Because the to_fft_range is so low, it needs more FFT data.
.set_fft_size(8192) .set_fft_size(8192)
//Tells the Visualiser how to parse data which mutates our initial shape //Tells the Visualiser how to parse data which mutates our initial shape
.set_fft_data_tresholds({ .set_fft_data_tresholds({ to_fft_range_i: 3, point_count_i: 40, fft_multiplier_i: 1.5, fft_offset_i: 150 })
to_fft_range_i: 3, .set_fft_time_smoothing(0.6)
point_count_i: 40, //If not using typescript enums, CatmullRom = number 2
fft_multiplier_i: 1.5, .set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
fft_offset_i: 150
})
.set_fft_time_smoothing(0.6)
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
const trapnation_visual = trapnation_visual_builder.build(ShapeType.Circle, false) const trapnation_visual = trapnation_visual_builder.build(ShapeType.Circle, false)
const bar_visual_builder = new AudioVisualBuilder(bar_analyser_node, document.querySelector("#bar-canvas") as SVGSVGElement) const bar_visual_builder = new AudioVisualBuilder(bar_analyser_node, document.querySelector("#bar-canvas") as SVGSVGElement)
.set_fft_size(4096) .set_fft_data_tresholds({ point_count_i: 50, fft_multiplier_i: 2, fft_offset_i: -100 })
.set_fft_data_tresholds({ .set_fft_time_smoothing(0.8)
point_count_i: 20, .set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular)
fft_multiplier_i: 3,
fft_offset_i: -130
})
.set_fft_time_smoothing(0.5)
.set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular)
//currently broken, duno why...
const bar_visual = bar_visual_builder.build(ShapeType.Line, false) const bar_visual = bar_visual_builder.build(ShapeType.Line, false)
const waveform_visual_builder = new AudioVisualBuilder(bar_analyser_node, waveform_canvas) const waveform_visual_builder = new AudioVisualBuilder(bar_analyser_node, waveform_canvas)
.set_fft_data_tresholds({ .set_fft_data_tresholds({ point_count_i: 100, fft_multiplier_i: 1, fft_offset_i: -80 })
point_count_i: 100, .set_fft_time_smoothing(0.8)
fft_multiplier_i: 1, .set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
fft_offset_i: -50 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_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()
@ -104,98 +83,71 @@ waveform_canvas.appendChild(seek_clip_use)
seek_clip_path.appendChild(seek_clip_rect) seek_clip_path.appendChild(seek_clip_rect)
time_clip_path.appendChild(time_clip_rect) time_clip_path.appendChild(time_clip_rect)
*/ */
function convert_range(value: number, r1: number[], r2: number[]) { function convert_range(value: number, r1: number[], r2: number[]) {
return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0] return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0]
} }
waveform_canvas.addEventListener("mousemove", (e) => { waveform_canvas.addEventListener("mousemove", (e) => {
const rect = e.target.getBoundingClientRect() const rect = e.target.getBoundingClientRect()
const x = e.clientX - rect.left const x = e.clientX - rect.left
const resX = convert_range(x, [0, rect.width], [0, waveform_canvas.viewBox.baseVal.width + 40]) const resX = convert_range(x, [0, rect.width], [0, waveform_canvas.viewBox.baseVal.width + 40])
const polygon = `polygon(0 0, ${resX}px 0, ${resX}px 100%, 0 100%)` const polygon = `polygon(0 0, ${resX}px 0, ${resX}px 100%, 0 100%)`
document.documentElement.style.setProperty("--clip-seek-path", polygon) document.documentElement.style.setProperty("--clip-seek-path", polygon)
}) })
waveform_canvas.addEventListener("mouseleave", (e) => { waveform_canvas.addEventListener("mouseleave", (e) => {
const polygon = `polygon(0 0, 0 0, 0 100%, 0 100%)` const polygon = `polygon(0 0, 0 0, 0 100%, 0 100%)`
document.documentElement.style.setProperty("--clip-seek-path", polygon) document.documentElement.style.setProperty("--clip-seek-path", polygon)
}) })
/* /*
* The player part * The player part
*/ */
music_player.try_new_song_async(encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg"))
.then(() => {
let is_seeking = false
document.querySelector("#play")?.addEventListener("click", () => {
music_player.play_async()
.then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e))
})
document.querySelector("#pause")?.addEventListener("click", () => {
music_player.pause()
})
document.querySelector("#mute")?.addEventListener("click", () => {
music_player.mute()
})
document.querySelector("#unmute")?.addEventListener("click", () => {
music_player.unmute()
})
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
music_player.mute_toggle()
})
document.querySelector("#toggle-play")?.addEventListener("click", () => {
music_player.play_toggle_async().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e))
})
document.querySelector("#volume")?.addEventListener("input", (e) => {
music_player.change_volume(e.target?.valueAsNumber)
})
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
is_seeking = true;
})
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => {
alert("Failed seeking! " + e)
})
is_seeking = false
})
// Subscriptions to AudioContext changes, eg. time..
music_player.on_duration_formatted((time) => {
duration_element.innerHTML = time
seek_element.max = "" + music_player.current_song_duration
})
music_player.on_time_tick_formatted((time) => {
current_time_element.innerHTML = time
})
music_player.on_time_tick((time) => {
if (is_seeking) return
seek_element.value = "" + time
const x = `${time / music_player.current_song_duration * 100}%`
const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)`
document.documentElement.style.setProperty("--clip-time-path", polygon)
})
let is_seeking = false }, (e) => console.log(e))
document.querySelector("#play")?.addEventListener("click", () => {
music_player.try_play().then(
() => {
console.log("Playing!")
},
(e) => alert("Failed to play, " + e)
)
})
document.querySelector("#pause")?.addEventListener("click", () => {
music_player.pause()
})
document.querySelector("#mute")?.addEventListener("click", () => {
music_player.mute()
})
document.querySelector("#unmute")?.addEventListener("click", () => {
music_player.unmute()
})
document.querySelector("#toggle-mute")?.addEventListener("click", () => {
music_player.mute_toggle()
})
document.querySelector("#toggle-play")?.addEventListener("click", () => {
music_player.try_play_toggle().then(
(s) => console.log("toggled play/pause"),
(e) => alert("failed to toggle pause/play!" + e)
)
})
document.querySelector("#volume")?.addEventListener("input", (e) => {
music_player.change_volume(e.target?.valueAsNumber)
})
document.querySelector("#seek")?.addEventListener("mousedown", (e) => {
is_seeking = true
})
document.querySelector("#seek")?.addEventListener("mouseup", (e) => {
music_player.try_seek(e.target?.valueAsNumber).then(
() => {
console.log("seeked to " + e.target?.valueAsNumber)
},
() => {
alert("Failed seeking! " + e)
}
)
is_seeking = false
})
// Subscriptions to AudioContext changes, eg. time..
music_player.on_duration_formatted((time) => {
duration_element.innerHTML = time
seek_element.max = "" + music_player.current_song_duration
})
music_player.on_time_tick_formatted((time) => {
current_time_element.innerHTML = time
})
music_player.on_time_tick((time) => {
if (is_seeking) return
seek_element.value = "" + time
const x = `${(time / music_player.current_song_duration) * 100}%`
const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)`
document.documentElement.style.setProperty("--clip-time-path", polygon)
})
music_player.try_new_song(encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg"))

View file

@ -31,9 +31,9 @@ body {
} }
.waveform-time { .waveform-time {
clip-path: var(--clip-time-path); clip-path: var(--clip-time-path);
fill: rgba(0,255,50,0.5); fill: rgba(50,145,200,0.3);
} }
.waveform-seek { .waveform-seek {
clip-path: var(--clip-seek-path); clip-path: var(--clip-seek-path);
fill: rgba(0,50,255,0.5); fill: rgba(50,145,200,0.3);
} }

View file

@ -1,11 +1,14 @@
//To see how to get these, check the preprocessor repo!
export const fft_data = { export const fft_data = {
fft_data: [ "fft_data": [
-106.24, -99.73, -100.98, -101.34, -107.01, -92.38, -84.85, -90.28, -93.68, -95.02, -97.16, -96.32, -99.23, -103.13, -85.57, -98.17, -103.27, -106.24, -99.73, -100.98, -101.34, -107.01, -92.38, -84.85, -90.28, -93.68, -95.02, -97.16,
-107.5, -83.62, -95.23, -97.12, -94.78, -95.93, -101.42, -97.83, -102.42, -111.74, -101.38, -106.8, -111.05, -88.04, -90.88, -97.67, -96.31, -96.32, -99.23, -103.13, -85.57, -98.17, -103.27, -107.5, -83.62, -95.23, -97.12, -94.78,
-96.69, -102.15, -102.03, -100.51, -107.14, -101.48, -101.6, -106.62, -73.94, -79.53, -92.74, -96.08, -96.26, -100.35, -99.13, -102.03, -95.93, -101.42, -97.83, -102.42, -111.74, -101.38, -106.8, -111.05, -88.04, -90.88, -97.67,
-107.4, -93.57, -102.31, -102.3, -109.04, -81.85, -92.79, -100.06, -95.79, -96.49, -99.89, -100.27, -102.69, -107.35, -103.94, -104.64, -96.31, -96.69, -102.15, -102.03, -100.51, -107.14, -101.48, -101.6, -106.62, -73.94,
-104.3, -78.82, -84.2, -95.29, -92.57, -93.47, -98.08, -98.9, -101.56, -109.38, -102.01, -102.51, -104.83, -72.18, -76.52, -91.69, -99.97, -79.53, -92.74, -96.08, -96.26, -100.35, -99.13, -102.03, -107.4, -93.57, -102.31, -102.3,
-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 -109.04, -81.85, -92.79, -100.06, -95.79, -96.49, -99.89, -100.27, -102.69, -107.35,
] -103.94, -104.64, -104.3, -78.82, -84.2, -95.29, -92.57, -93.47, -98.08, -98.9, -101.56,
-109.38, -102.01, -102.51, -104.83, -72.18, -76.52, -91.69, -99.97, -96.63, -98.61, -76.97,
-90.41, -100.38, -106.77, -102.83, -104.46, -108.59, -80.97, -88.05, -100.77, -79.64, -72.3,
-87.96, -92.89, -93.03
],
} }

View file

@ -5,8 +5,7 @@ Euterpe Visualizer is a unique audio visual library for the web. Using SVG eleme
### How to use ### How to use
This library relies on AudioContext API, especially on AnalyserNode. To use the Visualizer without our player, first we need to create an AudioContext, from which we can create and connect an AnalyserNode: This library relies on AudioContext API, especially on AnalyserNode. To use the Visualizer without our player, first we need to create an AudioContext, from which we can create and connect an AnalyserNode:
```js
```ts
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer" import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer"
// Refrence to an <audio id="audio"></audio> element inside your HTML // Refrence to an <audio id="audio"></audio> element inside your HTML
const audio_element = document.querySelector("#audio") as HTMLAudioElement const audio_element = document.querySelector("#audio") as HTMLAudioElement
@ -16,10 +15,8 @@ const track = audio_context.createMediaElementSource(audio_element)
const analyzer = audio_context.createAnalyser() const analyzer = audio_context.createAnalyser()
track.connect(analyzer).connect(audio_context.destination) track.connect(analyzer).connect(audio_context.destination)
``` ```
Now that the AudioContext is ready, we start constructing our Visualizer Now that the AudioContext is ready, we start constructing our Visualizer
```js
```ts
// For more options during the building process, I recommend reading the docs // For more options during the building process, I recommend reading the docs
/* Refrence an /* Refrence an
* "<svg id="canvas" viewBox="0 0 500 500" preserveAspectRatio="none" ></svg>" * "<svg id="canvas" viewBox="0 0 500 500" preserveAspectRatio="none" ></svg>"
@ -28,14 +25,11 @@ Now that the AudioContext is ready, we start constructing our Visualizer
const visualizer = new AudioVisualBuilder(analyser_node, document.querySelector("#canvas") as SVGSVGElement) const visualizer = new AudioVisualBuilder(analyser_node, document.querySelector("#canvas") as SVGSVGElement)
.build(ShapeType.Circle) .build(ShapeType.Circle)
``` ```
And it's ready! Once the audio context starts playing, the visualizer should start generating the path inside our SVG element. And it's ready! Once the audio context starts playing, the visualizer should start generating the path inside our SVG element.
```js
```ts
visualizer.draw() visualizer.draw()
audio_element.play() audio_element.play()
``` ```
This is a minimal setup, and more options during the build process are recommended. Especially the `.set_fft_data_tresholds()` method, as each setting affects another. There's really no scientific method to this for now unfortunately, so some trial and error will be necessary. This is a minimal setup, and more options during the build process are recommended. Especially the `.set_fft_data_tresholds()` method, as each setting affects another. There's really no scientific method to this for now unfortunately, so some trial and error will be necessary.
#### How to use with Euterpe Player #### How to use with Euterpe Player
@ -43,8 +37,7 @@ This is a minimal setup, and more options during the build process are recommend
##### Full demo on how to use together with Euterpe player at [github](https://github.com/euterpe-js/euterpe-source/tree/master/packages/visualizer-web-test). ##### Full demo on how to use together with Euterpe player at [github](https://github.com/euterpe-js/euterpe-source/tree/master/packages/visualizer-web-test).
First we need to create our [Euterpe Player](https://www.npmjs.com/package/@euterpe.js/player) First we need to create our [Euterpe Player](https://www.npmjs.com/package/@euterpe.js/player)
```js
```ts
import { MusicPlayerBuilder } from "@euterpe/player"; import { MusicPlayerBuilder } from "@euterpe/player";
import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer" import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer"
@ -56,9 +49,8 @@ const analyzer_node = music_player_builder.add_analyser()
const music_player = music_player_builder.build() const music_player = music_player_builder.build()
``` ```
Now that the AudioContext is ready, we start constructing our Visualizer Now that the AudioContext is ready, we start constructing our Visualizer
```ts ```js
// For more options during the building process, I recommend reading the docs // For more options during the building process, I recommend reading the docs
/* Refrence an /* Refrence an
* "<svg id="canvas" viewBox="0 0 500 500" preserveAspectRatio="none" ></svg>" * "<svg id="canvas" viewBox="0 0 500 500" preserveAspectRatio="none" ></svg>"
@ -70,18 +62,17 @@ const visual_builder = new AudioVisualBuilder(analyzer_node, document.querySelec
//Tells the Visualiser how to parse data which mutates our initial shape //Tells the Visualiser how to parse data which mutates our initial shape
.set_fft_data_tresholds({ to_fft_range_i: 3, point_count_i: 40, fft_multiplier_i: 1.5, fft_offset_i: 150 }) .set_fft_data_tresholds({ to_fft_range_i: 3, point_count_i: 40, fft_multiplier_i: 1.5, fft_offset_i: 150 })
.set_fft_time_smoothing(0.6) .set_fft_time_smoothing(0.6)
//If not using typescript enums, CatmullRom = number 2
.set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom) .set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom)
const visualizer = visual_builder.build(ShapeType.Circle) const visualizer = visual_builder.build(ShapeType.Circle)
``` ```
And it's ready! Once the audio context starts playing, the visualizer should start generating the path inside our SVG element. And it's ready! Once the audio context starts playing, the visualizer should start generating the path inside our SVG element.
```js
```ts
visualizer.draw() visualizer.draw()
music_player.try_new_song(url) music_player.try_new_song_async(url)
.then( music_player.try_play() .then( music_player.play_async()
.then( .then(
console.log("It's working!") console.log("It's working!")
) )

View file

@ -1,6 +1,6 @@
{ {
"name": "@euterpe.js/visualizer", "name": "@euterpe.js/visualizer",
"version": "1.1.1", "version": "1.1.0",
"type": "module", "type": "module",
"description": "Music visualizer based on SVG and AudioContext", "description": "Music visualizer based on SVG and AudioContext",
"main": "./src/index.js", "main": "./src/index.js",

File diff suppressed because it is too large Load diff

79
test.js
View file

@ -1,50 +1,47 @@
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( audio_element.play().then((s) => {
(s) => { is_playing = true
is_playing = true resolve(s)
resolve(s) }, (r) => {
}, is_playing = false
(r) => { reject(r)
is_playing = false })
reject(r) } else {
} audio_element.pause()
) is_playing = false
} else { resolve(null)
audio_element.pause() }
is_playing = false })
resolve(null)
}
})
} }

View file

@ -35,10 +35,16 @@ invariant(
const graph = readCachedProjectGraph() const graph = readCachedProjectGraph()
const project = graph.nodes[name] const project = graph.nodes[name]
invariant(project, `Could not find project "${name}" in the workspace. Is the project.json configured correctly?`) invariant(
project,
`Could not find project "${name}" in the workspace. Is the project.json configured correctly?`
)
const outputPath = project.data?.targets?.build?.options?.outputPath const outputPath = project.data?.targets?.build?.options?.outputPath
invariant(outputPath, `Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?`) invariant(
outputPath,
`Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?`
)
process.chdir(outputPath) process.chdir(outputPath)
@ -48,7 +54,11 @@ try {
json.version = version json.version = version
writeFileSync(`package.json`, JSON.stringify(json, null, 2)) writeFileSync(`package.json`, JSON.stringify(json, null, 2))
} catch (e) { } catch (e) {
console.error(chalk.bold.red(`Error reading package.json file from library build output.`)) console.error(
chalk.bold.red(
`Error reading package.json file from library build output.`
)
)
} }
// Execute "npm publish" to publish // Execute "npm publish" to publish

View file

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