Start of euterpe dj!
This commit is contained in:
parent
044e38ccf1
commit
34facec032
13 changed files with 332 additions and 5 deletions
33
packages/dj/.eslintrc.json
Normal file
33
packages/dj/.eslintrc.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"../../.eslintrc.json"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"!**/*"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx",
|
||||||
|
"*.js",
|
||||||
|
"*.jsx"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.js",
|
||||||
|
"*.jsx"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
13
packages/dj/README.md
Normal file
13
packages/dj/README.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# dj
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Run `nx build dj` to build the library.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
5
packages/dj/package.json
Normal file
5
packages/dj/package.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "@euterpe.js/dj",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module"
|
||||||
|
}
|
40
packages/dj/project.json
Normal file
40
packages/dj/project.json
Normal file
|
@ -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": []
|
||||||
|
}
|
74
packages/dj/src/db_extend.ts
Normal file
74
packages/dj/src/db_extend.ts
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
125
packages/dj/src/euterpe_extend.ts
Normal file
125
packages/dj/src/euterpe_extend.ts
Normal file
|
@ -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<Error | self>
|
||||||
|
*/
|
||||||
|
async try_queue_track(track_i: number, delay: number) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.tracks[track_i].try_start(delay).then(() => resolve(this), (e) => reject(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Won't start playback, use try_queue_track() or try_start_track()
|
||||||
|
* @returns Promise<Error | self>
|
||||||
|
*/
|
||||||
|
async try_load_song_into_track(track_i: number, song: Song) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.tracks[track_i].change_song(song).then(() => resolve(this), (e) => reject(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param i index of track
|
||||||
|
* @returns Promise<Error | self>
|
||||||
|
*/
|
||||||
|
async try_start_track(track_i: number) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.tracks[track_i].try_start().then(() => resolve(this), (e) => reject(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This function will have to restart every track, so for now implementatino pending c:
|
||||||
|
* @param new_master_bpm number in bpm
|
||||||
|
*/
|
||||||
|
set_master_bpm(new_master_bpm: number) {
|
||||||
|
this.master_bpm = new_master_bpm
|
||||||
|
this.beat_duration = 60 / this.master_bpm
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Track {
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
1
packages/dj/src/index.ts
Normal file
1
packages/dj/src/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import { MusicPlayerBuilder } from "@euterpe.js/player";
|
19
packages/dj/tsconfig.json
Normal file
19
packages/dj/tsconfig.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
10
packages/dj/tsconfig.lib.json
Normal file
10
packages/dj/tsconfig.lib.json
Normal file
|
@ -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"]
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@euterpe.js/euterpe",
|
"name": "@euterpe.js/euterpe",
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!",
|
"description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@euterpe.js/player",
|
"name": "@euterpe.js/player",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "A simple, safe AudioContext web music player",
|
"description": "A simple, safe AudioContext web music player",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
|
|
|
@ -86,10 +86,10 @@ export class MusicPlayer {
|
||||||
time = 0
|
time = 0
|
||||||
#pub_sub = new PubSub
|
#pub_sub = new PubSub
|
||||||
constructor(
|
constructor(
|
||||||
private audio_context: AudioContext,
|
public audio_context: AudioContext,
|
||||||
private audio_element: HTMLAudioElement,
|
private audio_element: HTMLAudioElement,
|
||||||
public track: MediaElementAudioSourceNode,
|
public track: MediaElementAudioSourceNode,
|
||||||
private gain: GainNode,
|
public gain: GainNode,
|
||||||
public volume: number,
|
public volume: number,
|
||||||
private current_song_path?: string) {
|
private current_song_path?: string) {
|
||||||
this.#volume_cache = volume
|
this.#volume_cache = volume
|
||||||
|
@ -278,7 +278,6 @@ export class MusicPlayer {
|
||||||
try_new_song_async(path: string) {
|
try_new_song_async(path: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.audio_element.src = this.current_song_path = path
|
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!
|
//Found out today about this. Such a nice new way to mass remove event listeners!
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
@ -297,6 +296,11 @@ export class MusicPlayer {
|
||||||
reject(e)
|
reject(e)
|
||||||
}, { signal: controller.signal })
|
}, { 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
|
this.is_playing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"@euterpe.js/dj": [
|
||||||
|
"packages/dj/src/index.ts"
|
||||||
|
],
|
||||||
"@euterpe.js/euterpe": [
|
"@euterpe.js/euterpe": [
|
||||||
"packages/euterpe/src/index.ts"
|
"packages/euterpe/src/index.ts"
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in a new issue