diff --git a/babel.config.json b/babel.config.json index bccf492..9ee5df2 100644 --- a/babel.config.json +++ b/babel.config.json @@ -1,5 +1,5 @@ { - "babelrcRoots": [ - "*" - ] + "babelrcRoots": [ + "*" + ] } diff --git a/nx.json b/nx.json index c873791..03f7f22 100644 --- a/nx.json +++ b/nx.json @@ -1,76 +1,76 @@ { - "$schema": "./node_modules/nx/schemas/nx-schema.json", - "affected": { - "defaultBase": "master" - }, - "tasksRunnerOptions": { - "default": { - "runner": "nx/tasks-runners/default", - "options": { - "cacheableOperations": [ - "build", - "lint", - "test", - "e2e" - ] - } - } - }, - "targetDefaults": { - "build": { - "dependsOn": [ - "^build" - ], - "inputs": [ - "production", - "^production" - ] - }, - "lint": { - "inputs": [ - "default", - "{workspaceRoot}/.eslintrc.json", - "{workspaceRoot}/.eslintignore" - ] - }, - "e2e": { - "inputs": [ - "default", - "^production" - ] - }, - "test": { - "inputs": [ - "default", - "^production" - ] - } - }, - "namedInputs": { - "default": [ - "{projectRoot}/**/*", - "sharedGlobals" - ], - "production": [ - "default", - "!{projectRoot}/.eslintrc.json", - "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", - "!{projectRoot}/tsconfig.spec.json" - ], - "sharedGlobals": [ - "{workspaceRoot}/babel.config.json" - ] - }, - "workspaceLayout": { - "appsDir": "packages", - "libsDir": "packages" - }, - "generators": { - "@nx/web:application": { - "style": "css", - "linter": "eslint", - "unitTestRunner": "vitest", - "e2eTestRunner": "cypress" - } - } + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "affected": { + "defaultBase": "master" + }, + "tasksRunnerOptions": { + "default": { + "runner": "nx/tasks-runners/default", + "options": { + "cacheableOperations": [ + "build", + "lint", + "test", + "e2e" + ] + } + } + }, + "targetDefaults": { + "build": { + "dependsOn": [ + "^build" + ], + "inputs": [ + "production", + "^production" + ] + }, + "lint": { + "inputs": [ + "default", + "{workspaceRoot}/.eslintrc.json", + "{workspaceRoot}/.eslintignore" + ] + }, + "e2e": { + "inputs": [ + "default", + "^production" + ] + }, + "test": { + "inputs": [ + "default", + "^production" + ] + } + }, + "namedInputs": { + "default": [ + "{projectRoot}/**/*", + "sharedGlobals" + ], + "production": [ + "default", + "!{projectRoot}/.eslintrc.json", + "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", + "!{projectRoot}/tsconfig.spec.json" + ], + "sharedGlobals": [ + "{workspaceRoot}/babel.config.json" + ] + }, + "workspaceLayout": { + "appsDir": "packages", + "libsDir": "packages" + }, + "generators": { + "@nx/web:application": { + "style": "css", + "linter": "eslint", + "unitTestRunner": "vitest", + "e2eTestRunner": "cypress" + } + } } diff --git a/package.json b/package.json index 0c1b73d..da9dce9 100644 --- a/package.json +++ b/package.json @@ -1,44 +1,45 @@ { - "name": "@euterpe.js/source", - "version": "0.0.0", - "license": "MIT", - "scripts": { - "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-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-all": "npm run publish-player && npm run publish-library && npm run publish-visualizer && npm run publish-euterpe" - }, - "private": false, - "devDependencies": { - "@nx/cypress": "16.2.1", - "@nx/eslint-plugin": "16.2.1", - "@nx/js": "16.2.1", - "@nx/linter": "16.2.1", - "@nx/vite": "^16.2.1", - "@nx/web": "^16.2.1", - "@nx/workspace": "16.2.1", - "@swc/core": "~1.3.51", - "@types/node": "^20.2.1", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", - "@vitest/coverage-c8": "^0.31.0", - "@vitest/ui": "^0.31.0", - "cypress": "^12.11.0", - "eslint": "~8.15.0", - "eslint-config-prettier": "8.1.0", - "eslint-plugin-cypress": "^2.10.3", - "jsdom": "~20.0.3", - "nx": "16.2.1", - "prettier": "^2.6.2", - "swc-loader": "0.1.15", - "typescript": "~5.0.2", - "vite": "^4.3.4", - "vite-plugin-eslint": "^1.8.1", - "vite-tsconfig-paths": "^4.0.2", - "vitest": "^0.31.0" - }, - "dependencies": { - "tslib": "^2.3.0" - } + "name": "@euterpe.js/source", + "version": "0.0.0", + "license": "MIT", + "scripts": { + "format": "prettier --write '**/*.{js,ts,css,html,json,mjs}'", + "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-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-all": "npm run publish-player && npm run publish-library && npm run publish-visualizer && npm run publish-euterpe" + }, + "private": false, + "devDependencies": { + "@nx/cypress": "16.2.1", + "@nx/eslint-plugin": "16.2.1", + "@nx/js": "16.2.1", + "@nx/linter": "16.2.1", + "@nx/vite": "^16.2.1", + "@nx/web": "^16.2.1", + "@nx/workspace": "16.2.1", + "@swc/core": "~1.3.51", + "@types/node": "^20.2.1", + "@typescript-eslint/eslint-plugin": "^5.58.0", + "@typescript-eslint/parser": "^5.58.0", + "@vitest/coverage-c8": "^0.31.0", + "@vitest/ui": "^0.31.0", + "cypress": "^12.11.0", + "eslint": "~8.15.0", + "eslint-config-prettier": "8.1.0", + "eslint-plugin-cypress": "^2.10.3", + "jsdom": "~20.0.3", + "nx": "16.2.1", + "prettier": "^2.6.2", + "swc-loader": "0.1.15", + "typescript": "~5.0.2", + "vite": "^4.3.4", + "vite-plugin-eslint": "^1.8.1", + "vite-tsconfig-paths": "^4.0.2", + "vitest": "^0.31.0" + }, + "dependencies": { + "tslib": "^2.3.0" + } } diff --git a/packages/dj/src/db_extend.ts b/packages/dj/src/db_extend.ts index 00dc53e..3525403 100644 --- a/packages/dj/src/db_extend.ts +++ b/packages/dj/src/db_extend.ts @@ -1,74 +1,94 @@ -import { Artist, Collection, DB, Ref, RefTo, Song } from "@euterpe.js/music-library"; +import { + Artist, + Collection, + DB, + Ref, + RefTo, + Song +} from "@euterpe.js/music-library" export { DJSong, DJDB } type ID = number interface SongConstructor { - name: string - artists?: Ref[] - url: URL - duration?: number - publish_date?: Date - remix_artists?: Ref[] - in_collection?: Ref - cover?: URL - bpm?: number - key?: string - fft_data?: number[] - id?: ID - metadata?: any[] + name: string + artists?: Ref[] + url: URL + duration?: number + publish_date?: Date + remix_artists?: Ref[] + in_collection?: Ref + cover?: URL + bpm?: number + key?: string + fft_data?: number[] + id?: ID + metadata?: any[] } class DJSong extends Song { - audio_buffer?: AudioBuffer - constructor(data: SongConstructor, audio_context?: AudioContext) { - super(data) + audio_buffer?: AudioBuffer + constructor(data: SongConstructor, audio_context?: AudioContext) { + super(data) - if (!audio_context) return - try { - fetch(data.url).then((file) => { - file.arrayBuffer().then((buffer) => { - audio_context.decodeAudioData(buffer).then((audio_buffer) => { - this.audio_buffer = audio_buffer - }) - }) - }); - } catch (e) { - console.error(new Error("Failed to preprocess DJSong. " + e)) - } - } - public async analyze(url: URL, audio_context: AudioContext) { - this.audio_buffer = await audio_context.decodeAudioData(await (await fetch(url)).arrayBuffer()) - } + if (!audio_context) return + try { + fetch(data.url).then((file) => { + file.arrayBuffer().then((buffer) => { + audio_context + .decodeAudioData(buffer) + .then((audio_buffer) => { + this.audio_buffer = audio_buffer + }) + }) + }) + } catch (e) { + console.error(new Error("Failed to preprocess DJSong. " + e)) + } + } + public async analyze(url: URL, audio_context: AudioContext) { + this.audio_buffer = await audio_context.decodeAudioData( + await (await fetch(url)).arrayBuffer() + ) + } } class DJDB extends DB { - dj_add(dj_songs: DJSong[]): void { - let inputs - typeof dj_songs[Symbol.iterator] == "function" ? inputs = dj_songs : inputs = [dj_songs] - for (const input of inputs) { - if (input instanceof DJSong) { - const song = input as DJSong - if (!song.id) song.id = this.songs.length + dj_add(dj_songs: DJSong[]): void { + let inputs + typeof dj_songs[Symbol.iterator] == "function" + ? (inputs = dj_songs) + : (inputs = [dj_songs]) + for (const input of inputs) { + if (input instanceof DJSong) { + const song = input as DJSong + 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!))) - } + 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.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)) - } + 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) - } - } - } - -} \ No newline at end of file + this.songs.push(song) + } + } + } +} diff --git a/packages/dj/src/euterpe_extend.ts b/packages/dj/src/euterpe_extend.ts index 392fb82..16f868b 100644 --- a/packages/dj/src/euterpe_extend.ts +++ b/packages/dj/src/euterpe_extend.ts @@ -1,125 +1,159 @@ -import { Euterpe } from "@euterpe.js/euterpe"; -import { Song } from "@euterpe.js/music-library"; -import { MusicPlayer } from "@euterpe.js/player"; +import { Euterpe } from "@euterpe.js/euterpe" +import { Song } from "@euterpe.js/music-library" +import { MusicPlayer } from "@euterpe.js/player" export { DJ } /** * 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) */ class DJ { - tracks: Track[] = [] - /**in ms */ - beat_duration?: number - beat = { current: 0, max: 4, next_bar_in: 4 } - on_beat?: (beat: { current: number, max: number, next_bar_in: number }) => void - constructor(public player: Euterpe | MusicPlayer, public master_bpm: number | 120) { - this.beat_duration = 60 / master_bpm - this.#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) - - 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 - */ - 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 - */ - 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 - */ - 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 - } + tracks: Track[] = [] + /**in ms */ + beat_duration?: number + beat = { current: 0, max: 4, next_bar_in: 4 } + on_beat?: (beat: { + current: number + max: number + next_bar_in: number + }) => void + constructor( + public player: Euterpe | MusicPlayer, + public master_bpm: number | 120 + ) { + this.beat_duration = 60 / master_bpm + this.#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)) + 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 + */ + 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 + */ + 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 + */ + 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 { - private audio_buffer?: AudioBuffer - private buffer_source?: AudioBufferSourceNode - gain: GainNode - audio_context: AudioContext | BaseAudioContext + private audio_buffer?: AudioBuffer + private buffer_source?: AudioBufferSourceNode + gain: GainNode + audio_context: AudioContext | BaseAudioContext - constructor(public player: MusicPlayer | Euterpe, public current_song?: Song, public should_loop?: boolean) { - this.audio_context = player.audio_context - this.gain = this.audio_context.createGain() - if (current_song) this.change_song(current_song).catch((e) => console.error("error during track construction - " + e)) - } + constructor( + public player: MusicPlayer | Euterpe, + public current_song?: Song, + public should_loop?: boolean + ) { + this.audio_context = player.audio_context + this.gain = this.audio_context.createGain() + if (current_song) + this.change_song(current_song).catch((e) => + console.error("error during track construction - " + e) + ) + } - async #prepare() { - return new Promise((resolve, reject) => { - if (!this.current_song) reject(new Error("No current song")) - fetch(this.current_song!.url) - .then( - async (file) => { - this.audio_buffer = await this.audio_context.decodeAudioData(await file.arrayBuffer()) - resolve(this) - }, - (reason) => reject(reason)) - }) - } - #connect() { - if (!this.audio_buffer) throw new Error("Somehow buffer not in track even though it analyzed properly. Report this as a bug") - this.buffer_source = this.audio_context.createBufferSource() - this.buffer_source.buffer = this.audio_buffer! - this.buffer_source.connect(this.gain) - this.buffer_source.loop = this.should_loop || false - this.gain.connect(this.player.gain) - } - async change_song(new_song: Song) { - return new Promise((resolve, reject) => { - this.current_song = new_song - this.#prepare().then(() => { - this.#connect() - resolve(this) - }, (reason) => reject(reason)) - }) - } - /** - * - * @param delay in seconds - */ - async try_start(delay?: number) { - return new Promise((resolve, reject) => { - if (!this.buffer_source) reject(new Error("No buffer source yet, set a song first")) - this.buffer_source!.start(this.audio_context.currentTime + (delay || 0)) - }) - } -} \ No newline at end of file + async #prepare() { + return new Promise((resolve, reject) => { + if (!this.current_song) reject(new Error("No current song")) + fetch(this.current_song!.url).then( + async (file) => { + this.audio_buffer = + await this.audio_context.decodeAudioData( + await file.arrayBuffer() + ) + resolve(this) + }, + (reason) => reject(reason) + ) + }) + } + #connect() { + if (!this.audio_buffer) + throw new Error( + "Somehow buffer not in track even though it analyzed properly. Report this as a bug" + ) + this.buffer_source = this.audio_context.createBufferSource() + this.buffer_source.buffer = this.audio_buffer! + this.buffer_source.connect(this.gain) + this.buffer_source.loop = this.should_loop || false + this.gain.connect(this.player.gain) + } + async change_song(new_song: Song) { + return new Promise((resolve, reject) => { + this.current_song = new_song + this.#prepare().then( + () => { + this.#connect() + resolve(this) + }, + (reason) => reject(reason) + ) + }) + } + /** + * + * @param delay in seconds + */ + async try_start(delay?: number) { + return new Promise((resolve, reject) => { + if (!this.buffer_source) + reject(new Error("No buffer source yet, set a song first")) + this.buffer_source!.start( + this.audio_context.currentTime + (delay || 0) + ) + }) + } +} diff --git a/packages/dj/src/index.ts b/packages/dj/src/index.ts index 22f1992..e694042 100644 --- a/packages/dj/src/index.ts +++ b/packages/dj/src/index.ts @@ -1 +1 @@ -import { MusicPlayerBuilder } from "@euterpe.js/player"; \ No newline at end of file +import { MusicPlayerBuilder } from "@euterpe.js/player" diff --git a/packages/euterpe-web-test/src/db.ts b/packages/euterpe-web-test/src/db.ts index 05a44f7..9eca910 100644 --- a/packages/euterpe-web-test/src/db.ts +++ b/packages/euterpe-web-test/src/db.ts @@ -1,62 +1,92 @@ -import { DB, Song, Artist, Ref, RefTo, Platforms } from "@euterpe.js/music-library" -export const db = new DB +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: "Jamie xx", - }), - new Artist({ - name: "janz", - }), - 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")] - ] - }), - new Artist({ - name: "toe", - id: 10 - }), + //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: "Jamie xx" + }), + new Artist({ + name: "janz" + }), + 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" + ) + ] + ] + }), + new Artist({ + name: "toe", + id: 10 + }) ]) 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") - }), - new Song({ - //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!)], - duration: 331, - name: "Sleep Sound", - url: new URL("http://" + window.location.host + "/Jamie xx - Sleep Sound.mp3") - }), - new Song({ - artists: [new Ref(RefTo.Artists, 1)], - duration: 75, - name: "wish", - url: new URL("http://" + window.location.host + "/janz - wish.mp3") - }), - new Song({ - artists: [new Ref(RefTo.Artists, 10)], - duration: 4 * 60 + 5, - name: "サニーボーイ・ラプソディ", - url: new URL("http://" + window.location.host + "/16.サニーボーイ・ラプソディ.ogg") - }) + 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" + ) + }), + new Song({ + //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! + ) + ], + duration: 331, + name: "Sleep Sound", + url: new URL( + "http://" + window.location.host + "/Jamie xx - Sleep Sound.mp3" + ) + }), + new Song({ + artists: [new Ref(RefTo.Artists, 1)], + duration: 75, + name: "wish", + url: new URL("http://" + window.location.host + "/janz - wish.mp3") + }), + new Song({ + artists: [new Ref(RefTo.Artists, 10)], + duration: 4 * 60 + 5, + name: "サニーボーイ・ラプソディ", + url: new URL( + "http://" + + window.location.host + + "/16.サニーボーイ・ラプソディ.ogg" + ) + }) ]) - diff --git a/packages/euterpe-web-test/src/main.ts b/packages/euterpe-web-test/src/main.ts index 83c2996..cecfec4 100644 --- a/packages/euterpe-web-test/src/main.ts +++ b/packages/euterpe-web-test/src/main.ts @@ -1,129 +1,147 @@ -import { db } from "./db"; -import { EuterpeBuilder } from "@euterpe.js/euterpe"; +import { db } from "./db" +import { EuterpeBuilder } from "@euterpe.js/euterpe" let is_seeking = false // document.addEventListener("click", start, { once: true }) -const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db) - .build() +const euterpe = new EuterpeBuilder( + document.querySelector("#audio")!, + db +).build() add_library_to_dom() -euterpe.try_preload_song(0).then(() => { - document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() -}, (e) => console.log(e + " Failed to preload")) +euterpe.try_preload_song(0).then( + () => { + document.querySelector("#text-playing")!.innerHTML = + euterpe.format_current_song() + }, + (e) => console.log(e + " Failed to preload") +) document.querySelector("#seek")?.addEventListener("mouseup", (e) => { - try { - euterpe.try_seek(e.target?.valueAsNumber) - console.log("seeked to " + e.target?.valueAsNumber) - } catch { - alert("Failed seeking! " + e) - } - is_seeking = false + try { + euterpe.try_seek(e.target?.valueAsNumber) + console.log("seeked to " + e.target?.valueAsNumber) + } catch { + alert("Failed seeking! " + e) + } + is_seeking = false }) // Subscriptions to AudioContext changes, eg. time.. euterpe.on_duration_formatted((time) => { - document.querySelector("#duration")!.innerHTML = time - document.querySelector("#seek")!.max = "" + euterpe.current_song_duration + document.querySelector("#duration")!.innerHTML = time + document.querySelector("#seek")!.max = "" + euterpe.current_song_duration }) euterpe.on_time_tick_formatted((time) => { - document.querySelector("#current")!.innerHTML = 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() + if (is_seeking) return + document.querySelector("#seek")!.value = "" + time + dev_queue_update() + dev_history_update() }) document.querySelector("#previous")?.addEventListener("click", () => { - euterpe.try_previous_song_looping().then(() => { - document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() - }, (e) => alert(e + "Failed to change song")) + euterpe.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", () => { - euterpe.try_next_song_looping().then(() => { - document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() - }, (e) => alert(e + "Failed to change song")) + euterpe.try_next_song_looping().then( + () => { + document.querySelector("#text-playing")!.innerHTML = + euterpe.format_current_song() + }, + (e) => alert(e + "Failed to change song") + ) }) 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", () => { - euterpe.pause() + euterpe.pause() }) document.querySelector("#mute")?.addEventListener("click", () => { - euterpe.mute() + euterpe.mute() }) document.querySelector("#unmute")?.addEventListener("click", () => { - euterpe.unmute() + euterpe.unmute() }) document.querySelector("#toggle-mute")?.addEventListener("click", () => { - euterpe.mute_toggle() + euterpe.mute_toggle() }) 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) => { - 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 document.querySelector("#seek")?.addEventListener("mousedown", () => { - is_seeking = true; + is_seeking = true }) function add_library_to_dom() { - const lib_dom = document.querySelector(".library-wrapper") as HTMLDivElement - for (const song of euterpe.db.songs) { - const div = document.createElement("div") - const p = document.createElement("p") - const button_play = document.createElement("button") - const button_queue = document.createElement("button") - const span = document.createElement("span") - p.innerHTML = `${euterpe.format_current_song(song.id)}` + const lib_dom = document.querySelector(".library-wrapper") as HTMLDivElement + for (const song of euterpe.db.songs) { + const div = document.createElement("div") + const p = document.createElement("p") + const button_play = document.createElement("button") + const button_queue = document.createElement("button") + const span = document.createElement("span") + p.innerHTML = `${euterpe.format_current_song(song.id)}` - button_play.innerHTML = "play" - button_play.dataset["id"] = `${song.id}` - button_play.onclick = library_play + button_play.innerHTML = "play" + button_play.dataset["id"] = `${song.id}` + button_play.onclick = library_play - button_queue.innerHTML = "queue" - button_queue.dataset["id"] = `${song.id}` - button_queue.onclick = library_queue + button_queue.innerHTML = "queue" + button_queue.dataset["id"] = `${song.id}` + button_queue.onclick = library_queue - div.appendChild(p) - span.appendChild(button_play) - span.appendChild(button_queue) - div.appendChild(span) + div.appendChild(p) + span.appendChild(button_play) + span.appendChild(button_queue) + div.appendChild(span) - lib_dom.appendChild(div) - } + lib_dom.appendChild(div) + } } function library_play(e: MouseEvent) { - const b = e.currentTarget as HTMLButtonElement - euterpe.try_specific_song(parseInt(b.dataset["id"]!)).then( - () => document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song(), - (e) => alert(e) - ) + const b = e.currentTarget as HTMLButtonElement + euterpe.try_specific_song(parseInt(b.dataset["id"]!)).then( + () => + (document.querySelector("#text-playing")!.innerHTML = + euterpe.format_current_song()), + (e) => alert(e) + ) } function library_queue(e: MouseEvent) { - const b = e.currentTarget as HTMLButtonElement - euterpe.queue_append(parseInt(b.dataset["id"]!)) + const b = e.currentTarget as HTMLButtonElement + euterpe.queue_append(parseInt(b.dataset["id"]!)) } 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() + 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() -} \ No newline at end of file + 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() +} diff --git a/packages/euterpe/src/index.ts b/packages/euterpe/src/index.ts index 148d375..ddab7e6 100644 --- a/packages/euterpe/src/index.ts +++ b/packages/euterpe/src/index.ts @@ -6,427 +6,482 @@ export { Euterpe, EuterpeBuilder } * Avoid Writing directly to any fields in this class! */ class Euterpe extends MusicPlayer { - current_song: Song | undefined - current_song_id = 0 - queue: Song[] = [] - played_history: Song[] = [] - constructor( - public db: DB, - audio_context: AudioContext, - audio_element: HTMLAudioElement, - track: MediaElementAudioSourceNode, - gain: GainNode, - volume: number, - current_song_path?: string, - private options?: BuilderOptions - ) { + current_song: Song | undefined + current_song_id = 0 + queue: Song[] = [] + played_history: Song[] = [] + constructor( + public db: DB, + audio_context: AudioContext, + audio_element: HTMLAudioElement, + track: MediaElementAudioSourceNode, + gain: GainNode, + volume: number, + current_song_path?: string, + private options?: BuilderOptions + ) { + super( + audio_context, + audio_element, + track, + gain, + volume, + current_song_path + ) + } - super(audio_context, audio_element, track, gain, volume, current_song_path) - } + /** + * Use to load song on page load. + * @throws if song with ID doesn't exist + */ + async try_preload_song(id: number) { + const next = this.db.songs.find((song) => song!.id == id) + if (!next) throw new Error(`Song with id ${id} doesn't exist`) + else { + await this.try_new_song(next.url.pathname) + this.current_song = next + } + } - /** - * Use to load song on page load. - * @throws if song with ID doesn't exist - */ - async try_preload_song(id: number) { - const next = this.db.songs.find((song) => song!.id == id) - if (!next) throw new Error(`Song with id ${id} doesn't exist`) - else { - await this.try_new_song(next.url.pathname) - this.current_song = next - } - } + /** + * Won't loop back to first song if already on the last. + * 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() { + let new_song: Song + 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 - /** - * Won't loop back to first song if already on the last. - * 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() { - let new_song: Song - 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") + 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() + await this.try_new_song(url) + await this.try_play() + if (this.current_song) this.played_history.push(this.current_song) + this.current_song = new_song + this.current_song_id = new_song.id! + } - 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)! - } - const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() - await this.try_new_song(url) - await this.try_play() - if (this.current_song) this.played_history.push(this.current_song) - this.current_song = new_song - this.current_song_id = new_song.id! - } + /** + * Will loop back to first song if already on last song, + * If queue present, uses that, if not, relies on Song ID directly from DB + */ + async try_next_song_looping() { + let new_song: Song + 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); + let next_id = ++id_i - /** - * 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 - */ - async try_next_song_looping() { - let new_song: Song - 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); - let next_id = ++id_i + if (next_id == this.db.songs.length) next_id = this.db.songs[0].id! + new_song = this.db.songs.find((song) => song.id == next_id)! + } + const url = this.options?.use_only_pathname_url + ? new_song.url.pathname + : new_song.url.toString() + await this.try_new_song(url) + await this.try_play() + if (this.current_song) this.played_history.push(this.current_song) + 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)! - } - const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() - await this.try_new_song(url) - await this.try_play() - if (this.current_song) this.played_history.push(this.current_song) - this.current_song = new_song - this.current_song_id = new_song.id! - } + /** + * Won't tell you if the playback was successsful & wil loop back if already on last song. Best use try_next_song_async() + * If queue present, uses that, if not, relies on Song ID directly from DB + */ + next_song_looping() { + let new_song: Song + 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); + let next_id = ++id_i - /** - * 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 - */ - next_song_looping() { - let new_song: Song - 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); - let next_id = ++id_i; + if (next_id == this.db.songs.length) next_id = this.db.songs[0].id! + new_song = this.db.songs.find((song) => song.id == next_id)! + } + 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)! - } - const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() - this.new_song(url) - this.play() - if (this.current_song) this.played_history.push(this.current_song) - this.current_song = new_song - this.current_song_id = new_song.id! - } + /** + * Won't tell you if the playback was successsful, won't loop back if already on last song and won't throw error if attempted. Best use next_song_async() + * If queue present, uses that, if not, relies on Song ID directly from DB + */ + next_song() { + let new_song: Song + 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 + new_song = this.db.songs.find((song) => song.id == next_id)! + } + const url = this.options?.use_only_pathname_url + ? new_song.url.pathname + : new_song.url.toString() + this.new_song(url) + this.play() + if (this.current_song) this.played_history.push(this.current_song) + this.current_song = new_song + this.current_song_id = new_song.id! + } - /** - * Won't tell you if the playback was successsful, won't loop back if already on last song and won't throw error if attempted. Best use next_song_async() - * If queue present, uses that, if not, relies on Song ID directly from DB - */ - next_song() { - let new_song: Song - 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; - new_song = this.db.songs.find((song) => song.id == next_id)! - } - const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() - this.new_song(url) - this.play() - if (this.current_song) this.played_history.push(this.current_song) - this.current_song = new_song - this.current_song_id = new_song.id! - } + /** + * Uses safer try_play_async. Normal play / play_async will try to start the player even if the track hasn't started yet, or was previously suspended/closed + */ + async try_specific_song(new_song_id: number) { + const new_song = this.db.songs.find((song) => song.id! == new_song_id) + if (!new_song) throw new Error(`No song with id "${new_song_id}" found`) + else { + this.try_new_song(new_song.url.pathname) + await this.try_play() + if (this.current_song) this.played_history.push(this.current_song) + this.current_song = new_song + this.current_song_id = new_song.id! + } + } - /** - * Uses safer try_play_async. Normal play / play_async will try to start the player even if the track hasn't started yet, or was previously suspended/closed - */ - async try_specific_song(new_song_id: number) { - const new_song = this.db.songs.find((song) => song.id! == new_song_id) - if (!new_song) throw new Error(`No song with id "${new_song_id}" found`) - else { - this.try_new_song(new_song.url.pathname) - await this.try_play() - if (this.current_song) this.played_history.push(this.current_song) - this.current_song = new_song - this.current_song_id = new_song.id! - } - } + /** + * Won't throw an error if new ID not found. Won't tell you if the play was successful, best use specific_song_async() or try_specific_song_async() + */ + specific_song(new_song_id: number) { + const new_song = this.db.songs.find((song) => song.id! == new_song_id) + if (!new_song) return + const url = this.options?.use_only_pathname_url + ? new_song.url.pathname + : new_song.url.toString() + this.new_song(url) + this.play() + if (this.current_song) this.played_history.push(this.current_song) + this.current_song = new_song + this.current_song_id = new_song.id! + } - /** - * Won't throw an error if new ID not found. Won't tell you if the play was successful, best use specific_song_async() or try_specific_song_async() - */ - specific_song(new_song_id: number) { - const new_song = this.db.songs.find((song) => song.id! == new_song_id) - if (!new_song) return - const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() - this.new_song(url) - this.play() - if (this.current_song) this.played_history.push(this.current_song) - this.current_song = new_song - this.current_song_id = new_song.id! - } + /** + * 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 + * @throws if playback was unsuccessful or at first song/ can't go more previous + */ + async try_previous_song() { + 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); + const next_id = --id_i - /** - * 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 - * @throws if playback was unsuccessful or at first song/ can't go more previous - */ - async try_previous_song() { - 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); - const next_id = --id_i; + if (next_id == this.db.songs.length) + throw new Error("Won't roll backwards to last song") + new_song = this.db.songs.find((song) => song.id == next_id)! + } + const url = this.options?.use_only_pathname_url + ? new_song.url.pathname + : new_song.url.toString() + await this.try_new_song(url) + await this.try_play() + //if (this.current_song) this.played_history.push(this.current_song) + this.current_song = new_song + this.current_song_id = new_song.id! + } - if (next_id == this.db.songs.length) throw new Error("Won't roll backwards to last song") - new_song = this.db.songs.find((song) => song.id == next_id)! - } - const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() - await this.try_new_song(url) - await this.try_play() - //if (this.current_song) this.played_history.push(this.current_song) - this.current_song = new_song - this.current_song_id = new_song.id! - } + /** + * Will loop back to first song if already on the last. + * If history present, uses that, if not, relies on Song ID directly from DB + */ + async try_previous_song_looping() { + let new_song: Song + if (this.played_history.length > 0) { + new_song = this.played_history.pop()! + } else { + let id_i = -1 + while (this.db.songs[++id_i].id! < this.current_song_id); + let next_id = --id_i - /** - * Will loop back to first song if already on the last. - * If history present, uses that, if not, relies on Song ID directly from DB - */ - async try_previous_song_looping() { - let new_song: Song - if (this.played_history.length > 0) { - new_song = this.played_history.pop()! - } else { - let id_i = -1; - while (this.db.songs[++id_i].id! < this.current_song_id); - let next_id = --id_i; + if (next_id == -1) + next_id = this.db.songs[this.db.songs.length - 1].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() + await this.try_new_song(url) + await this.try_play() + //if (this.current_song) this.played_history.push(this.current_song) + this.current_song = new_song + this.current_song_id = new_song.id! + } - 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)! - } - const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() - await this.try_new_song(url) - await this.try_play() - //if (this.current_song) this.played_history.push(this.current_song) - this.current_song = new_song - this.current_song_id = new_song.id! - } + /** + * won't tell you if the play was successful, won't loop back to last song if already on the first and will throw error if attempted. + * If history present, uses that, if not, relies on Song ID directly from DB + */ + previous_song() { + 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); + const next_id = -id_i - /** - * 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 - */ - previous_song() { - 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); - const next_id = -id_i; + if (next_id == this.db.songs.length) + throw new Error("Won't go past the last song") + new_song = this.db.songs.find((song) => song.id == next_id)! + } + 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_id = new_song.id! + this.current_song = new_song + } - if (next_id == this.db.songs.length) throw new Error("Won't go past the last song") - new_song = this.db.songs.find((song) => song.id == next_id)! - } - 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_id = new_song.id! - 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. + * If queue present, uses that, if not, relies on Song ID directly from DB + */ + previous_song_looping() { + 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 - /** - * won't tell you if the play was successful & will loop back to last song if already on the first. - * If queue present, uses that, if not, relies on Song ID directly from DB - */ - previous_song_looping() { - 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! + 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_id = new_song.id! + this.current_song = new_song + } - if (next_id == this.db.songs.length) next_id = this.db.songs[this.db.songs.length].id! - new_song = this.db.songs.find((song) => song.id == next_id)! - } - 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_id = new_song.id! - this.current_song = new_song - } + /** + * Takes the song data from current song if no song ID is specified. Will return "ID - ID" if ID and current song doesn't exist + * @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix) + */ + 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 = "" - /** - * Takes the song data from current song if no song ID is specified. Will return "ID - ID" if ID and current song doesn't exist - * @returns {ARTIST}, {ARTIST2}... - {SONG NAME} ({REMIX ARTIST}, {REMIX ARTIST2}... remix) - */ - format_current_song(id = this.current_song?.id) { + for (const artist of curr_song.artists) { + const curr_artist = artist.get(this.db) as Artist + final_text += curr_artist.name + ", " + } - const curr_song = this.db.songs.find((song) => song.id == id) - if (!curr_song) { - return "ID - ID" - } - let final_text = "" + final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " + final_text += " - " + curr_song.name - for (const artist of curr_song.artists) { - const curr_artist = artist.get(this.db) as Artist - final_text += curr_artist.name + ", " - } + if (curr_song.remix_artists.length > 0) { + final_text += " (" - final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " - final_text += " - " + curr_song.name + for (const artist of curr_song.remix_artists) { + const curr_artist = artist.get(this.db) as Artist + if (curr_artist.links && curr_artist.links.length > 0) { + final_text += curr_artist.name + } else { + final_text += curr_artist.name + ", " + } + } - if (curr_song.remix_artists.length > 0) { - final_text += " (" + final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " + final_text += " Remix)" + } - for (const artist of curr_song.remix_artists) { - 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 + ", " - } - } + return final_text + } - final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " - 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 throws error. if ID is already in queue, throws error. - */ - try_queue_add(id: number) { - const curr_song = this.db.songs.find((song) => song.id == id) - if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`) - if (this.queue.find((song) => song.id == id)) throw new Error(`Song of id "${id}" already queued`) - this.queue.push(curr_song) - } - /** - * Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. Throws error if ID doesn't exist. - */ - try_queue_append(id: number) { - const curr_song = this.db.songs.find((song) => song.id == id) - if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`) - const i = this.queue.findIndex((song) => song.id == id) - if (i != -1) this.queue.push(this.queue.splice(i, 1)[0]) - else this.queue.push(curr_song) - } - /** - * Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. If ID Doesn't exist, does nothing. For more control use try_queue_append() - */ - queue_append(id: number) { - const curr_song = this.db.songs.find((song) => song.id == id) - if (!curr_song) return - const i = this.queue.findIndex((song) => song.id == id) - if (i != -1) this.queue.push(this.queue.splice(i, 1)[0]) - else this.queue.push(curr_song) - } - /** - * Removes song of ID from queue and returns it. Does and returns nothing if song already not found. - */ - queue_remove(id: number) { - const i = this.queue.findIndex((song) => song.id == id) - if (i == -1) return - return this.queue.splice(i, 1) - } + /** + * Will add to queue, if ID is undefined nothing will happen. If ID already is in queue, nothing will happen. For more control use `try_queue_add()` + */ + queue_add(id: number) { + const curr_song = this.db.songs.find((song) => song.id == id) + if (!curr_song) return + if (this.queue.find((song) => song.id == id)) return + this.queue.push(curr_song) + } + /** + * Will add to queue. If ID is undefined throws error. if ID is already in queue, throws error. + */ + try_queue_add(id: number) { + const curr_song = this.db.songs.find((song) => song.id == id) + if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`) + if (this.queue.find((song) => song.id == id)) + throw new Error(`Song of id "${id}" already queued`) + this.queue.push(curr_song) + } + /** + * Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. Throws error if ID doesn't exist. + */ + try_queue_append(id: number) { + const curr_song = this.db.songs.find((song) => song.id == id) + if (!curr_song) throw new Error(`Song of id "${id}" doesn't exist`) + const i = this.queue.findIndex((song) => song.id == id) + if (i != -1) this.queue.push(this.queue.splice(i, 1)[0]) + else this.queue.push(curr_song) + } + /** + * Will add to queue. Unlike queue_add, if given ID is already in queue, it will move it to the end of the queue. If ID Doesn't exist, does nothing. For more control use try_queue_append() + */ + queue_append(id: number) { + const curr_song = this.db.songs.find((song) => song.id == id) + if (!curr_song) return + const i = this.queue.findIndex((song) => song.id == id) + if (i != -1) this.queue.push(this.queue.splice(i, 1)[0]) + else this.queue.push(curr_song) + } + /** + * Removes song of ID from queue and returns it. Does and returns nothing if song already not found. + */ + queue_remove(id: number) { + const i = this.queue.findIndex((song) => song.id == id) + if (i == -1) return + return this.queue.splice(i, 1) + } } type BuilderOptions = { - use_only_pathname_url?: boolean + use_only_pathname_url?: boolean } class EuterpeBuilder { - #audio_context: AudioContext - #gain: GainNode - #track: MediaElementAudioSourceNode - #volume = 1 - #prev_node: any; + #audio_context: AudioContext + #gain: GainNode + #track: MediaElementAudioSourceNode + #volume = 1 + #prev_node: any - #is_gain_connected = false - /** - * Creates a context and #gain( Gets connected at the end ) - * 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) - */ - constructor(private audio_element: HTMLAudioElement, private db: DB, private options?: BuilderOptions) { - if (audio_element === undefined) throw Error("audio_element was undefined") - // ↓ For old browsers - const AudioContext = window.AudioContext || window.webkitAudioContext; - this.#audio_context = new AudioContext() - this.#track = this.#audio_context.createMediaElementSource(audio_element) - this.#gain = this.#audio_context.createGain() - } - /** - * For external use, not kept inside player after connection. - * @returns {AnalyserNode} - */ - add_analyser() { - const analyser = this.#audio_context.createAnalyser() - !this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser) - this.#prev_node = analyser - return analyser - } - /** - * For external use, not kept inside player after connection. - * @returns {StereoPannerNode} - */ - add_stereo_panner_node() { - const panner = this.#audio_context.createStereoPanner() - !this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner) - this.#prev_node = panner - return panner - } - /** - * For external use, not kept inside player after connection. - * @returns {StereoPannerNode} - */ - add_wave_shaper_node() { - const shaper = this.#audio_context.createWaveShaper() - !this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper) - this.#prev_node = shaper - return shaper - } - /** - * For additional trickery, you can connect your own node. - */ - connect_custom_node(node: AudioNode) { - !this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node) - this.#prev_node = 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 - */ - connect_gain() { - !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) - this.#prev_node = this.#gain - this.#is_gain_connected = true - } - /** - * Finishes the build - * @returns {Euterpe} - */ - build() { - if (!this.#is_gain_connected) { - !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) - this.#prev_node = this.#gain - } - this.#prev_node.connect(this.#audio_context.destination) - return new Euterpe(this.db, this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume, undefined, this.options) - } + #is_gain_connected = false + /** + * Creates a context and #gain( Gets connected at the end ) + * 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) + */ + constructor( + private audio_element: HTMLAudioElement, + private db: DB, + private options?: BuilderOptions + ) { + if (audio_element === undefined) + throw Error("audio_element was undefined") + // ↓ For old browsers + const AudioContext = window.AudioContext || window.webkitAudioContext + this.#audio_context = new AudioContext() + this.#track = + this.#audio_context.createMediaElementSource(audio_element) + this.#gain = this.#audio_context.createGain() + } + /** + * For external use, not kept inside player after connection. + * @returns {AnalyserNode} + */ + add_analyser() { + const analyser = this.#audio_context.createAnalyser() + !this.#prev_node + ? this.#track.connect(analyser) + : this.#prev_node.connect(analyser) + this.#prev_node = analyser + return analyser + } + /** + * For external use, not kept inside player after connection. + * @returns {StereoPannerNode} + */ + add_stereo_panner_node() { + const panner = this.#audio_context.createStereoPanner() + !this.#prev_node + ? this.#track.connect(panner) + : this.#prev_node.connect(panner) + this.#prev_node = panner + return panner + } + /** + * For external use, not kept inside player after connection. + * @returns {StereoPannerNode} + */ + add_wave_shaper_node() { + const shaper = this.#audio_context.createWaveShaper() + !this.#prev_node + ? this.#track.connect(shaper) + : this.#prev_node.connect(shaper) + this.#prev_node = shaper + return shaper + } + /** + * For additional trickery, you can connect your own node. + */ + connect_custom_node(node: AudioNode) { + !this.#prev_node + ? this.#track.connect(node) + : this.#prev_node.connect(node) + this.#prev_node = 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 + */ + connect_gain() { + !this.#prev_node + ? this.#track.connect(this.#gain) + : this.#prev_node.connect(this.#gain) + this.#prev_node = this.#gain + this.#is_gain_connected = true + } + /** + * Finishes the build + * @returns {Euterpe} + */ + build() { + if (!this.#is_gain_connected) { + !this.#prev_node + ? this.#track.connect(this.#gain) + : this.#prev_node.connect(this.#gain) + this.#prev_node = this.#gain + } + this.#prev_node.connect(this.#audio_context.destination) + return new Euterpe( + this.db, + this.#audio_context, + this.audio_element, + this.#track, + this.#gain, + this.#volume, + undefined, + this.options + ) + } } diff --git a/packages/music-library-web-test/src/db.ts b/packages/music-library-web-test/src/db.ts index 16cb9f7..9dcf1b7 100644 --- a/packages/music-library-web-test/src/db.ts +++ b/packages/music-library-web-test/src/db.ts @@ -1,50 +1,74 @@ -import { DB, Artist, Song, RefTo, Ref, Platforms } from "@euterpe.js/music-library"; -export const db = new DB +import { + DB, + Artist, + Song, + RefTo, + Ref, + 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: "Jamie xx", - }), - new Artist({ - name: "janz", - }), - 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")] - ] - })]) + //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: "Jamie xx" + }), + new Artist({ + name: "janz" + }), + 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://127.0.0.1:4200/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3") - }), - new Song({ - //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!)], - duration: 331, - name: "Sleep Sound", - url: new URL("http://127.0.0.1:4200/Jamie xx - Sleep Sound.mp3") - }), - new Song({ - artists: [new Ref(RefTo.Artists, 1)], - duration: 75, - name: "wish", - url: new URL("http://127.0.0.1:4200/janz - wish.mp3") - }) -]) \ No newline at end of file + 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://127.0.0.1:4200/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3" + ) + }), + new Song({ + //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! + ) + ], + duration: 331, + name: "Sleep Sound", + url: new URL("http://127.0.0.1:4200/Jamie xx - Sleep Sound.mp3") + }), + new Song({ + artists: [new Ref(RefTo.Artists, 1)], + duration: 75, + name: "wish", + url: new URL("http://127.0.0.1:4200/janz - wish.mp3") + }) +]) diff --git a/packages/music-library-web-test/src/main.ts b/packages/music-library-web-test/src/main.ts index b0bb067..576e493 100644 --- a/packages/music-library-web-test/src/main.ts +++ b/packages/music-library-web-test/src/main.ts @@ -1,115 +1,152 @@ -import { MusicPlayerBuilder } from "@euterpe.js/player"; -import { db } from "./db"; -import { Artist } from "@euterpe.js/music-library"; -import { DB, Platforms } from "@euterpe.js/music-library"; +import { MusicPlayerBuilder } from "@euterpe.js/player" +import { db } from "./db" +import { Artist } from "@euterpe.js/music-library" +import { DB, Platforms } from "@euterpe.js/music-library" const audio_el = document.querySelector("#audio") as HTMLAudioElement const music_player_builder = new MusicPlayerBuilder(audio_el) const music_player = music_player_builder.build() music_player.change_volume(1) -let curr_song_id = 1; +let curr_song_id = 1 const elem_curr_song = document.querySelector("#text-playing") -music_player.try_new_song_async(db.songs[curr_song_id].url.pathname) - .then(() => { - let is_seeking = false - change_current_song_text(db) +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", () => { - curr_song_id-- - if (curr_song_id < 0) curr_song_id = 2 - music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then((s) => { - change_current_song_text(db) - music_player.play_async().catch((err) => { console.log(err) }) - }, (e) => { console.log(e) }) - }) - document.querySelector("#next")?.addEventListener("click", () => { - curr_song_id++ - if (curr_song_id > 2) curr_song_id = 0 - music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then((s) => { - change_current_song_text(db) - music_player.play_async().catch((err) => { console.log(err) }) - }, (e) => { console.log(e) }) - }) - - document.querySelector("#play")?.addEventListener("click", () => { - music_player.play_async() - .then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e)) - }) - document.querySelector("#pause")?.addEventListener("click", () => { - music_player.pause() - }) - document.querySelector("#mute")?.addEventListener("click", () => { - music_player.mute() - }) - document.querySelector("#unmute")?.addEventListener("click", () => { - music_player.unmute() - }) - document.querySelector("#toggle-mute")?.addEventListener("click", () => { - music_player.mute_toggle() - }) - document.querySelector("#toggle-play")?.addEventListener("click", () => { - music_player.play_toggle_async().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e)) - }) - document.querySelector("#volume")?.addEventListener("input", (e) => { - music_player.change_volume(e.target?.valueAsNumber) - }) - document.querySelector("#seek")?.addEventListener("mousedown", (e) => { - is_seeking = true; - }) - document.querySelector("#seek")?.addEventListener("mouseup", (e) => { - music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => { - alert("Failed seeking! " + e) - }) - is_seeking = false - }) - // Subscriptions to AudioContext changes, eg. time.. - music_player.subscribe_to_formatted_duration_time((time) => { - document.querySelector("#duration").innerHTML = time - document.querySelector("#seek").max = "" + music_player.get_current_duration() - }) - music_player.subscribe_to_formatted_current_time_tick((time) => { - document.querySelector("#current").innerHTML = time - }) - music_player.subscribe_to_time_tick((time) => { - if (is_seeking) return - document.querySelector("#seek").value = "" + time - }) - - }, (e) => console.log(e)) + document.querySelector("#previous")?.addEventListener("click", () => { + curr_song_id-- + if (curr_song_id < 0) curr_song_id = 2 + music_player + .try_new_song_async(db.songs[curr_song_id].url.pathname) + .then( + (s) => { + change_current_song_text(db) + music_player.play_async().catch((err) => { + console.log(err) + }) + }, + (e) => { + console.log(e) + } + ) + }) + document.querySelector("#next")?.addEventListener("click", () => { + curr_song_id++ + if (curr_song_id > 2) curr_song_id = 0 + music_player + .try_new_song_async(db.songs[curr_song_id].url.pathname) + .then( + (s) => { + change_current_song_text(db) + music_player.play_async().catch((err) => { + console.log(err) + }) + }, + (e) => { + console.log(e) + } + ) + }) + document.querySelector("#play")?.addEventListener("click", () => { + music_player.play_async().then( + () => { + console.log("Playing!") + }, + (e) => alert("Failed to play, " + e) + ) + }) + document.querySelector("#pause")?.addEventListener("click", () => { + music_player.pause() + }) + document.querySelector("#mute")?.addEventListener("click", () => { + music_player.mute() + }) + document.querySelector("#unmute")?.addEventListener("click", () => { + music_player.unmute() + }) + document + .querySelector("#toggle-mute") + ?.addEventListener("click", () => { + music_player.mute_toggle() + }) + document + .querySelector("#toggle-play") + ?.addEventListener("click", () => { + music_player.play_toggle_async().then( + (s) => console.log("toggled play/pause"), + (e) => alert("failed to toggle pause/play!" + e) + ) + }) + document.querySelector("#volume")?.addEventListener("input", (e) => { + music_player.change_volume(e.target?.valueAsNumber) + }) + document.querySelector("#seek")?.addEventListener("mousedown", (e) => { + is_seeking = true + }) + document.querySelector("#seek")?.addEventListener("mouseup", (e) => { + music_player.try_seek_async(e.target?.valueAsNumber).then( + () => { + console.log("seeked to " + e.target?.valueAsNumber) + }, + () => { + alert("Failed seeking! " + e) + } + ) + is_seeking = false + }) + // Subscriptions to AudioContext changes, eg. time.. + music_player.subscribe_to_formatted_duration_time((time) => { + document.querySelector("#duration").innerHTML = time + document.querySelector("#seek").max = + "" + music_player.get_current_duration() + }) + music_player.subscribe_to_formatted_current_time_tick((time) => { + document.querySelector("#current").innerHTML = time + }) + music_player.subscribe_to_time_tick((time) => { + if (is_seeking) return + document.querySelector("#seek").value = "" + time + }) + }, + (e) => console.log(e) +) function change_current_song_text(db: DB) { - const curr_song = db.songs[curr_song_id] - let final_text = "" + const curr_song = db.songs[curr_song_id] + let final_text = "" - for (const artist of curr_song.artists) { - const curr_artist = artist.get(db) as Artist - final_text += curr_artist.name + ", " - } + for (const artist of curr_song.artists) { + const curr_artist = artist.get(db) as Artist + final_text += curr_artist.name + ", " + } - final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " - final_text += " - " + curr_song.name + final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " + final_text += " - " + curr_song.name - if (curr_song.remix_artists.length > 0) { - final_text += " (" + if (curr_song.remix_artists.length > 0) { + final_text += " (" - for (const artist of curr_song.remix_artists) { - const curr_artist = artist.get(db) as Artist - if (curr_artist.links && curr_artist.links.length > 0) { - //returns "found a link! Spotify" - console.log("found a link! " + Platforms[curr_artist.links[0][0]]) + for (const artist of curr_song.remix_artists) { + const curr_artist = artist.get(db) as Artist + if (curr_artist.links && curr_artist.links.length > 0) { + //returns "found a link! Spotify" + console.log( + "found a link! " + Platforms[curr_artist.links[0][0]] + ) - const url = curr_artist.links[0][1] - final_text += `${curr_artist.name}, ` - } else { - final_text += curr_artist.name + ", " - } - } + const url = curr_artist.links[0][1] + final_text += `${curr_artist.name}, ` + } else { + final_text += curr_artist.name + ", " + } + } - final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " - final_text += " Remix)" - } + final_text = final_text.slice(0, final_text.length - 2) // remove trailing ", " + final_text += " Remix)" + } - elem_curr_song!.innerHTML = final_text -} \ No newline at end of file + elem_curr_song!.innerHTML = final_text +} diff --git a/packages/music-library/src/index.ts b/packages/music-library/src/index.ts index 125791b..c0441bf 100644 --- a/packages/music-library/src/index.ts +++ b/packages/music-library/src/index.ts @@ -1,299 +1,360 @@ export { - RefTo, - Ref, - Song, - Collection, - DB, - Artist, - Platforms, - CollectionType, - from_json + RefTo, + Ref, + Song, + Collection, + DB, + Artist, + Platforms, + CollectionType, + from_json } type ID = number enum RefTo { - Artists, - Songs, - Collections + Artists, + Songs, + Collections } enum CollectionType { - Album = "Album", - EP = "EP", - Single = "Single", - Playlist = "Playlist", - Release = "Release", + Album = "Album", + EP = "EP", + Single = "Single", + Playlist = "Playlist", + Release = "Release" } enum Platforms { - Youtube = "Youtube", - Linktree = "Linktree", - Bandcamp = "Bandcamp", - Spotify = "Spotify", - Portfolio = "Portfolio", - BeatPort = "BeatPort", - SoundCloud = "SoundCloud", - Instagram = "Instagram", - Patreon = "Patreon", - Twitter = "Twitter", - Facebook = "Facebook", + Youtube = "Youtube", + Linktree = "Linktree", + Bandcamp = "Bandcamp", + Spotify = "Spotify", + Portfolio = "Portfolio", + BeatPort = "BeatPort", + SoundCloud = "SoundCloud", + Instagram = "Instagram", + Patreon = "Patreon", + Twitter = "Twitter", + Facebook = "Facebook" } class Ref { - constructor(public to: RefTo, public id: ID) { } - get(from: DB) { - switch (this.to) { - case RefTo.Artists: { - return from.artists.find((artist) => artist.id == this.id) - } - case RefTo.Songs: { - return from.songs.find((song) => song.id == this.id) - } - case RefTo.Collections: { - return from.collections.find((col) => col.id == this.id) - } - } - } + constructor(public to: RefTo, public id: ID) {} + get(from: DB) { + switch (this.to) { + case RefTo.Artists: { + return from.artists.find((artist) => artist.id == this.id) + } + case RefTo.Songs: { + return from.songs.find((song) => song.id == this.id) + } + case RefTo.Collections: { + return from.collections.find((col) => col.id == this.id) + } + } + } } function ref_from_json(ref: any): Ref { - return new Ref(ref.to, ref.id) + return new Ref(ref.to, ref.id) } interface SongConstructor { - name: string - artists?: Ref[] - url: URL - duration?: number - publish_date?: Date - remix_artists?: Ref[] - in_collection?: Ref - cover?: URL - bpm?: number - key?: string - fft_data?: number[] - id?: ID, - metadata?: any[] + name: string + artists?: Ref[] + url: URL + duration?: number + publish_date?: Date + remix_artists?: Ref[] + in_collection?: Ref + cover?: URL + bpm?: number + key?: string + fft_data?: number[] + id?: ID + metadata?: any[] } class Song { - name: string - artists: Ref[] - url: URL - duration?: number - remix_artists: Ref[] - publish_date?: Date - in_collection?: Ref - cover?: URL - bpm?: number - key?: string - fft_data?: number[] - metadata: any[] - /** - * The ID is always there, don't worry :) - */ - id?: ID - constructor(data: SongConstructor) { - this.name = data.name - this.artists = data.artists || [] - this.url = data.url - this.duration = data.duration - this.publish_date = data.publish_date - this.remix_artists = data.remix_artists || [] - this.in_collection = data.in_collection - this.cover = data.cover - this.bpm = data.bpm - this.key = data.key - this.fft_data = data.fft_data - this.id = data.id - this.metadata = data.metadata || [] - } + name: string + artists: Ref[] + url: URL + duration?: number + remix_artists: Ref[] + publish_date?: Date + in_collection?: Ref + cover?: URL + bpm?: number + key?: string + fft_data?: number[] + metadata: any[] + /** + * The ID is always there, don't worry :) + */ + id?: ID + constructor(data: SongConstructor) { + this.name = data.name + this.artists = data.artists || [] + this.url = data.url + this.duration = data.duration + this.publish_date = data.publish_date + this.remix_artists = data.remix_artists || [] + this.in_collection = data.in_collection + this.cover = data.cover + this.bpm = data.bpm + this.key = data.key + this.fft_data = data.fft_data + this.id = data.id + this.metadata = data.metadata || [] + } } interface ArtistConstructor { - name: string, - pfp?: URL - songs?: Ref[] - collections?: Ref[] - links?: [Platforms, URL][] - id?: ID - metadata?: any[] + name: string + pfp?: URL + songs?: Ref[] + collections?: Ref[] + links?: [Platforms, URL][] + id?: ID + metadata?: any[] } class Artist { - name = "" - pfp?: URL - songs: Ref[] - collections: Ref[] - links?: [Platforms, URL][] - metadata: any[] - /** - * The ID is always there, don't worry :) - */ - id?: ID - constructor(data: ArtistConstructor) { - this.name = data.name - this.pfp = data.pfp - this.songs = data.songs || [] - this.collections = data.collections || [] - this.links = data.links - this.id = data.id - this.metadata = data.metadata || [] - } + name = "" + pfp?: URL + songs: Ref[] + collections: Ref[] + links?: [Platforms, URL][] + metadata: any[] + /** + * The ID is always there, don't worry :) + */ + id?: ID + constructor(data: ArtistConstructor) { + this.name = data.name + this.pfp = data.pfp + this.songs = data.songs || [] + this.collections = data.collections || [] + this.links = data.links + this.id = data.id + this.metadata = data.metadata || [] + } } interface CollectionConstructor { - artists: Ref[] - songs: Ref[] - cover?: URL - duration?: number - publish_date?: Date - id?: ID - metadata?: any[] - name?: string - type?: CollectionType - + artists: Ref[] + songs: Ref[] + cover?: URL + duration?: number + publish_date?: Date + id?: ID + metadata?: any[] + name?: string + type?: CollectionType } class Collection { - name?: string - type?: CollectionType - artists: Ref[] - songs: Ref[] - cover?: URL - duration?: number - publish_date?: Date - metadata: any[] - /** - * The ID is always there, don't worry :) - */ - id?: ID - constructor(data: CollectionConstructor) { - this.artists = data.artists - this.songs = data.songs - this.cover = data.cover - this.duration = data.duration - this.publish_date = data.publish_date - this.id = data.id - this.name = data.name - this.metadata = data.metadata ? data.metadata : [] - } + name?: string + type?: CollectionType + artists: Ref[] + songs: Ref[] + cover?: URL + duration?: number + publish_date?: Date + metadata: any[] + /** + * The ID is always there, don't worry :) + */ + id?: ID + constructor(data: CollectionConstructor) { + this.artists = data.artists + this.songs = data.songs + this.cover = data.cover + this.duration = data.duration + this.publish_date = data.publish_date + this.id = data.id + this.name = data.name + this.metadata = data.metadata ? data.metadata : [] + } } class DB { - artists: Artist[] = [] - songs: Song[] = [] - collections: Collection[] = [] + artists: Artist[] = [] + songs: Song[] = [] + collections: Collection[] = [] - add(song: Song[]): void - add(artist: Artist[]): void - add(collection: Collection[]): void - add(mix: (Song | Artist | Collection)[]): void - add(stuff: Artist[] | Collection[] | Song[] | (Song | Artist | Collection)[]) { - /** 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 - * and adds incremental ids - */ - let inputs - if (typeof stuff[Symbol.iterator] != "function") { - inputs = [stuff] - } else { - inputs = stuff - } - for (const input of inputs) { - if (input instanceof Artist) { - const artist = input as Artist - if (!artist.id) artist.id = this.artists.length + add(song: Song[]): void + add(artist: Artist[]): void + add(collection: Collection[]): void + add(mix: (Song | Artist | Collection)[]): void + add( + stuff: Artist[] | Collection[] | Song[] | (Song | Artist | Collection)[] + ) { + /** 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 + * and adds incremental ids + */ + let inputs + if (typeof stuff[Symbol.iterator] != "function") { + inputs = [stuff] + } else { + inputs = stuff + } + for (const input of inputs) { + if (input instanceof Artist) { + const artist = input as Artist + if (!artist.id) artist.id = this.artists.length - for (const song_ref of artist.songs) { - const curr_song = song_ref.get(this) as Song - curr_song?.artists.push(new Ref(RefTo.Artists, artist.id)) - } + for (const song_ref of artist.songs) { + const curr_song = song_ref.get(this) as Song + curr_song?.artists.push(new Ref(RefTo.Artists, artist.id)) + } - for (const col_ref of artist.collections) { - const curr_col = col_ref.get(this) as Collection - curr_col?.artists.push(new Ref(RefTo.Artists, artist.id)) - } - this.artists.push(artist) - } + for (const col_ref of artist.collections) { + const curr_col = col_ref.get(this) as Collection + curr_col?.artists.push(new Ref(RefTo.Artists, artist.id)) + } + this.artists.push(artist) + } else if (input instanceof Collection) { + const col = input as Collection + if (!col.id) col.id = this.collections.length - else if (input instanceof Collection) { - const col = input as Collection - if (!col.id) col.id = this.collections.length + for (const song_ref of col.songs) { + const curr_song = song_ref.get(this) as Song + curr_song.in_collection = new Ref(RefTo.Collections, col.id) + } + for (const artist_ref of col.artists) { + const curr_artist = artist_ref.get(this) as Artist + curr_artist.collections.push( + new Ref(RefTo.Collections, col.id) + ) + } + this.collections.push(col) + } else if (input instanceof Song) { + const song = input as Song + if (!song.id) song.id = this.songs.length - for (const song_ref of col.songs) { - const curr_song = song_ref.get(this) as Song - curr_song.in_collection = new Ref(RefTo.Collections, col.id) - } - for (const artist_ref of col.artists) { - const curr_artist = artist_ref.get(this) as Artist - curr_artist.collections.push(new Ref(RefTo.Collections, col.id)) - } - this.collections.push(col) - } + 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!) + ) + ) + } - else if (input instanceof Song) { - const song = input as Song - 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.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!) + } } -function from_json(db_stringified: { artists?: any, songs?: any, collections?: any }): DB { - const db = new DB - if (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.collections) artist.collections = artist.collections.map((e: any) => ref_from_json(e)) - if (artist.links) artist.links = artist.links.map((e: any) => { try { [e[0] as Platforms, new URL(e[1])] } catch (e) { console.log(e) } }) - if (artist.publish_date) artist.publish_date = new Date(JSON.parse(artist.publish_date)) - if (artist.id) artist.id = artist.id as ID - try { if (artist.pfp) artist.pfp = new URL(artist.pfp) } - catch (e) { console.error(e), console.error("failed to parse artist URL") } - db.artists.push(artist) - } - } - if (db_stringified.songs) { - for (const song of db_stringified.songs) { - try { if (song.url) song.url = new URL(song.url) } catch (e) { console.error("failed to parse song.url" + e) } - if (song.artists) song.artists = song.artists.map((e: any) => ref_from_json(e)) - if (song.remix_artists) song.remix_artists = song.remix_artists.map((e: any) => ref_from_json(e)) - if (song.in_collection) song.in_collection = ref_from_json(song.in_collection) - try { if (song.cover) song.cover = new URL(song.cover) } - catch (e) { console.error(e), console.error("failed to parse artist URL") } - try { if (song.publish_date) song.publish_date = new Date(JSON.parse(song.publish_date)) } - catch (e) { console.error(e), console.error("Failed to song cover url") } - if (song.id) song.id = song.id as ID - db.songs.push(song) - } - } - if (db_stringified.collections) { - for (const collection of db_stringified.collections) { - if (collection.artists) collection.artists = collection.artists.map((e: any) => ref_from_json(e)) - if (collection.songs) collection.songs = collection.songs.map((e: any) => ref_from_json(e)) - if (collection.type) collection.type = collection.type.map((e: any) => e as CollectionType) - try { if (collection.publish_date) collection.publish_date = new Date(JSON.parse(collection.publish_date)) } - catch (e) { console.error(e), console.error("Failed to parse date") } - try { if (collection.cover) collection.cover = new URL(collection.cover) } - catch (e) { console.error(e), console.error("failed to parse collection cover url") } - if (collection.id) collection.id = collection.id as ID - db.collections.push(collection) - } - } - return db +function from_json(db_stringified: { + artists?: any + songs?: any + collections?: any +}): DB { + const db = new DB() + if (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.collections) + artist.collections = artist.collections.map((e: any) => + ref_from_json(e) + ) + if (artist.links) + artist.links = artist.links.map((e: any) => { + try { + ;[e[0] as Platforms, new URL(e[1])] + } catch (e) { + console.log(e) + } + }) + if (artist.publish_date) + artist.publish_date = new Date(JSON.parse(artist.publish_date)) + if (artist.id) artist.id = artist.id as ID + try { + if (artist.pfp) artist.pfp = new URL(artist.pfp) + } catch (e) { + console.error(e), console.error("failed to parse artist URL") + } + db.artists.push(artist) + } + } + if (db_stringified.songs) { + for (const song of db_stringified.songs) { + try { + if (song.url) song.url = new URL(song.url) + } catch (e) { + console.error("failed to parse song.url" + e) + } + if (song.artists) + song.artists = song.artists.map((e: any) => ref_from_json(e)) + if (song.remix_artists) + song.remix_artists = song.remix_artists.map((e: any) => + ref_from_json(e) + ) + if (song.in_collection) + song.in_collection = ref_from_json(song.in_collection) + try { + if (song.cover) song.cover = new URL(song.cover) + } catch (e) { + console.error(e), console.error("failed to parse artist URL") + } + try { + if (song.publish_date) + song.publish_date = new Date(JSON.parse(song.publish_date)) + } catch (e) { + console.error(e), console.error("Failed to song cover url") + } + if (song.id) song.id = song.id as ID + db.songs.push(song) + } + } + if (db_stringified.collections) { + for (const collection of db_stringified.collections) { + if (collection.artists) + collection.artists = collection.artists.map((e: any) => + ref_from_json(e) + ) + if (collection.songs) + collection.songs = collection.songs.map((e: any) => + ref_from_json(e) + ) + if (collection.type) + collection.type = collection.type.map( + (e: any) => e as CollectionType + ) + try { + if (collection.publish_date) + collection.publish_date = new Date( + JSON.parse(collection.publish_date) + ) + } catch (e) { + console.error(e), console.error("Failed to parse date") + } + try { + if (collection.cover) + collection.cover = new URL(collection.cover) + } catch (e) { + console.error(e), + console.error("failed to parse collection cover url") + } + if (collection.id) collection.id = collection.id as ID + db.collections.push(collection) + } + } + return db } // const db = new DB // db.add( @@ -312,4 +373,4 @@ function from_json(db_stringified: { artists?: any, songs?: any, collections?: a // console.dir(db, { depth: null }) // const res = db.artists[0].songs[0].get(db) as Song -// console.log(`${db.artists[0].name} has song ${db.songs[0].name}? : ${res.name} is there!`) \ No newline at end of file +// console.log(`${db.artists[0].name} has song ${db.songs[0].name}? : ${res.name} is there!`) diff --git a/packages/player-web-test/src/main.ts b/packages/player-web-test/src/main.ts index 7d00c1f..5a6ad56 100644 --- a/packages/player-web-test/src/main.ts +++ b/packages/player-web-test/src/main.ts @@ -1,58 +1,85 @@ -import { MusicPlayerBuilder } from "@euterpe.js/player"; +import { MusicPlayerBuilder } from "@euterpe.js/player" const audio_el = document.querySelector("#audio") as HTMLAudioElement const music_player_builder = new MusicPlayerBuilder(audio_el) const music_player = music_player_builder.build() music_player.change_volume(1) -music_player.try_new_song(encodeURI("http://" + window.location.host + "/nuphory - NVISION (EXTENDED MIX).ogg")) - .then(() => { - let is_seeking = false - document.querySelector("#play")?.addEventListener("click", () => { - //const analyser_node = music_player_builder.add_analyser() - music_player.try_play() - .then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e)) - }) - document.querySelector("#pause")?.addEventListener("click", () => { - music_player.pause() - }) - document.querySelector("#mute")?.addEventListener("click", () => { - music_player.mute() - }) - document.querySelector("#unmute")?.addEventListener("click", () => { - music_player.unmute() - }) - document.querySelector("#toggle-mute")?.addEventListener("click", () => { - music_player.mute_toggle() - }) - document.querySelector("#toggle-play")?.addEventListener("click", () => { - music_player.try_play_toggle().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e)) - }) - document.querySelector("#volume")?.addEventListener("input", (e) => { - music_player.change_volume(e.target?.valueAsNumber) - }) - document.querySelector("#seek")?.addEventListener("mousedown", (e) => { - is_seeking = true; - }) - document.querySelector("#seek")?.addEventListener("mouseup", (e) => { - try { - music_player.try_seek(e.target?.valueAsNumber) - console.log("seeked to " + e.target?.valueAsNumber) - } catch (e) { - alert("Failed seeking! " + e) - } - is_seeking = false - }) - // Subscriptions to AudioContext changes, eg. time.. - music_player.on_duration_formatted((time) => { - document.querySelector("#duration")!.innerHTML = time - document.querySelector("#seek")!.max = "" + music_player.current_song_duration - }) - music_player.on_time_tick_formatted((time) => { - document.querySelector("#current")!.innerHTML = time - }) - music_player.on_time_tick((time) => { - if (is_seeking) return - document.querySelector("#seek")!.value = "" + time - }) - - }, (e) => console.log(e)) \ No newline at end of file +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) + ) diff --git a/packages/player/src/index.ts b/packages/player/src/index.ts index b983972..cba671f 100644 --- a/packages/player/src/index.ts +++ b/packages/player/src/index.ts @@ -1,436 +1,489 @@ export enum SubscribeEvents { - CurrentTimeTick, - FormattedDurationTick, - FormattedCurrentTimeTick, + CurrentTimeTick, + FormattedDurationTick, + FormattedCurrentTimeTick } class PubSub { - //el = event listener - el_current_time_tick: Array<(data: any) => void> = [] - el_formatted_duration_tick: Array<(data: any) => void> = [] - el_formatted_current_time_tick: Array<(data: any) => void> = [] + //el = event listener + el_current_time_tick: Array<(data: any) => void> = [] + el_formatted_duration_tick: Array<(data: any) => void> = [] + el_formatted_current_time_tick: Array<(data: any) => void> = [] - subscribe(event_name: SubscribeEvents, func: (data: any) => void) { - switch (event_name) { - case SubscribeEvents.CurrentTimeTick: { - this.el_current_time_tick.push(func) - break - } - case SubscribeEvents.FormattedDurationTick: { - this.el_formatted_duration_tick.push(func) - break - } - case SubscribeEvents.FormattedCurrentTimeTick: { - this.el_formatted_current_time_tick.push(func) - break - } - } - } - unsubscribe(event_name: SubscribeEvents, func: (data: any) => void) { - switch (event_name) { - case SubscribeEvents.CurrentTimeTick: { - if (this.el_current_time_tick.includes(func)) { - this.el_current_time_tick.splice(this.el_current_time_tick.indexOf(func), 1) - } - break - } - case SubscribeEvents.FormattedDurationTick: { - if (this.el_formatted_duration_tick.includes(func)) { - this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1) - } - break - } - case SubscribeEvents.FormattedCurrentTimeTick: { - if (this.el_formatted_duration_tick.includes(func)) { - this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1) - } - break - } - } - } - emit(event_name: SubscribeEvents, data: any) { - switch (event_name) { - case SubscribeEvents.CurrentTimeTick: { - this.el_current_time_tick.forEach((func) => { - func(data) - }) - break - } - case SubscribeEvents.FormattedDurationTick: { - this.el_formatted_duration_tick.forEach((func) => { - func(data) - }) - break - } - case SubscribeEvents.FormattedCurrentTimeTick: { - this.el_formatted_current_time_tick.forEach((func) => { - func(data) - }) - break - } - } - } + subscribe(event_name: SubscribeEvents, func: (data: any) => void) { + switch (event_name) { + case SubscribeEvents.CurrentTimeTick: { + this.el_current_time_tick.push(func) + break + } + case SubscribeEvents.FormattedDurationTick: { + this.el_formatted_duration_tick.push(func) + break + } + case SubscribeEvents.FormattedCurrentTimeTick: { + this.el_formatted_current_time_tick.push(func) + break + } + } + } + unsubscribe(event_name: SubscribeEvents, func: (data: any) => void) { + switch (event_name) { + case SubscribeEvents.CurrentTimeTick: { + if (this.el_current_time_tick.includes(func)) { + this.el_current_time_tick.splice( + this.el_current_time_tick.indexOf(func), + 1 + ) + } + break + } + case SubscribeEvents.FormattedDurationTick: { + if (this.el_formatted_duration_tick.includes(func)) { + this.el_formatted_duration_tick.splice( + this.el_formatted_duration_tick.indexOf(func), + 1 + ) + } + break + } + case SubscribeEvents.FormattedCurrentTimeTick: { + if (this.el_formatted_duration_tick.includes(func)) { + this.el_formatted_duration_tick.splice( + this.el_formatted_duration_tick.indexOf(func), + 1 + ) + } + break + } + } + } + emit(event_name: SubscribeEvents, data: any) { + switch (event_name) { + case SubscribeEvents.CurrentTimeTick: { + this.el_current_time_tick.forEach((func) => { + func(data) + }) + break + } + case SubscribeEvents.FormattedDurationTick: { + this.el_formatted_duration_tick.forEach((func) => { + func(data) + }) + break + } + case SubscribeEvents.FormattedCurrentTimeTick: { + this.el_formatted_current_time_tick.forEach((func) => { + func(data) + }) + break + } + } + } } /* For old browsers */ declare global { - interface Window { - webkitAudioContext: typeof AudioContext - } + interface Window { + webkitAudioContext: typeof AudioContext + } } - export class MusicPlayer { - current_song_duration = 0 - #volume_cache: number - is_playing = false - time = 0 - #pub_sub = new PubSub - constructor( - public audio_context: AudioContext, - private audio_element: HTMLAudioElement, - public track: MediaElementAudioSourceNode, - public gain: GainNode, - public volume: number, - private current_song_path?: string) { - this.#volume_cache = volume - } + current_song_duration = 0 + #volume_cache: number + is_playing = false + time = 0 + #pub_sub = new PubSub() + constructor( + public audio_context: AudioContext, + private audio_element: HTMLAudioElement, + public track: MediaElementAudioSourceNode, + public gain: GainNode, + public volume: number, + private current_song_path?: string + ) { + this.#volume_cache = volume + } - mute_toggle() { - if (this.gain.gain.value == 0) { - this.unmute() - } else { - this.mute() - } - } - mute() { - this.#volume_cache = this.gain.gain.value - /* Gentler mute, doesn't pop + mute_toggle() { + if (this.gain.gain.value == 0) { + this.unmute() + } else { + this.mute() + } + } + mute() { + this.#volume_cache = this.gain.gain.value + /* Gentler mute, doesn't pop gain.gain.linearRampToValueAtTime( 0, audio_context.currentTime + 0.1 );*/ - this.volume = this.gain.gain.value = 0 - } - unmute() { - this.volume = this.gain.gain.value = this.#volume_cache - } - change_volume(volume_i: number) { - this.volume = this.gain.gain.value = volume_i - } - /** - * Safer seek_async. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed. - * Will also resume playback if player is paused (by finishing the song etc) - * @throws if "Can't seek - Audiocontext is not running" - */ - async try_seek(new_time: number) { - if (this.audio_context.state !== "running") { - this.is_playing = false - throw new Error("Can't seek - audioContext not running, audio_context.state : " + this.audio_context.state) - } - if (this.audio_element.paused) await this.try_play() - this.audio_element.currentTime = new_time - } + this.volume = this.gain.gain.value = 0 + } + unmute() { + this.volume = this.gain.gain.value = this.#volume_cache + } + change_volume(volume_i: number) { + this.volume = this.gain.gain.value = volume_i + } + /** + * Safer seek_async. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed. + * Will also resume playback if player is paused (by finishing the song etc) + * @throws if "Can't seek - Audiocontext is not running" + */ + async try_seek(new_time: number) { + if (this.audio_context.state !== "running") { + this.is_playing = false + throw new Error( + "Can't seek - audioContext not running, audio_context.state : " + + this.audio_context.state + ) + } + if (this.audio_element.paused) await this.try_play() + this.audio_element.currentTime = new_time + } - /** - * Unsafe, throws error if failed. Use try_seek_async or seek_async unless you don't care about the result. - */ - seek(new_time: number) { - this.audio_element.currentTime = new_time - } + /** + * Unsafe, throws error if failed. Use try_seek_async or seek_async unless you don't care about the result. + */ + seek(new_time: number) { + this.audio_element.currentTime = new_time + } - /** - * Safer play_toggle_async. Normal play_toggle will try to start the player even if the track hasn't started yet, or was previously suspended/closed - * @throws Error if playback failed - */ - async try_play_toggle() { - if (this.audio_context.state !== "running") { - await this.audio_context.resume() - } - if (this.audio_element.paused) { - try { - await this.audio_element.play() - this.is_playing = true - } catch (e) { - this.is_playing = false - throw e - } - } else { - this.audio_element.pause() - this.is_playing = false - } - } + /** + * 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 + */ + async try_play_toggle() { + if (this.audio_context.state !== "running") { + await this.audio_context.resume() + } + if (this.audio_element.paused) { + try { + await this.audio_element.play() + this.is_playing = true + } catch (e) { + this.is_playing = false + throw e + } + } else { + this.audio_element.pause() + this.is_playing = false + } + } - /** - * Unsafe, can just fail. Use try_play_toggle unless you don't care about the result. - */ - play_toggle() { - if (this.audio_element.paused) { - this.is_playing = true - this.audio_element.play().catch((r) => { - this.is_playing = false - throw r - }) - } else { - this.is_playing = false - this.audio_element.pause() - } - } + /** + * Unsafe, can just fail. Use try_play_toggle unless you don't care about the result. + */ + play_toggle() { + if (this.audio_element.paused) { + this.is_playing = true + this.audio_element.play().catch((r) => { + this.is_playing = false + throw r + }) + } else { + this.is_playing = false + 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 - * @throws Error if playback failed - */ - async try_play() { - if (this.is_playing) return - if (this.audio_context.state !== "running") { - await this.audio_context.resume() - } - if (this.audio_element.paused) { - try { - await this.audio_element.play() - this.is_playing = true - } catch (e) { - this.is_playing = false - throw e - } - } - } + /** + * 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 + */ + async try_play() { + if (this.is_playing) return + if (this.audio_context.state !== "running") { + await this.audio_context.resume() + } + if (this.audio_element.paused) { + try { + await this.audio_element.play() + this.is_playing = true + } catch (e) { + this.is_playing = false + throw e + } + } + } - /** - * Unsafe, can just fail. Use play_async or try_play_async unless you don't care about the result. - */ - play() { - if (this.is_playing) return - this.audio_element.play().catch(() => { - this.is_playing = false - }) - } + /** + * Unsafe, can just fail. Use play_async or try_play_async unless you don't care about the result. + */ + play() { + if (this.is_playing) return + this.audio_element.play().catch(() => { + this.is_playing = false + }) + } - /** - * Safe technically. Even if audioContext is suspended or closed it will pretend that it paused. - */ - pause() { - this.audio_element.pause() - this.is_playing = false - } + /** + * Safe technically. Even if audioContext is suspended or closed it will pretend that it paused. + */ + pause() { + this.audio_element.pause() + this.is_playing = false + } - /** - * 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 - */ - async try_new_song(path: string) { - if (this.audio_context.state !== "running") { - try { - await this.audio_context.resume() - } catch (e) { - console.log("loading new song - couldn't resume context before hand", e) - } - } - return new Promise((resolve, reject) => { - this.audio_element.src = this.current_song_path = path - //Found out today about this. Such a nice new way to mass remove event listeners! - const controller = new AbortController(); + /** + * 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 + */ + async try_new_song(path: string) { + if (this.audio_context.state !== "running") { + try { + await this.audio_context.resume() + } catch (e) { + console.log( + "loading new song - couldn't resume context before hand", + e + ) + } + } + return new Promise((resolve, reject) => { + this.audio_element.src = this.current_song_path = path + //Found out today about this. Such a nice new way to mass remove event listeners! + const controller = new AbortController() - this.audio_element.addEventListener("canplaythrough", function canplay_listener() { - controller.abort() - }, { signal: controller.signal }) + this.audio_element.addEventListener( + "canplaythrough", + function canplay_listener() { + controller.abort() + }, + { signal: controller.signal } + ) - this.audio_element.addEventListener("error", function error_listener() { - controller.abort("new src error") - }, { signal: controller.signal }) + this.audio_element.addEventListener( + "error", + function error_listener() { + controller.abort("new src error") + }, + { signal: controller.signal } + ) - this.audio_element.addEventListener("stalled", function stalled_listener() { - controller.abort("new src stalled") - }, { signal: controller.signal }) + this.audio_element.addEventListener( + "stalled", + function stalled_listener() { + controller.abort("new src stalled") + }, + { signal: controller.signal } + ) - //once aborted, try to set current_song_duration - controller.signal.addEventListener("abort", (r) => { - this.current_song_duration = this.audio_element.duration - if (typeof controller.signal.reason == "string") reject(new Error(controller.signal.reason)) - resolve() - }) - this.is_playing = false - }) - } - /** - * Won't tell if you if the song actually got loaded or if it failed. For a safer version use try_new_song_async() unless you don't care about the result - */ - 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 + //once aborted, try to set current_song_duration + controller.signal.addEventListener("abort", (r) => { + this.current_song_duration = this.audio_element.duration + if (typeof controller.signal.reason == "string") + reject(new Error(controller.signal.reason)) + resolve() + }) + this.is_playing = false + }) + } + /** + * Won't tell if you if the song actually got loaded or if it failed. For a safer version use try_new_song_async() unless you don't care about the result + */ + 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" + 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() + const hrs = ~~(dur / 3600) + const mins = ~~((dur % 3600) / 60) + const secs = ~~dur % 60 - let ret = "" - if (hrs > 0) { - ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); - } + let ret = "" + if (hrs > 0) { + ret += "" + hrs + ":" + (mins < 10 ? "0" : "") + } - ret += "" + mins + ":" + (secs < 10 ? "0" : ""); - ret += "" + secs; - 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" : "") + ret += "" + secs + 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 - if (curr == 0 || !curr) return "0:00" - // ~~ is Bitwise OR, equivalent to Math.floor() - const hrs = ~~(curr / 3600); - const mins = ~~((curr % 3600) / 60); - const secs = ~~curr % 60; + if (curr == 0 || !curr) return "0:00" + // ~~ is Bitwise OR, equivalent to Math.floor() + const hrs = ~~(curr / 3600) + const mins = ~~((curr % 3600) / 60) + const secs = ~~curr % 60 - let ret = "" - if (hrs > 0) { - ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); - } + let ret = "" + if (hrs > 0) { + ret += "" + hrs + ":" + (mins < 10 ? "0" : "") + } - ret += "" + mins + ":" + (secs < 10 ? "0" : ""); - ret += "" + secs; - 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" : "") + ret += "" + secs + 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 - this.time = this.audio_element.currentTime - if (this.#pub_sub.el_current_time_tick.length == 0) cancelAnimationFrame(request_id) - this.#pub_sub.emit(SubscribeEvents.CurrentTimeTick, this.time) - } - #emit_duration_fmt() { - const request_id = requestAnimationFrame(this.#emit_duration_fmt.bind(this)) - 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) - } - #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) + this.#pub_sub.emit(SubscribeEvents.CurrentTimeTick, this.time) + } + #emit_duration_fmt() { + const request_id = requestAnimationFrame( + this.#emit_duration_fmt.bind(this) + ) + 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) + } + #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() + } - /** - * 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() - } + /** + * 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 { - #audio_context: AudioContext - #gain: GainNode - #track: MediaElementAudioSourceNode - #volume = 1 - #prev_node: any; - #is_gain_connected = false - /** - * Creates a context and #gain( Gets connected at the end ) - * 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) - */ - constructor(private audio_element: HTMLAudioElement) { - if (audio_element === undefined) throw Error("audio_element was undefined") - // ↓ For old browsers - const AudioContext = window.AudioContext || window.webkitAudioContext; - this.#audio_context = new AudioContext() - this.#track = this.#audio_context.createMediaElementSource(audio_element) - this.#gain = this.#audio_context.createGain() - } - /** - * For external use, not kept inside player after connection. - * @returns {AnalyserNode} - */ - add_analyser() { - const analyser = this.#audio_context.createAnalyser() - !this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser) - this.#prev_node = analyser - return analyser - } - /** - * For external use, not kept inside player after connection. - * @returns {StereoPannerNode} - */ - add_stereo_panner_node() { - const panner = this.#audio_context.createStereoPanner() - !this.#prev_node ? this.#track.connect(panner) : this.#prev_node.connect(panner) - this.#prev_node = panner - return panner - } - /** - * For external use, not kept inside player after connection. - * @returns {StereoPannerNode} - */ - add_wave_shaper_node() { - const shaper = this.#audio_context.createWaveShaper() - !this.#prev_node ? this.#track.connect(shaper) : this.#prev_node.connect(shaper) - this.#prev_node = shaper - return shaper - } - /** - * For additional trickery, you can connect your own node. - */ - connect_custom_node(node: AudioNode) { - !this.#prev_node ? this.#track.connect(node) : this.#prev_node.connect(node) - this.#prev_node = 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 - */ - connect_gain() { - !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) - this.#prev_node = this.#gain - this.#is_gain_connected = true - } - /** - * Finishes the build - * @returns {Euterpe} - */ - build() { - if (!this.#is_gain_connected) { - !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) - this.#prev_node = this.#gain - } - this.#prev_node.connect(this.#audio_context.destination) - return new MusicPlayer(this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume) - } -} \ No newline at end of file + #audio_context: AudioContext + #gain: GainNode + #track: MediaElementAudioSourceNode + #volume = 1 + #prev_node: any + #is_gain_connected = false + /** + * Creates a context and #gain( Gets connected at the end ) + * 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) + */ + constructor(private audio_element: HTMLAudioElement) { + if (audio_element === undefined) + throw Error("audio_element was undefined") + // ↓ For old browsers + const AudioContext = window.AudioContext || window.webkitAudioContext + this.#audio_context = new AudioContext() + this.#track = + this.#audio_context.createMediaElementSource(audio_element) + this.#gain = this.#audio_context.createGain() + } + /** + * For external use, not kept inside player after connection. + * @returns {AnalyserNode} + */ + add_analyser() { + const analyser = this.#audio_context.createAnalyser() + !this.#prev_node + ? this.#track.connect(analyser) + : this.#prev_node.connect(analyser) + this.#prev_node = analyser + return analyser + } + /** + * For external use, not kept inside player after connection. + * @returns {StereoPannerNode} + */ + add_stereo_panner_node() { + const panner = this.#audio_context.createStereoPanner() + !this.#prev_node + ? this.#track.connect(panner) + : this.#prev_node.connect(panner) + this.#prev_node = panner + return panner + } + /** + * For external use, not kept inside player after connection. + * @returns {StereoPannerNode} + */ + add_wave_shaper_node() { + const shaper = this.#audio_context.createWaveShaper() + !this.#prev_node + ? this.#track.connect(shaper) + : this.#prev_node.connect(shaper) + this.#prev_node = shaper + return shaper + } + /** + * For additional trickery, you can connect your own node. + */ + connect_custom_node(node: AudioNode) { + !this.#prev_node + ? this.#track.connect(node) + : this.#prev_node.connect(node) + this.#prev_node = 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 + */ + connect_gain() { + !this.#prev_node + ? this.#track.connect(this.#gain) + : this.#prev_node.connect(this.#gain) + this.#prev_node = this.#gain + this.#is_gain_connected = true + } + /** + * Finishes the build + * @returns {Euterpe} + */ + build() { + if (!this.#is_gain_connected) { + !this.#prev_node + ? this.#track.connect(this.#gain) + : this.#prev_node.connect(this.#gain) + this.#prev_node = this.#gain + } + this.#prev_node.connect(this.#audio_context.destination) + return new MusicPlayer( + this.#audio_context, + this.audio_element, + this.#track, + this.#gain, + this.#volume + ) + } +} diff --git a/packages/preprocessor/src/crawler.mjs b/packages/preprocessor/src/crawler.mjs index 6e84a80..6fc09f6 100644 --- a/packages/preprocessor/src/crawler.mjs +++ b/packages/preprocessor/src/crawler.mjs @@ -1,4 +1,15 @@ 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 }) \ No newline at end of file +const songs = filehound + .create() + .path("../public/samples") + .ext(["ogg"]) + .findSync() +fs.writeFile( + "songs_list.ts", + `export const songs = ` + JSON.stringify(songs), + "utf8", + () => { + 1 + 1 + } +) diff --git a/packages/preprocessor/src/generate_db.ts b/packages/preprocessor/src/generate_db.ts index 3a880aa..6552e7a 100644 --- a/packages/preprocessor/src/generate_db.ts +++ b/packages/preprocessor/src/generate_db.ts @@ -1,10 +1,10 @@ -import { Collection, Ref, RefTo, Song, DB, } from "@euterpe.js/music-library" +import { Collection, Ref, RefTo, Song, DB } from "@euterpe.js/music-library" import { songs } from "./songs_list" export function generate_db() { console.log(songs) // construct db - let db = new DB + let db = new DB() let collections: string[] = new Array() let new_songs = [] const path_char = songs[0].includes("\\") ? "\\" : "/" @@ -13,22 +13,27 @@ export function generate_db() { 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 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: [], - })]) + 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 = db.collections.find((col) => col.name == collection_name)! let col_id = col.id new_songs.push({ song: song, collection_id: col_id! }) } @@ -39,11 +44,15 @@ export function generate_db() { const last_i = song.song.lastIndexOf(path_char) const name = song.song.slice(last_i + 1) - const song_url = song.song.slice(song.song.indexOf(`public${path_char}`) + 7) + const song_url = song.song.slice( + song.song.indexOf(`public${path_char}`) + 7 + ) const db_song = new Song({ name: name.slice(0, name.lastIndexOf(".")), artists: [], - url: new URL(`${window.location.href}${song_url}`.replaceAll("\\", "/")), + url: new URL( + `${window.location.href}${song_url}`.replaceAll("\\", "/") + ), duration: 0, remix_artists: [], in_collection: new Ref(RefTo.Collections, song.collection_id) diff --git a/packages/preprocessor/src/main.ts b/packages/preprocessor/src/main.ts index 359bbac..d26ecfe 100644 --- a/packages/preprocessor/src/main.ts +++ b/packages/preprocessor/src/main.ts @@ -1,8 +1,14 @@ 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" +import { + AudioVisualBuilder, + SmoothingAlgorythm, + ShapeType, + WaveformOrientation, + WaveformShape +} from "@euterpe.js/visualizer" -let result: AnalyzeReturn | undefined; +let result: AnalyzeReturn | undefined let db = generate_db() //Create all audio nodes @@ -17,126 +23,163 @@ audioContextAnalyser.smoothingTimeConstant = 0 const analyserBufferLength = audioContextAnalyser.frequencyBinCount const FFTDataArray = new Float32Array(analyserBufferLength) //Connect all audio Nodes -track.connect(audioContextAnalyser).connect(gain).connect(audioContext.destination) - +track + .connect(audioContextAnalyser) + .connect(gain) + .connect(audioContext.destination) document.getElementById("analyze")!.addEventListener("click", async (ev) => { - audioContext.resume() - result = await analyze() - download(JSON.stringify(result.db), "db.json", "text/plain") + audioContext.resume() + result = await analyze() + download(JSON.stringify(result.db), "db.json", "text/plain") }) document.getElementById("create-svg")!.addEventListener("click", (ev) => { - audioContext.resume() - svg() + 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 } - } - + 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 + 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 + 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: .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((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") + 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((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 { - console.clear() - const audioEl = document.querySelector("#audio") as HTMLAudioElement - console.log("analysing...") - const samplingRate = 100 + 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((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 + // 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((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(); + 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 + analyzer_node: AnalyserNode + db: DB } function awaitLoad(audioEl: HTMLAudioElement) { - return new Promise((resolve, reject) => { - audioEl.addEventListener("loadeddata", function () { - if (audioEl.readyState >= 4) { - resolve() - } - }) - }) + return new Promise((resolve, reject) => { + audioEl.addEventListener("loadeddata", function () { + if (audioEl.readyState >= 4) { + resolve() + } + }) + }) } diff --git a/packages/preprocessor/src/media_process.js b/packages/preprocessor/src/media_process.js index 9d8c122..b890b54 100644 --- a/packages/preprocessor/src/media_process.js +++ b/packages/preprocessor/src/media_process.js @@ -5,7 +5,7 @@ */ import filehound from "filehound" -import { execSync, exec } from 'child_process' +import { execSync, exec } from "child_process" import { fstat, unlinkSync } from "fs" function generate_new_photo_sizes(file, currentExtention) { @@ -22,8 +22,12 @@ function generate_new_photo_sizes(file, currentExtention) { 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"`) + 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("\\")) @@ -35,7 +39,8 @@ function generate_new_sounds_ogg(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'"` + 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) @@ -54,7 +59,11 @@ function generate_new_sounds_mp3(file, currentExtention) { exec(command) // console.log(command) } -function generate_new_video_sizes_mp4(file, currentExtention, width_resolutions) { +function generate_new_video_sizes_mp4( + file, + currentExtention, + width_resolutions +) { const path = file.substring(0, file.lastIndexOf("\\")) file = file.substring(file.lastIndexOf("\\") + 1) @@ -72,12 +81,16 @@ function generate_new_video_sizes_mp4(file, currentExtention, width_resolutions) res_command += `cd "${path}" && ` res_command += `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\"" + res_command += '&& exit"' exec(res_command) } }) } -function generate_new_video_sizes_webm(file, currentExtention, width_resolutions) { +function generate_new_video_sizes_webm( + file, + currentExtention, + width_resolutions +) { const path = file.substring(0, file.lastIndexOf("\\")) file = file.substring(file.lastIndexOf("\\") + 1) @@ -95,36 +108,42 @@ function generate_new_video_sizes_webm(file, currentExtention, width_resolutions res_command += `cd "${path}" && ` res_command += `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\"" + res_command += '&& exit"' exec(res_command) } }) } -let dirs = filehound.create() - .path("../public") - .directory() - .findSync() +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() + 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) { } + 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)] + 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]}`) + generate_new_sounds_ogg( + `${current_media[0]}`, + `${current_media[1]}` + ) + generate_new_sounds_mp3( + `${current_media[0]}`, + `${current_media[1]}` + ) } /* @@ -147,6 +166,3 @@ for (let i = 0; i < dirs.length; i++) { } } } - - - diff --git a/packages/preprocessor/src/songs_list.ts b/packages/preprocessor/src/songs_list.ts index 6471b09..0df95f8 100644 --- a/packages/preprocessor/src/songs_list.ts +++ b/packages/preprocessor/src/songs_list.ts @@ -1 +1,33 @@ -export const songs = ["../public/samples/FX/01 HTS Boom Kicks.ogg","../public/samples/FX/02 HTS Verbclaps.ogg","../public/samples/FX/03 HTS Noisesweep.ogg","../public/samples/FX/04 HTS Combined FX.ogg","../public/samples/bass/01 HTS Arp Bass.ogg","../public/samples/bass/02 HTS Hard Bass.ogg","../public/samples/bass/03 HTS Break Bass.ogg","../public/samples/bass/04 HTS Sub Bass.ogg","../public/samples/demos/01 demo 1 - Violet Delta & Eyhz.ogg","../public/samples/demos/02 demo 2 - Walras.ogg","../public/samples/demos/03 demo 3 - G4TE-16.ogg","../public/samples/demos/04 demo 4 - shadeux.ogg","../public/samples/demos/05 demo 5 - crowit.ogg","../public/samples/demos/06 demo 6 - sh0wtime.ogg","../public/samples/demos/07 demo 7 - nuphory & Luna Lenta.ogg","../public/samples/drums/01 HTS Rides and Hats.ogg","../public/samples/drums/02 HTS Claps and Hats.ogg","../public/samples/drums/03 HTS Club Snares.ogg","../public/samples/drums/04 HTS Buildup Snares.ogg","../public/samples/kicks/01 HTS Trancekick.ogg","../public/samples/kicks/02 HTS Sizzle Layer.ogg","../public/samples/kicks/03 HTS Transients.ogg","../public/samples/kicks/04 HTS Kick Combined.ogg","../public/samples/loops/01 HTS Loop CH.ogg","../public/samples/loops/02 HTS Loop OH.ogg","../public/samples/loops/03 HTS Perc Loop.ogg","../public/samples/loops/04 HTS Full Loop.ogg","../public/samples/synths/01 HTS Leads and Bass 01.ogg","../public/samples/synths/02 HTS Leads and Pads 01.ogg","../public/samples/synths/03 HTS Leads and Bass 02.ogg","../public/samples/synths/04 HTS Leads and Pads 02.ogg"] \ No newline at end of file +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" +] diff --git a/packages/visualizer-web-test/src/main.ts b/packages/visualizer-web-test/src/main.ts index 076297a..0368e21 100644 --- a/packages/visualizer-web-test/src/main.ts +++ b/packages/visualizer-web-test/src/main.ts @@ -1,43 +1,82 @@ -import { MusicPlayerBuilder } from "@euterpe.js/player"; -import { fft_data } from "./waveform_data"; -import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType, WaveformOrientation, WaveformShape } from "@euterpe.js/visualizer" +import { MusicPlayerBuilder } from "@euterpe.js/player" +import { fft_data } from "./waveform_data" +import { + AudioVisualBuilder, + SmoothingAlgorythm, + ShapeType, + WaveformOrientation, + WaveformShape +} from "@euterpe.js/visualizer" const audio_el = document.querySelector("#audio") as HTMLAudioElement const music_player_builder = new MusicPlayerBuilder(audio_el) const trapnation_analyser_node = music_player_builder.add_analyser() const bar_analyser_node = music_player_builder.add_analyser() const music_player = music_player_builder.build() -music_player.change_volume(.5) +music_player.change_volume(0.5) -const waveform_canvas = document.querySelector("#waveform-canvas") as SVGSVGElement +const waveform_canvas = document.querySelector( + "#waveform-canvas" +) as SVGSVGElement const seek_element = document.querySelector("#seek") as HTMLInputElement const duration_element = document.querySelector("#duration") as HTMLElement const current_time_element = document.querySelector("#current") as HTMLElement /** * Create the Audio Visualizer */ -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. - .set_fft_size(8192) - //Tells the Visualiser how to parse data which mutates our initial shape - .set_fft_data_tresholds({ to_fft_range_i: 3, point_count_i: 40, fft_multiplier_i: 1.5, fft_offset_i: 150 }) - .set_fft_time_smoothing(0.6) - //If not using typescript enums, CatmullRom = number 2 - .set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom) -const trapnation_visual = trapnation_visual_builder.build(ShapeType.Circle, false) +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. + .set_fft_size(8192) + //Tells the Visualiser how to parse data which mutates our initial shape + .set_fft_data_tresholds({ + to_fft_range_i: 3, + point_count_i: 40, + fft_multiplier_i: 1.5, + fft_offset_i: 150 + }) + .set_fft_time_smoothing(0.6) + //If not using typescript enums, CatmullRom = number 2 + .set_smoothing_algorythm(SmoothingAlgorythm.CatmullRom) +const trapnation_visual = trapnation_visual_builder.build( + ShapeType.Circle, + false +) -const bar_visual_builder = new AudioVisualBuilder(bar_analyser_node, document.querySelector("#bar-canvas") as SVGSVGElement) - .set_fft_data_tresholds({ point_count_i: 50, fft_multiplier_i: 2, fft_offset_i: -100 }) - .set_fft_time_smoothing(0.8) - .set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular) +const bar_visual_builder = new AudioVisualBuilder( + bar_analyser_node, + document.querySelector("#bar-canvas") as SVGSVGElement +) + .set_fft_data_tresholds({ + point_count_i: 50, + fft_multiplier_i: 2, + fft_offset_i: -100 + }) + .set_fft_time_smoothing(0.8) + .set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular) const bar_visual = bar_visual_builder.build(ShapeType.Line, false) -const waveform_visual_builder = new AudioVisualBuilder(bar_analyser_node, waveform_canvas) - .set_fft_data_tresholds({ point_count_i: 100, fft_multiplier_i: 1, fft_offset_i: -80 }) - .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 }) - - +const waveform_visual_builder = new AudioVisualBuilder( + bar_analyser_node, + waveform_canvas +) + .set_fft_data_tresholds({ + point_count_i: 100, + fft_multiplier_i: 1, + fft_offset_i: -80 + }) + .set_fft_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() bar_visual.draw() @@ -84,70 +123,106 @@ seek_clip_path.appendChild(seek_clip_rect) time_clip_path.appendChild(time_clip_rect) */ function convert_range(value: number, r1: number[], r2: number[]) { - return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0] + return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0] } waveform_canvas.addEventListener("mousemove", (e) => { - const rect = e.target.getBoundingClientRect() - const x = e.clientX - rect.left - 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%)` - document.documentElement.style.setProperty("--clip-seek-path", polygon) + const rect = e.target.getBoundingClientRect() + const x = e.clientX - rect.left + 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%)` + document.documentElement.style.setProperty("--clip-seek-path", polygon) }) waveform_canvas.addEventListener("mouseleave", (e) => { - const polygon = `polygon(0 0, 0 0, 0 100%, 0 100%)` - document.documentElement.style.setProperty("--clip-seek-path", polygon) + const polygon = `polygon(0 0, 0 0, 0 100%, 0 100%)` + document.documentElement.style.setProperty("--clip-seek-path", polygon) }) /* * The player part */ -music_player.try_new_song_async(encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg")) - .then(() => { - let is_seeking = false - document.querySelector("#play")?.addEventListener("click", () => { - music_player.play_async() - .then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e)) - }) - document.querySelector("#pause")?.addEventListener("click", () => { - music_player.pause() - }) - document.querySelector("#mute")?.addEventListener("click", () => { - music_player.mute() - }) - document.querySelector("#unmute")?.addEventListener("click", () => { - music_player.unmute() - }) - document.querySelector("#toggle-mute")?.addEventListener("click", () => { - music_player.mute_toggle() - }) - document.querySelector("#toggle-play")?.addEventListener("click", () => { - music_player.play_toggle_async().then((s) => console.log("toggled play/pause"), (e) => alert("failed to toggle pause/play!" + e)) - }) - document.querySelector("#volume")?.addEventListener("input", (e) => { - music_player.change_volume(e.target?.valueAsNumber) - }) - document.querySelector("#seek")?.addEventListener("mousedown", (e) => { - is_seeking = true; - }) - document.querySelector("#seek")?.addEventListener("mouseup", (e) => { - music_player.try_seek_async(e.target?.valueAsNumber).then(() => { console.log("seeked to " + e.target?.valueAsNumber) }, () => { - alert("Failed seeking! " + e) - }) - is_seeking = false - }) - // Subscriptions to AudioContext changes, eg. time.. - music_player.on_duration_formatted((time) => { - duration_element.innerHTML = time - seek_element.max = "" + music_player.current_song_duration - }) - music_player.on_time_tick_formatted((time) => { - current_time_element.innerHTML = time - }) - music_player.on_time_tick((time) => { - if (is_seeking) return - seek_element.value = "" + time - const x = `${time / music_player.current_song_duration * 100}%` - const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)` - document.documentElement.style.setProperty("--clip-time-path", polygon) - }) - - }, (e) => console.log(e)) \ No newline at end of file +music_player + .try_new_song_async( + encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg") + ) + .then( + () => { + let is_seeking = false + document.querySelector("#play")?.addEventListener("click", () => { + music_player.play_async().then( + () => { + console.log("Playing!") + }, + (e) => alert("Failed to play, " + e) + ) + }) + document.querySelector("#pause")?.addEventListener("click", () => { + music_player.pause() + }) + document.querySelector("#mute")?.addEventListener("click", () => { + music_player.mute() + }) + document.querySelector("#unmute")?.addEventListener("click", () => { + music_player.unmute() + }) + document + .querySelector("#toggle-mute") + ?.addEventListener("click", () => { + music_player.mute_toggle() + }) + document + .querySelector("#toggle-play") + ?.addEventListener("click", () => { + music_player.play_toggle_async().then( + (s) => console.log("toggled play/pause"), + (e) => alert("failed to toggle pause/play!" + e) + ) + }) + document + .querySelector("#volume") + ?.addEventListener("input", (e) => { + music_player.change_volume(e.target?.valueAsNumber) + }) + document + .querySelector("#seek") + ?.addEventListener("mousedown", (e) => { + is_seeking = true + }) + document + .querySelector("#seek") + ?.addEventListener("mouseup", (e) => { + music_player.try_seek_async(e.target?.valueAsNumber).then( + () => { + console.log("seeked to " + e.target?.valueAsNumber) + }, + () => { + alert("Failed seeking! " + e) + } + ) + is_seeking = false + }) + // Subscriptions to AudioContext changes, eg. time.. + music_player.on_duration_formatted((time) => { + duration_element.innerHTML = time + seek_element.max = "" + music_player.current_song_duration + }) + music_player.on_time_tick_formatted((time) => { + current_time_element.innerHTML = time + }) + music_player.on_time_tick((time) => { + if (is_seeking) return + seek_element.value = "" + time + const x = `${ + (time / music_player.current_song_duration) * 100 + }%` + const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)` + document.documentElement.style.setProperty( + "--clip-time-path", + polygon + ) + }) + }, + (e) => console.log(e) + ) diff --git a/packages/visualizer-web-test/src/waveform_data.ts b/packages/visualizer-web-test/src/waveform_data.ts index 7f7bb2f..6c62797 100644 --- a/packages/visualizer-web-test/src/waveform_data.ts +++ b/packages/visualizer-web-test/src/waveform_data.ts @@ -1,14 +1,17 @@ export const fft_data = { - "fft_data": [ - -106.24, -99.73, -100.98, -101.34, -107.01, -92.38, -84.85, -90.28, -93.68, -95.02, -97.16, - -96.32, -99.23, -103.13, -85.57, -98.17, -103.27, -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.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, -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, -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 - ], -} \ No newline at end of file + 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, -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.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, -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, -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 + ] +} diff --git a/packages/visualizer/src/index.ts b/packages/visualizer/src/index.ts index f0f834f..6e16ab3 100644 --- a/packages/visualizer/src/index.ts +++ b/packages/visualizer/src/index.ts @@ -1,475 +1,590 @@ export enum SmoothingAlgorythm { - Linear, - BezierPerpendicular, - CatmullRom, - BezierWeighted, + Linear, + BezierPerpendicular, + CatmullRom, + BezierWeighted } export enum ShapeType { - Circle, - Line, - Waveform, - /*To be Implmeneted + Circle, + Line, + Waveform + /*To be Implmeneted Custom, FullSongWaveForm */ } export enum WaveformOrientation { - Vertical, - Horizontal, + Vertical, + Horizontal } export enum WaveformShape { - LineLike, - Striped, + LineLike, + Striped } export type WaveformOptions = { - fft_data: Float32Array, - shape_type: WaveformShape, - orientation: WaveformOrientation + fft_data: Float32Array + shape_type: WaveformShape + orientation: WaveformOrientation } type Point = { - x: number, - y: number, + x: number + y: number } type Shape = { - shape_type: ShapeType, - //Algo-rythm, because this is about music. Get it? xd - smoothing_algorythm: SmoothingAlgorythm - points: Point[] - waveform_options?: WaveformOptions - symmetry?: boolean + shape_type: ShapeType + //Algo-rythm, because this is about music. Get it? xd + smoothing_algorythm: SmoothingAlgorythm + points: Point[] + waveform_options?: WaveformOptions + symmetry?: boolean } export class AudioVisual { - #analyzer_node - #svg_injecting_element - #shape - #buffer_length - #fft_multiplier - #fft_offset - #from_fft_range - #to_fft_range - #point_count - #canvas_height - #canvas_width - #fft_data - #subscriber_fns = new Array<(data: Float32Array) => void>() - constructor( - analyzer_node: AnalyserNode, - svg_injecting_element: SVGSVGElement, - shape: Shape, - buffer_length: number, - fft_multiplier: number, - fft_offset: number, - from_fft_range: number, - to_fft_range: number, - point_count: number, - ) { - this.#analyzer_node = analyzer_node - this.#svg_injecting_element = svg_injecting_element - this.#shape = shape - this.#buffer_length = buffer_length - this.#fft_multiplier = fft_multiplier - this.#fft_offset = fft_offset - this.#from_fft_range = from_fft_range - this.#to_fft_range = to_fft_range - this.#point_count = point_count + #analyzer_node + #svg_injecting_element + #shape + #buffer_length + #fft_multiplier + #fft_offset + #from_fft_range + #to_fft_range + #point_count + #canvas_height + #canvas_width + #fft_data + #subscriber_fns = new Array<(data: Float32Array) => void>() + constructor( + analyzer_node: AnalyserNode, + svg_injecting_element: SVGSVGElement, + shape: Shape, + buffer_length: number, + fft_multiplier: number, + fft_offset: number, + from_fft_range: number, + to_fft_range: number, + point_count: number + ) { + this.#analyzer_node = analyzer_node + this.#svg_injecting_element = svg_injecting_element + this.#shape = shape + this.#buffer_length = buffer_length + this.#fft_multiplier = fft_multiplier + this.#fft_offset = fft_offset + this.#from_fft_range = from_fft_range + this.#to_fft_range = to_fft_range + this.#point_count = point_count - this.#fft_data = new Float32Array(this.#analyzer_node.frequencyBinCount) - this.#canvas_width = svg_injecting_element.viewBox.baseVal.width - this.#canvas_height = svg_injecting_element.viewBox.baseVal.height - } + this.#fft_data = new Float32Array(this.#analyzer_node.frequencyBinCount) + this.#canvas_width = svg_injecting_element.viewBox.baseVal.width + this.#canvas_height = svg_injecting_element.viewBox.baseVal.height + } - #get_cured_frequency_data(): Array { - let buffer_length_cache - if (this.#shape.shape_type == ShapeType.Waveform) { - this.#fft_data = this.#shape.waveform_options!.fft_data - buffer_length_cache = this.#buffer_length - this.#buffer_length = this.#fft_data.length - } else { - this.#analyzer_node.getFloatFrequencyData(this.#fft_data) - } - const from = Math.round((this.#point_count / 100) * this.#from_fft_range) - const to = Math.round(this.#buffer_length - (this.#buffer_length / 100) * this.#to_fft_range) - const squeeze_factor = Math.round((this.#buffer_length - to) / this.#point_count) + #get_cured_frequency_data(): Array { + let buffer_length_cache + if (this.#shape.shape_type == ShapeType.Waveform) { + this.#fft_data = this.#shape.waveform_options!.fft_data + buffer_length_cache = this.#buffer_length + this.#buffer_length = this.#fft_data.length + } else { + this.#analyzer_node.getFloatFrequencyData(this.#fft_data) + } + const from = Math.round( + (this.#point_count / 100) * this.#from_fft_range + ) + const to = Math.round( + this.#buffer_length - + (this.#buffer_length / 100) * this.#to_fft_range + ) + const squeeze_factor = Math.round( + (this.#buffer_length - to) / this.#point_count + ) - const return_array = new Array(this.#point_count) - for (let i = 0; i < this.#point_count + 1; i++) { - return_array[i] = this.#fft_data[from + i * squeeze_factor] - } - return return_array - } + const return_array = new Array(this.#point_count) + for (let i = 0; i < this.#point_count + 1; i++) { + return_array[i] = this.#fft_data[from + i * squeeze_factor] + } + return return_array + } - #normalise_perpendicular_anchors(x: number, y: number) { - const magnitude = Math.sqrt(x * x + y * y) - return [x / magnitude, y / magnitude] - } + #normalise_perpendicular_anchors(x: number, y: number) { + const magnitude = Math.sqrt(x * x + y * y) + return [x / magnitude, y / magnitude] + } - #create_perpendicular_anchors(arr: { x: number, y: number }[]) { - const anchors = [] - switch (this.#shape.shape_type) { - case ShapeType.Circle: { - const pointDistance = 7 - for (let curPoint = 0; curPoint < arr.length; curPoint++) { - const [dx, dy] = this.#normalise_perpendicular_anchors(arr[curPoint].x, arr[curPoint].y) - const perpendicular = [-dy, dx] - anchors.push({ - leftAnchor: { - x: arr[curPoint].x + pointDistance * perpendicular[0], - y: arr[curPoint].y + pointDistance * perpendicular[1], - }, - rightAnchor: { - x: arr[curPoint].x - pointDistance * perpendicular[0], - y: arr[curPoint].y - pointDistance * perpendicular[1], - }, - }) - } - break - } - case ShapeType.Line: { - const pointDistance = this.#canvas_width / arr.length - for (let curPoint = 0; curPoint < arr.length; curPoint++) { - anchors.push({ - leftAnchor: { - x: pointDistance * curPoint - pointDistance / 3, - y: arr[curPoint].y, - }, - rightAnchor: { - x: pointDistance * curPoint + pointDistance / 3, - y: arr[curPoint].y, - }, - }) - } - } - } + #create_perpendicular_anchors(arr: { x: number; y: number }[]) { + const anchors = [] + switch (this.#shape.shape_type) { + case ShapeType.Circle: { + const pointDistance = 7 + for (let curPoint = 0; curPoint < arr.length; curPoint++) { + const [dx, dy] = this.#normalise_perpendicular_anchors( + arr[curPoint].x, + arr[curPoint].y + ) + const perpendicular = [-dy, dx] + anchors.push({ + leftAnchor: { + x: + arr[curPoint].x + + pointDistance * perpendicular[0], + y: + arr[curPoint].y + + pointDistance * perpendicular[1] + }, + rightAnchor: { + x: + arr[curPoint].x - + pointDistance * perpendicular[0], + y: + arr[curPoint].y - + pointDistance * perpendicular[1] + } + }) + } + break + } + case ShapeType.Line: { + const pointDistance = this.#canvas_width / arr.length + for (let curPoint = 0; curPoint < arr.length; curPoint++) { + anchors.push({ + leftAnchor: { + x: pointDistance * curPoint - pointDistance / 3, + y: arr[curPoint].y + }, + rightAnchor: { + x: pointDistance * curPoint + pointDistance / 3, + y: arr[curPoint].y + } + }) + } + } + } - return anchors - } + return anchors + } - #catmull_rom_smooth(arr: { x: number, y: number }[], k: number) { - if (k == null) k = 1 - const last = arr.length - 2 - let path = "" + #catmull_rom_smooth(arr: { x: number; y: number }[], k: number) { + if (k == null) k = 1 + const last = arr.length - 2 + let path = "" - for (let i = 0; i < arr.length - 1; i++) { + for (let i = 0; i < arr.length - 1; i++) { + const x0 = i ? arr[i - 1].x : arr[0].x + const y0 = i ? arr[i - 1].y : arr[0].y - const x0 = i ? arr[i - 1].x : arr[0].x - const y0 = i ? arr[i - 1].y : arr[0].y + const x1 = arr[i].x + const y1 = arr[i].y - const x1 = arr[i].x - const y1 = arr[i].y + const x2 = arr[i + 1].x + const y2 = arr[i + 1].y - const x2 = arr[i + 1].x - const y2 = arr[i + 1].y + let subx = y2 + let suby = y2 + //Makes the last line before Z a bit less jarring + if (this.#shape.shape_type == ShapeType.Circle) { + subx = arr[0].x + suby = arr[0].y + } + const x3 = i !== last ? arr[i + 2].x : subx + const y3 = i !== last ? arr[i + 2].y : suby - let subx = y2 - let suby = y2 - //Makes the last line before Z a bit less jarring - if (this.#shape.shape_type == ShapeType.Circle) { - subx = arr[0].x - suby = arr[0].y - } - const x3 = i !== last ? arr[i + 2].x : subx - const y3 = i !== last ? arr[i + 2].y : suby + const cp1x = x1 + ((x2 - x0) / 6) * k + const cp1y = y1 + ((y2 - y0) / 6) * k - const cp1x = x1 + (x2 - x0) / 6 * k - const cp1y = y1 + (y2 - y0) / 6 * k + const cp2x = x2 - ((x3 - x1) / 6) * k + const cp2y = y2 - ((y3 - y1) / 6) * k - const cp2x = x2 - (x3 - x1) / 6 * k - const cp2y = y2 - (y3 - y1) / 6 * k + path += + "C" + + [ + cp1x.toFixed(2), + cp1y.toFixed(2), + cp2x.toFixed(2), + cp2y.toFixed(2), + x2.toFixed(2), + y2.toFixed(2) + ] + } + return path + } - path += "C" + [cp1x.toFixed(2), cp1y.toFixed(2), cp2x.toFixed(2), cp2y.toFixed(2), x2.toFixed(2), y2.toFixed(2)] - } - return path - } - - #mutate_points() { - const mutated_points = [] - const frequency_data = this.#get_cured_frequency_data() - const out_range = [0, this.#canvas_height] - const in_range = [-165, -30] - switch (this.#shape.shape_type) { - case ShapeType.Line: { - for (let i = 0; i < frequency_data.length - 1; i++) { - const mutator = isFinite(frequency_data[i]) ? this.#convert_range(frequency_data[i] * this.#fft_multiplier + this.#fft_offset, in_range, out_range) : -1 * this.#canvas_height - mutated_points.push({ - x: this.#shape.points[i].x /** ((Math.max(FFTDataArray[i] + 100)) * 4)*/, - y: this.#shape.points[i].y - mutator, - }) - } - break - } - case ShapeType.Circle: { - for (let i = 0; i < frequency_data.length - 1; i++) { - const new_i = i > (frequency_data.length - 1) / 2 ? frequency_data.length - 1 - i : i - mutated_points.push({ - x: this.#shape.points[i].x * Math.max((frequency_data[new_i] * this.#fft_multiplier + this.#fft_offset) / 50, 1) + this.#canvas_width / 2, - y: this.#shape.points[i].y * Math.max((frequency_data[new_i] * this.#fft_multiplier + this.#fft_offset) / 50, 1) + this.#canvas_height / 2, - }) - /* TODO: IMPLEMENT SCALING TO BEAT + #mutate_points() { + const mutated_points = [] + const frequency_data = this.#get_cured_frequency_data() + const out_range = [0, this.#canvas_height] + const in_range = [-165, -30] + switch (this.#shape.shape_type) { + case ShapeType.Line: { + for (let i = 0; i < frequency_data.length - 1; i++) { + const mutator = isFinite(frequency_data[i]) + ? this.#convert_range( + frequency_data[i] * this.#fft_multiplier + + this.#fft_offset, + in_range, + out_range + ) + : -1 * this.#canvas_height + mutated_points.push({ + x: this.#shape.points[i] + .x /** ((Math.max(FFTDataArray[i] + 100)) * 4)*/, + y: this.#shape.points[i].y - mutator + }) + } + break + } + case ShapeType.Circle: { + for (let i = 0; i < frequency_data.length - 1; i++) { + const new_i = + i > (frequency_data.length - 1) / 2 + ? frequency_data.length - 1 - i + : i + mutated_points.push({ + x: + this.#shape.points[i].x * + Math.max( + (frequency_data[new_i] * + this.#fft_multiplier + + this.#fft_offset) / + 50, + 1 + ) + + this.#canvas_width / 2, + y: + this.#shape.points[i].y * + Math.max( + (frequency_data[new_i] * + this.#fft_multiplier + + this.#fft_offset) / + 50, + 1 + ) + + this.#canvas_height / 2 + }) + /* TODO: IMPLEMENT SCALING TO BEAT this.injectingHTMLElement.parentElement.style.transform = `scale(${(100 + Math.max((frequencyData[2] * 2 + 130) / 5, 1)) / 100})` */ - } + } - break - } - case ShapeType.Waveform: { - if (this.#shape.waveform_options!.shape_type == WaveformShape.LineLike) { - if (this.#shape.symmetry) { - for (let i = 0; i < this.#shape.points.length; i += 2) { - let mutator = this.#convert_range(frequency_data[i / 2] * this.#fft_multiplier + this.#fft_offset, in_range, out_range) - if (mutator <= 0) mutator = 2 - if (this.#shape.waveform_options!.orientation == WaveformOrientation.Horizontal) { - mutated_points.push({ - x: this.#shape.points[i].x, - y: this.#shape.points[i].y - mutator - }) - mutated_points.push({ - x: this.#shape.points[i + 1].x, - y: this.#shape.points[i + 1].y + mutator - }) - } else { - mutated_points.push({ - x: this.#shape.points[i].x + mutator, - y: this.#shape.points[i].y - }) - mutated_points.push({ - x: this.#shape.points[i + 1].x - mutator, - y: this.#shape.points[i + 1].y - }) - } - } - } else { - for (let i = 0; i < frequency_data.length - 1; i++) { - const mutator = this.#convert_range(frequency_data[i] * this.#fft_multiplier + this.#fft_offset, in_range, out_range) - if (this.#shape.waveform_options!.orientation == WaveformOrientation.Horizontal) { - mutated_points.push({ - x: this.#shape.points[i].x, - y: this.#shape.points[i].y - mutator - }) - } else { - mutated_points.push({ - x: this.#shape.points[i].x - mutator, - y: this.#shape.points[i].y - }) - } - } - } - } - break - } - } - return mutated_points - } + break + } + case ShapeType.Waveform: { + if ( + this.#shape.waveform_options!.shape_type == + WaveformShape.LineLike + ) { + if (this.#shape.symmetry) { + for (let i = 0; i < this.#shape.points.length; i += 2) { + let mutator = this.#convert_range( + frequency_data[i / 2] * this.#fft_multiplier + + this.#fft_offset, + in_range, + out_range + ) + if (mutator <= 0) mutator = 2 + if ( + this.#shape.waveform_options!.orientation == + WaveformOrientation.Horizontal + ) { + mutated_points.push({ + x: this.#shape.points[i].x, + y: this.#shape.points[i].y - mutator + }) + mutated_points.push({ + x: this.#shape.points[i + 1].x, + y: this.#shape.points[i + 1].y + mutator + }) + } else { + mutated_points.push({ + x: this.#shape.points[i].x + mutator, + y: this.#shape.points[i].y + }) + mutated_points.push({ + x: this.#shape.points[i + 1].x - mutator, + y: this.#shape.points[i + 1].y + }) + } + } + } else { + for (let i = 0; i < frequency_data.length - 1; i++) { + const mutator = this.#convert_range( + frequency_data[i] * this.#fft_multiplier + + this.#fft_offset, + in_range, + out_range + ) + if ( + this.#shape.waveform_options!.orientation == + WaveformOrientation.Horizontal + ) { + mutated_points.push({ + x: this.#shape.points[i].x, + y: this.#shape.points[i].y - mutator + }) + } else { + mutated_points.push({ + x: this.#shape.points[i].x - mutator, + y: this.#shape.points[i].y + }) + } + } + } + } + break + } + } + return mutated_points + } - #convert_range(value: number, r1: number[], r2: number[]) { - if (!isFinite(value)) return 0 - if (value < r1[0]) return 0 - return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0] - } + #convert_range(value: number, r1: number[], r2: number[]) { + if (!isFinite(value)) return 0 + if (value < r1[0]) return 0 + return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0] + } - #create_svg_element() { - let path - const arr = this.#mutate_points() - switch (this.#shape.shape_type) { - case ShapeType.Line: { - path = `M ${0} ${this.#canvas_height} ` - break - } - case ShapeType.Circle: { - path = `M ${arr[0].x} ${arr[0].y} ` - break - } - case ShapeType.Waveform: { - path = `M ${0} ${this.#canvas_height / 2}` - break - } - } - switch (this.#shape.smoothing_algorythm) { - case SmoothingAlgorythm.Linear: { - switch (this.#shape.shape_type) { - case ShapeType.Line: { - for (let i = 0; i < arr.length; i++) { - path += `L ${arr[i].x.toFixed(2)},${arr[i].y.toFixed(2)} ` - } - if (this.#shape.shape_type == ShapeType.Line) { - path += `L ${this.#canvas_width} ${this.#canvas_height} ` - //path += `L ${canvas_width} ${canvas_height} ` - } - break - } - case ShapeType.Circle: { - for (let i = 0; i < arr.length; i++) { - path += `L ${arr[i].x.toFixed(2)},${arr[i].y.toFixed(2)} ` - } - break - } - case ShapeType.Waveform: { - for (let i = 0; i < arr.length; i += 2) { - path += `L ${arr[i].x.toFixed(2)},${arr[i].y.toFixed(2)} ` - } - for (let i = arr.length - 1; i >= 0; i -= 2) { - path += `L ${arr[i].x.toFixed(2)},${arr[i].y.toFixed(2)} ` - } - } - } - path += `Z ` - break - } + #create_svg_element() { + let path + const arr = this.#mutate_points() + switch (this.#shape.shape_type) { + case ShapeType.Line: { + path = `M ${0} ${this.#canvas_height} ` + break + } + case ShapeType.Circle: { + path = `M ${arr[0].x} ${arr[0].y} ` + break + } + case ShapeType.Waveform: { + path = `M ${0} ${this.#canvas_height / 2}` + break + } + } + switch (this.#shape.smoothing_algorythm) { + case SmoothingAlgorythm.Linear: { + switch (this.#shape.shape_type) { + case ShapeType.Line: { + for (let i = 0; i < arr.length; i++) { + path += `L ${arr[i].x.toFixed(2)},${arr[ + i + ].y.toFixed(2)} ` + } + if (this.#shape.shape_type == ShapeType.Line) { + path += `L ${this.#canvas_width} ${ + this.#canvas_height + } ` + //path += `L ${canvas_width} ${canvas_height} ` + } + break + } + case ShapeType.Circle: { + for (let i = 0; i < arr.length; i++) { + path += `L ${arr[i].x.toFixed(2)},${arr[ + i + ].y.toFixed(2)} ` + } + break + } + case ShapeType.Waveform: { + for (let i = 0; i < arr.length; i += 2) { + path += `L ${arr[i].x.toFixed(2)},${arr[ + i + ].y.toFixed(2)} ` + } + for (let i = arr.length - 1; i >= 0; i -= 2) { + path += `L ${arr[i].x.toFixed(2)},${arr[ + i + ].y.toFixed(2)} ` + } + } + } + path += `Z ` + break + } - case SmoothingAlgorythm.BezierPerpendicular: { - const anchors = this.#create_perpendicular_anchors(arr) + case SmoothingAlgorythm.BezierPerpendicular: { + const anchors = this.#create_perpendicular_anchors(arr) - for (let i = 1; i < arr.length; i++) { - path += `C ${anchors[i - 1].rightAnchor.x.toFixed(2)} ${anchors[i - 1].rightAnchor.y.toFixed(2)} ${anchors[i].leftAnchor.x.toFixed(2)} ${anchors[i].leftAnchor.y.toFixed(2)} ${arr[i].x.toFixed(2)} ${arr[i].y.toFixed(2)} ` - } - if (this.#shape.shape_type == ShapeType.Line) { - //path += `L ${this.canvasWidth} ${this.canvasHeight / 2} ` - path += `L ${this.#canvas_width} ${this.#canvas_height} ` - } - path += `Z ` - break - } + for (let i = 1; i < arr.length; i++) { + path += `C ${anchors[i - 1].rightAnchor.x.toFixed( + 2 + )} ${anchors[i - 1].rightAnchor.y.toFixed(2)} ${anchors[ + i + ].leftAnchor.x.toFixed(2)} ${anchors[ + i + ].leftAnchor.y.toFixed(2)} ${arr[i].x.toFixed(2)} ${arr[ + i + ].y.toFixed(2)} ` + } + if (this.#shape.shape_type == ShapeType.Line) { + //path += `L ${this.canvasWidth} ${this.canvasHeight / 2} ` + path += `L ${this.#canvas_width} ${this.#canvas_height} ` + } + path += `Z ` + break + } - case SmoothingAlgorythm.BezierWeighted: { - /*THIS IS VERY MUCH BROKEN ATM :( + case SmoothingAlgorythm.BezierWeighted: { + /*THIS IS VERY MUCH BROKEN ATM :( for (let i = 2; i < arr.length; i++) { const end = [arr.x[i], arr.y[i]] // the current point is the end of this segment of the curve path += `C ${startControl[0]} ${startControl[1]} ${endControl[0]} ${endControl[1]} ${end[0]} ${end[1]}` }*/ - console.error("BezierWeighted not implemented yet...") - break - } - case SmoothingAlgorythm.CatmullRom: { - if (this.#shape.shape_type == ShapeType.Waveform && this.#shape.symmetry == true) { - //adding points so both halfs ends and start at the same center point - console.log(arr) - const first_half = [{ x: 0, y: this.#canvas_height / 2 }] - const second_half = [{ x: 0, y: this.#canvas_height / 2 }] - for (let i = 0; i < arr.length - 1; i += 2) { - first_half.push(arr[i]) - second_half.push(arr[i + 1]) - } - first_half.push({ x: this.#canvas_width, y: this.#canvas_height / 2 }) - second_half.push({ x: this.#canvas_width, y: this.#canvas_height / 2 }) - // path += `M ${this.#canvas_width},${this.#canvas_height / 2}` - second_half.reverse() - //path += ` L 0 ${this.#canvas_height / 2}` - path += this.#catmull_rom_smooth(first_half, 1) - //path += ` L ${this.#canvas_width} ${this.#canvas_height / 2}` - path += this.#catmull_rom_smooth(second_half, 1) - //path += `L 0 ${this.#canvas_height / 2}` - } - else { - path += this.#catmull_rom_smooth(arr, 1) - } - path += `Z` - break - } - } - return `` - } + console.error("BezierWeighted not implemented yet...") + break + } + case SmoothingAlgorythm.CatmullRom: { + if ( + this.#shape.shape_type == ShapeType.Waveform && + this.#shape.symmetry == true + ) { + //adding points so both halfs ends and start at the same center point + console.log(arr) + const first_half = [{ x: 0, y: this.#canvas_height / 2 }] + const second_half = [{ x: 0, y: this.#canvas_height / 2 }] + for (let i = 0; i < arr.length - 1; i += 2) { + first_half.push(arr[i]) + second_half.push(arr[i + 1]) + } + first_half.push({ + x: this.#canvas_width, + y: this.#canvas_height / 2 + }) + second_half.push({ + x: this.#canvas_width, + y: this.#canvas_height / 2 + }) + // path += `M ${this.#canvas_width},${this.#canvas_height / 2}` + second_half.reverse() + //path += ` L 0 ${this.#canvas_height / 2}` + path += this.#catmull_rom_smooth(first_half, 1) + //path += ` L ${this.#canvas_width} ${this.#canvas_height / 2}` + path += this.#catmull_rom_smooth(second_half, 1) + //path += `L 0 ${this.#canvas_height / 2}` + } else { + path += this.#catmull_rom_smooth(arr, 1) + } + path += `Z` + break + } + } + return `` + } - on_data(fn: ((data: Float32Array) => void)) { - this.#subscriber_fns.push(fn) - } - /** - * Useful for waveforms or shapes that don't need to redraw every frame - */ - draw_once() { - this.#svg_injecting_element.innerHTML = this.#create_svg_element() - this.#subscriber_fns.forEach((fn) => fn(this.#fft_data)) - } + on_data(fn: (data: Float32Array) => void) { + this.#subscriber_fns.push(fn) + } + /** + * Useful for waveforms or shapes that don't need to redraw every frame + */ + draw_once() { + this.#svg_injecting_element.innerHTML = this.#create_svg_element() + this.#subscriber_fns.forEach((fn) => fn(this.#fft_data)) + } - draw() { - this.#svg_injecting_element.innerHTML = this.#create_svg_element() - this.#subscriber_fns.forEach((fn) => fn(this.#fft_data)) - requestAnimationFrame(this.draw.bind(this)) - } + draw() { + this.#svg_injecting_element.innerHTML = this.#create_svg_element() + this.#subscriber_fns.forEach((fn) => fn(this.#fft_data)) + requestAnimationFrame(this.draw.bind(this)) + } } export class AudioVisualBuilder { - #analyzer_node - #svg_injecting_element - #canvas_height - #canvas_width - #buffer_length - #smoothing_algorythm - #fft_size - #fft_multipier - #fft_offset - #from_fft_range - #to_fft_range - #point_count: number - constructor(analyzer_node: AnalyserNode, svg_injecting_element: SVGSVGElement) { - this.#analyzer_node = analyzer_node - this.#svg_injecting_element = svg_injecting_element - this.#canvas_width = svg_injecting_element.viewBox.baseVal.width - this.#canvas_height = svg_injecting_element.viewBox.baseVal.height - this.#buffer_length = analyzer_node.frequencyBinCount - this.#smoothing_algorythm = SmoothingAlgorythm.Linear - this.#fft_size = 2048 - this.#fft_multipier = 1.5 - this.#fft_offset = 150 - this.#from_fft_range = 0 - this.#to_fft_range = 100 - this.#point_count = Math.round((this.#buffer_length / 100) * (this.#from_fft_range - this.#to_fft_range)) - } - /** - * The smoothingTimeConstant property of the AnalyserNode interface is a double value representing the averaging constant with the last analysis frame. It's basically an average between the current buffer and the last buffer the AnalyserNode processed, and results in a much smoother set of value changes over time. - * @param fft_time_smoothing_i A double within the range 0 to 1 (0 meaning no time averaging). The default value is 0.8. - * @returns this - */ - set_fft_time_smoothing(fft_time_smoothing_i: number) { - this.#analyzer_node.smoothingTimeConstant = fft_time_smoothing_i - return this - } - /** - * The fftSize property of the AnalyserNode interface is an unsigned long value and represents the window size in samples that is used when performing a Fast Fourier Transform (FFT) to get frequency domain data. - * - * If the point count set for the analyzer is less than the fft_from > FFT_DATA > fft_to, points might go missing or something will break. In that case increase, otherwise keep to as low as possible due to performance reasons - * @param fft_size Must be a power of 2 between 2^5 and 2^15, so one of: `32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, and 32768.` Defaults to `2048`. - * @returns this - */ - set_fft_size(fft_size: number) { - if (!(this.#fft_size && !(this.#fft_size & (this.#fft_size - 1)))) throw Error("fft_size not power of two") - this.#analyzer_node.fftSize = this.#fft_size = fft_size - this.#buffer_length = this.#analyzer_node.frequencyBinCount - return this - } - /** - * Changes how the FFT data is parsed. - * Limits the range like: - * - * `from_fft_range < FFT_DATA < to_fft_range`, - * - * then splits the remaining data evently between point_count. - * `fft_multiplier` and `fft_offset` define how the data then affects the point mutation. FFT data goes from `-Infinity` to `0` (Noise floor usually around `-160`db from experience) Multiplying the number exaggerates the higher decibel changes, making the result more dynamic, whilst the offset keeps the visualizer from clipping. - * - * Reccomended values are around: - * - * `shape_type == line` - * - * - multiplier: 3 - * - * - offset: -30 - * - * `shape_type == circle` - * - * - multiplier: 1.5 - * - * - offset: 150 - * @param param0 - * @returns - */ - set_fft_data_tresholds({ from_fft_range_i = 0, to_fft_range_i = 100, point_count_i = Math.round((this.#buffer_length / 100) * (from_fft_range_i - to_fft_range_i)), fft_multiplier_i = 2, fft_offset_i = -50 }) { - this.#from_fft_range = from_fft_range_i - this.#to_fft_range = to_fft_range_i - this.#point_count = point_count_i - this.#fft_multipier = fft_multiplier_i - this.#fft_offset = fft_offset_i - return this - } - /** + #analyzer_node + #svg_injecting_element + #canvas_height + #canvas_width + #buffer_length + #smoothing_algorythm + #fft_size + #fft_multipier + #fft_offset + #from_fft_range + #to_fft_range + #point_count: number + constructor( + analyzer_node: AnalyserNode, + svg_injecting_element: SVGSVGElement + ) { + this.#analyzer_node = analyzer_node + this.#svg_injecting_element = svg_injecting_element + this.#canvas_width = svg_injecting_element.viewBox.baseVal.width + this.#canvas_height = svg_injecting_element.viewBox.baseVal.height + this.#buffer_length = analyzer_node.frequencyBinCount + this.#smoothing_algorythm = SmoothingAlgorythm.Linear + this.#fft_size = 2048 + this.#fft_multipier = 1.5 + this.#fft_offset = 150 + this.#from_fft_range = 0 + this.#to_fft_range = 100 + this.#point_count = Math.round( + (this.#buffer_length / 100) * + (this.#from_fft_range - this.#to_fft_range) + ) + } + /** + * The smoothingTimeConstant property of the AnalyserNode interface is a double value representing the averaging constant with the last analysis frame. It's basically an average between the current buffer and the last buffer the AnalyserNode processed, and results in a much smoother set of value changes over time. + * @param fft_time_smoothing_i A double within the range 0 to 1 (0 meaning no time averaging). The default value is 0.8. + * @returns this + */ + set_fft_time_smoothing(fft_time_smoothing_i: number) { + this.#analyzer_node.smoothingTimeConstant = fft_time_smoothing_i + return this + } + /** + * The fftSize property of the AnalyserNode interface is an unsigned long value and represents the window size in samples that is used when performing a Fast Fourier Transform (FFT) to get frequency domain data. + * + * If the point count set for the analyzer is less than the fft_from > FFT_DATA > fft_to, points might go missing or something will break. In that case increase, otherwise keep to as low as possible due to performance reasons + * @param fft_size Must be a power of 2 between 2^5 and 2^15, so one of: `32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, and 32768.` Defaults to `2048`. + * @returns this + */ + set_fft_size(fft_size: number) { + if (!(this.#fft_size && !(this.#fft_size & (this.#fft_size - 1)))) + throw Error("fft_size not power of two") + this.#analyzer_node.fftSize = this.#fft_size = fft_size + this.#buffer_length = this.#analyzer_node.frequencyBinCount + return this + } + /** + * Changes how the FFT data is parsed. + * Limits the range like: + * + * `from_fft_range < FFT_DATA < to_fft_range`, + * + * then splits the remaining data evently between point_count. + * `fft_multiplier` and `fft_offset` define how the data then affects the point mutation. FFT data goes from `-Infinity` to `0` (Noise floor usually around `-160`db from experience) Multiplying the number exaggerates the higher decibel changes, making the result more dynamic, whilst the offset keeps the visualizer from clipping. + * + * Reccomended values are around: + * + * `shape_type == line` + * + * - multiplier: 3 + * + * - offset: -30 + * + * `shape_type == circle` + * + * - multiplier: 1.5 + * + * - offset: 150 + * @param param0 + * @returns + */ + set_fft_data_tresholds({ + from_fft_range_i = 0, + to_fft_range_i = 100, + point_count_i = Math.round( + (this.#buffer_length / 100) * (from_fft_range_i - to_fft_range_i) + ), + fft_multiplier_i = 2, + fft_offset_i = -50 + }) { + this.#from_fft_range = from_fft_range_i + this.#to_fft_range = to_fft_range_i + this.#point_count = point_count_i + this.#fft_multipier = fft_multiplier_i + this.#fft_offset = fft_offset_i + return this + } + /** * Defines what the svg d attribute path command will use. * * `Linear` - Uses no interpolation between points, jagged but very fast @@ -483,99 +598,154 @@ export class AudioVisualBuilder { * @param algorythm Linear = 0; BezierPerpendicular = 1; CatmullRom = 2; BezierWeighted = 3; * @returns */ - set_smoothing_algorythm(algorythm: SmoothingAlgorythm) { - this.#smoothing_algorythm = algorythm - return this - } - /** - * Creates the Audio Visualizer. To start drawing, use .draw(). To further modify it's looks use CSS, eg: - * ```css - * svg { - * fill: grey; - * stroke: black; - * transform: rotate(90deg); - * } - * ``` - * @param shape_type Circle = 0; Line = 1; - * @returns `new AudioVisual` - */ - build(shape_type: ShapeType, symmetry: boolean, waveform_options?: WaveformOptions) { - const shape = this.#create_shape(shape_type, symmetry, waveform_options) - return new AudioVisual(this.#analyzer_node, this.#svg_injecting_element, shape, this.#buffer_length, this.#fft_multipier, this.#fft_offset, this.#from_fft_range, this.#to_fft_range, this.#point_count) - } - #create_shape(shape_type: ShapeType, symmetry: boolean, waveform_options?: WaveformOptions): Shape { - const point_amount = this.#get_cured_frequency_data().length - let new_shape: Shape - switch (shape_type) { - case ShapeType.Line: { - const points = [] - for (let i = 0; i < point_amount; i++) { - points.push({ - x: (this.#canvas_width / point_amount) * i, - y: 0, - }) - } - new_shape = { shape_type, points, smoothing_algorythm: this.#smoothing_algorythm } - break - } - case ShapeType.Circle: { - const points = [] - const radius = this.#canvas_height > this.#canvas_width ? this.#canvas_height / 5 : this.#canvas_width / 5 - for (let i = 0; i < point_amount; i++) { - points.push({ - x: Math.cos(((2 * Math.PI) / point_amount) * i - Math.PI / 2) * radius, - y: Math.sin(((2 * Math.PI) / point_amount) * i - Math.PI / 2) * radius, - }) - } + set_smoothing_algorythm(algorythm: SmoothingAlgorythm) { + this.#smoothing_algorythm = algorythm + return this + } + /** + * Creates the Audio Visualizer. To start drawing, use .draw(). To further modify it's looks use CSS, eg: + * ```css + * svg { + * fill: grey; + * stroke: black; + * transform: rotate(90deg); + * } + * ``` + * @param shape_type Circle = 0; Line = 1; + * @returns `new AudioVisual` + */ + build( + shape_type: ShapeType, + symmetry: boolean, + waveform_options?: WaveformOptions + ) { + const shape = this.#create_shape(shape_type, symmetry, waveform_options) + return new AudioVisual( + this.#analyzer_node, + this.#svg_injecting_element, + shape, + this.#buffer_length, + this.#fft_multipier, + this.#fft_offset, + this.#from_fft_range, + this.#to_fft_range, + this.#point_count + ) + } + #create_shape( + shape_type: ShapeType, + symmetry: boolean, + waveform_options?: WaveformOptions + ): Shape { + const point_amount = this.#get_cured_frequency_data().length + let new_shape: Shape + switch (shape_type) { + case ShapeType.Line: { + const points = [] + for (let i = 0; i < point_amount; i++) { + points.push({ + x: (this.#canvas_width / point_amount) * i, + y: 0 + }) + } + new_shape = { + shape_type, + points, + smoothing_algorythm: this.#smoothing_algorythm + } + break + } + case ShapeType.Circle: { + const points = [] + const radius = + this.#canvas_height > this.#canvas_width + ? this.#canvas_height / 5 + : this.#canvas_width / 5 + for (let i = 0; i < point_amount; i++) { + points.push({ + x: + Math.cos( + ((2 * Math.PI) / point_amount) * i - Math.PI / 2 + ) * radius, + y: + Math.sin( + ((2 * Math.PI) / point_amount) * i - Math.PI / 2 + ) * radius + }) + } - new_shape = { shape_type, points, smoothing_algorythm: this.#smoothing_algorythm } - break - } - case ShapeType.Waveform: { - if (waveform_options === undefined) { - console.error("Waveform options undefined at shapetype.waveform, please define!") - throw Error("Waveform options undefined at shapetype.waveform, please define!") - } - const fft_length = this.#get_cured_frequency_data(waveform_options.fft_data).length - const points = [] - for (let i = 0; i < fft_length; i++) { - let x, y - if (waveform_options.shape_type == WaveformShape.LineLike) { - x = (this.#canvas_width / point_amount) * i - y = this.#canvas_height / 2 - } else { - throw Error("WaveformShape.Striped not implemented yet") - } - waveform_options.orientation == WaveformOrientation.Horizontal ? - points.push({ x: x, y: y }) : - points.push({ x: y, y: x }) - //Douple the points needed for symmetry - if (symmetry) { - waveform_options.orientation == WaveformOrientation.Horizontal ? - points.push({ x: x, y: y }) : - points.push({ x: y, y: x }) - } - } - new_shape = { shape_type, points, smoothing_algorythm: this.#smoothing_algorythm, symmetry: symmetry, waveform_options: waveform_options } - } - } - return new_shape - } + new_shape = { + shape_type, + points, + smoothing_algorythm: this.#smoothing_algorythm + } + break + } + case ShapeType.Waveform: { + if (waveform_options === undefined) { + console.error( + "Waveform options undefined at shapetype.waveform, please define!" + ) + throw Error( + "Waveform options undefined at shapetype.waveform, please define!" + ) + } + const fft_length = this.#get_cured_frequency_data( + waveform_options.fft_data + ).length + const points = [] + for (let i = 0; i < fft_length; i++) { + let x, y + if (waveform_options.shape_type == WaveformShape.LineLike) { + x = (this.#canvas_width / point_amount) * i + y = this.#canvas_height / 2 + } else { + throw Error("WaveformShape.Striped not implemented yet") + } + waveform_options.orientation == + WaveformOrientation.Horizontal + ? points.push({ x: x, y: y }) + : points.push({ x: y, y: x }) + //Douple the points needed for symmetry + if (symmetry) { + waveform_options.orientation == + WaveformOrientation.Horizontal + ? points.push({ x: x, y: y }) + : points.push({ x: y, y: x }) + } + } + new_shape = { + shape_type, + points, + smoothing_algorythm: this.#smoothing_algorythm, + symmetry: symmetry, + waveform_options: waveform_options + } + } + } + return new_shape + } - #get_cured_frequency_data(fft_data?: Float32Array) { - if (!fft_data) { - fft_data = new Float32Array(this.#buffer_length) - this.#analyzer_node.getFloatFrequencyData(fft_data) - } - const from = Math.round((this.#point_count / 100) * this.#from_fft_range) - const to = Math.round(this.#buffer_length - (this.#buffer_length / 100) * this.#to_fft_range) - const squeezeFactor = Math.round((this.#buffer_length - to) / this.#point_count) - - const return_array = new Array(this.#point_count) - for (let i = 0; i < this.#point_count; i++) { - return_array[i] = fft_data[from + i * squeezeFactor] - } - return return_array - } + #get_cured_frequency_data(fft_data?: Float32Array) { + if (!fft_data) { + fft_data = new Float32Array(this.#buffer_length) + this.#analyzer_node.getFloatFrequencyData(fft_data) + } + const from = Math.round( + (this.#point_count / 100) * this.#from_fft_range + ) + const to = Math.round( + this.#buffer_length - + (this.#buffer_length / 100) * this.#to_fft_range + ) + const squeezeFactor = Math.round( + (this.#buffer_length - to) / this.#point_count + ) + const return_array = new Array(this.#point_count) + for (let i = 0; i < this.#point_count; i++) { + return_array[i] = fft_data[from + i * squeezeFactor] + } + return return_array + } } diff --git a/test.js b/test.js index 93cede9..015e61c 100644 --- a/test.js +++ b/test.js @@ -1,47 +1,50 @@ class AudioContexthehe { - state = "suspended" - constructor() { } - resume() { - return new Promise((resolve, reject) => { - this.state = "running" - resolve() - }) - } + state = "suspended" + constructor() {} + resume() { + return new Promise((resolve, reject) => { + this.state = "running" + resolve() + }) + } } class AudioElementHehe { - constructor() { } - play() { - return new Promise((resolve, reject) => { - console.log("playing!") - resolve() - }) - } - pause() { - console.log("Pausing!") - } + constructor() {} + play() { + return new Promise((resolve, reject) => { + console.log("playing!") + resolve() + }) + } + pause() { + console.log("Pausing!") + } } -const audio_context = new AudioContexthehe -const audio_element = new AudioElementHehe +const audio_context = new AudioContexthehe() +const audio_element = new AudioElementHehe() let is_playing = false try_play_toggle_async() function try_play_toggle_async() { - return new Promise((resolve, reject) => { - if (audio_context.state !== "running") { - audio_context.resume().catch((e) => reject(e)) - } - if (audio_element.paused) { - audio_element.play().then((s) => { - is_playing = true - resolve(s) - }, (r) => { - is_playing = false - reject(r) - }) - } else { - audio_element.pause() - is_playing = false - resolve(null) - } - }) -} \ No newline at end of file + return new Promise((resolve, reject) => { + if (audio_context.state !== "running") { + audio_context.resume().catch((e) => reject(e)) + } + if (audio_element.paused) { + audio_element.play().then( + (s) => { + is_playing = true + resolve(s) + }, + (r) => { + is_playing = false + reject(r) + } + ) + } else { + audio_element.pause() + is_playing = false + resolve(null) + } + }) +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 5d61caf..2da566e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,42 +1,42 @@ { - "compileOnSave": false, - "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, - "target": "es2015", - "module": "esnext", - "lib": [ - "es2020", - "dom" - ], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": { - "@euterpe.js/dj": [ - "packages/dj/src/index.ts" - ], - "@euterpe.js/euterpe": [ - "packages/euterpe/src/index.ts" - ], - "@euterpe.js/music-library": [ - "packages/music-library/src/index.ts" - ], - "@euterpe.js/player": [ - "packages/player/src/index.ts" - ], - "@euterpe.js/visualizer": [ - "packages/visualizer/src/index.ts" - ] - } - }, - "exclude": [ - "node_modules", - "tmp" - ] + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "es2015", + "module": "esnext", + "lib": [ + "es2020", + "dom" + ], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": { + "@euterpe.js/dj": [ + "packages/dj/src/index.ts" + ], + "@euterpe.js/euterpe": [ + "packages/euterpe/src/index.ts" + ], + "@euterpe.js/music-library": [ + "packages/music-library/src/index.ts" + ], + "@euterpe.js/player": [ + "packages/player/src/index.ts" + ], + "@euterpe.js/visualizer": [ + "packages/visualizer/src/index.ts" + ] + } + }, + "exclude": [ + "node_modules", + "tmp" + ] }