diff --git a/packages/dj/.eslintrc.json b/packages/dj/.eslintrc.json new file mode 100644 index 0000000..5313267 --- /dev/null +++ b/packages/dj/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": [ + "../../.eslintrc.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} diff --git a/packages/dj/README.md b/packages/dj/README.md new file mode 100644 index 0000000..f60306b --- /dev/null +++ b/packages/dj/README.md @@ -0,0 +1,13 @@ +# dj + +This library was generated with [Nx](https://nx.dev). + + + +## Building + +Run `nx build dj` to build the library. + + + + diff --git a/packages/dj/package.json b/packages/dj/package.json new file mode 100644 index 0000000..e41574d --- /dev/null +++ b/packages/dj/package.json @@ -0,0 +1,5 @@ +{ + "name": "@euterpe.js/dj", + "version": "0.0.1", + "type": "module" +} diff --git a/packages/dj/project.json b/packages/dj/project.json new file mode 100644 index 0000000..1c465b4 --- /dev/null +++ b/packages/dj/project.json @@ -0,0 +1,40 @@ +{ + "name": "dj", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/dj/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/dj", + "main": "packages/dj/src/index.ts", + "tsConfig": "packages/dj/tsconfig.lib.json", + "assets": [ + "packages/dj/*.md" + ] + } + }, + "publish": { + "command": "node tools/scripts/publish.mjs dj {args.ver} {args.tag}", + "dependsOn": [ + "build" + ] + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": [ + "{options.outputFile}" + ], + "options": { + "lintFilePatterns": [ + "packages/dj/**/*.ts" + ] + } + } + }, + "tags": [] +} diff --git a/packages/dj/src/db_extend.ts b/packages/dj/src/db_extend.ts new file mode 100644 index 0000000..00dc53e --- /dev/null +++ b/packages/dj/src/db_extend.ts @@ -0,0 +1,74 @@ +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[] +} + +class DJSong extends Song { + 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()) + } +} +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 + + 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) + } + } + } + +} \ No newline at end of file diff --git a/packages/dj/src/euterpe_extend.ts b/packages/dj/src/euterpe_extend.ts new file mode 100644 index 0000000..392fb82 --- /dev/null +++ b/packages/dj/src/euterpe_extend.ts @@ -0,0 +1,125 @@ +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 + } + +} + +class Track { + 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)) + } + + 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 diff --git a/packages/dj/src/index.ts b/packages/dj/src/index.ts new file mode 100644 index 0000000..22f1992 --- /dev/null +++ b/packages/dj/src/index.ts @@ -0,0 +1 @@ +import { MusicPlayerBuilder } from "@euterpe.js/player"; \ No newline at end of file diff --git a/packages/dj/tsconfig.json b/packages/dj/tsconfig.json new file mode 100644 index 0000000..db7b566 --- /dev/null +++ b/packages/dj/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/dj/tsconfig.lib.json b/packages/dj/tsconfig.lib.json new file mode 100644 index 0000000..33eca2c --- /dev/null +++ b/packages/dj/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/euterpe/package.json b/packages/euterpe/package.json index fb29ac2..003b613 100644 --- a/packages/euterpe/package.json +++ b/packages/euterpe/package.json @@ -1,6 +1,6 @@ { "name": "@euterpe.js/euterpe", - "version": "1.0.8", + "version": "1.0.9", "type": "module", "description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!", "main": "./src/index.js", diff --git a/packages/player/package.json b/packages/player/package.json index c85b403..e85f01c 100644 --- a/packages/player/package.json +++ b/packages/player/package.json @@ -1,6 +1,6 @@ { "name": "@euterpe.js/player", - "version": "1.0.4", + "version": "1.0.5", "type": "module", "description": "A simple, safe AudioContext web music player", "main": "./src/index.js", diff --git a/packages/player/src/index.ts b/packages/player/src/index.ts index dc58d25..c47d0f9 100644 --- a/packages/player/src/index.ts +++ b/packages/player/src/index.ts @@ -86,10 +86,10 @@ export class MusicPlayer { time = 0 #pub_sub = new PubSub constructor( - private audio_context: AudioContext, + public audio_context: AudioContext, private audio_element: HTMLAudioElement, public track: MediaElementAudioSourceNode, - private gain: GainNode, + public gain: GainNode, public volume: number, private current_song_path?: string) { this.#volume_cache = volume @@ -278,7 +278,6 @@ export class MusicPlayer { try_new_song_async(path: string) { return new Promise((resolve, reject) => { this.audio_element.src = this.current_song_path = path - this.current_song_duration = this.audio_element.duration //Found out today about this. Such a nice new way to mass remove event listeners! const controller = new AbortController(); @@ -297,6 +296,11 @@ export class MusicPlayer { reject(e) }, { signal: controller.signal }) + //once aborted, try to set current_song_duration + controller.signal.addEventListener("abort", () => { + this.current_song_duration = this.audio_element.duration + }) + this.is_playing = false }) } diff --git a/tsconfig.base.json b/tsconfig.base.json index bf68c10..5d61caf 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,6 +18,9 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@euterpe.js/dj": [ + "packages/dj/src/index.ts" + ], "@euterpe.js/euterpe": [ "packages/euterpe/src/index.ts" ],