Start of euterpe dj!

This commit is contained in:
Djkáťo 2023-07-28 16:23:28 +02:00
parent 044e38ccf1
commit 34facec032
13 changed files with 332 additions and 5 deletions

View 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
View 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
View file

@ -0,0 +1,5 @@
{
"name": "@euterpe.js/dj",
"version": "0.0.1",
"type": "module"
}

40
packages/dj/project.json Normal file
View 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": []
}

View 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)
}
}
}
}

View 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
View file

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

19
packages/dj/tsconfig.json Normal file
View 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"
}
]
}

View 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"]
}

View file

@ -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",

View file

@ -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",

View file

@ -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
})
}

View file

@ -18,6 +18,9 @@
"skipDefaultLibCheck": true,
"baseUrl": ".",
"paths": {
"@euterpe.js/dj": [
"packages/dj/src/index.ts"
],
"@euterpe.js/euterpe": [
"packages/euterpe/src/index.ts"
],