From b57321350d62ce129633e4508db850478c4155d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djk=C3=A1=C5=A5o?= Date: Wed, 27 Dec 2023 18:47:54 +0100 Subject: [PATCH 1/4] linuxify ffmpeg --- packages/preprocessor/src/generate_db.ts | 91 ++++----- packages/preprocessor/src/media_process.js | 223 ++++++++++----------- packages/preprocessor/src/songs_list.ts | 2 +- 3 files changed, 158 insertions(+), 158 deletions(-) diff --git a/packages/preprocessor/src/generate_db.ts b/packages/preprocessor/src/generate_db.ts index 8246948..3a880aa 100644 --- a/packages/preprocessor/src/generate_db.ts +++ b/packages/preprocessor/src/generate_db.ts @@ -2,53 +2,54 @@ import { Collection, Ref, RefTo, Song, DB, } from "@euterpe.js/music-library" import { songs } from "./songs_list" export function generate_db() { - console.log(songs) - // construct db - let db = new DB - let collections: string[] = new Array() - let new_songs = [] + console.log(songs) + // construct db + let db = new DB + let collections: string[] = new Array() + let new_songs = [] + const path_char = songs[0].includes("\\") ? "\\" : "/" - //create collections by folder names - for (let i = 0; i < songs.length; i++) { - const song = songs[i] - const last_i = song.lastIndexOf("\\") - const collection_name = song.slice(song.slice(0, last_i).lastIndexOf("\\") + 1, last_i) - /* - const foreforelast_i = song.slice(0, forelast_i - 1) - const foreforeforelast_i = song.slice(0, foreforelast_i - 1).lastIndexOf("\\") - */ - if (!collections.includes(collection_name)) { - console.log(`creating collection ${collection_name}`) - db.add([new Collection({ - name: collection_name, - songs: [], - artists: [], - })]) - collections.push(collection_name) - } + //create collections by folder names + for (let i = 0; i < songs.length; i++) { + const song = songs[i] + const last_i = song.lastIndexOf(path_char) + const collection_name = song.slice(song.slice(0, last_i).lastIndexOf(path_char) + 1, last_i) + /* + const foreforelast_i = song.slice(0, forelast_i - 1) + const foreforeforelast_i = song.slice(0, foreforelast_i - 1).lastIndexOf("\\") + */ + if (!collections.includes(collection_name)) { + console.log(`creating collection ${collection_name}`) + db.add([new Collection({ + name: collection_name, + songs: [], + artists: [], + })]) + collections.push(collection_name) + } - let col = db.collections.find(col => col.name == collection_name)! - let col_id = col.id - new_songs.push({ song: song, collection_id: col_id! }) - } + let col = db.collections.find(col => col.name == collection_name)! + let col_id = col.id + new_songs.push({ song: song, collection_id: col_id! }) + } - //create songs - for (let i = 0; i < new_songs.length; i++) { - let song = new_songs[i] - const last_i = song.song.lastIndexOf("\\") + //create songs + for (let i = 0; i < new_songs.length; i++) { + let song = new_songs[i] + const last_i = song.song.lastIndexOf(path_char) - const name = song.song.slice(last_i + 1) - const song_url = song.song.slice(song.song.indexOf("public\\") + 7) - const db_song = new Song({ - name: name.slice(0, name.lastIndexOf(".")), - artists: [], - url: new URL(`${window.location.href}${song_url}`.replaceAll("\\", "/")), - duration: 0, - remix_artists: [], - in_collection: new Ref(RefTo.Collections, song.collection_id) - }) - db.add([db_song]) - } - console.log(db) - return db + const name = song.song.slice(last_i + 1) + const song_url = song.song.slice(song.song.indexOf(`public${path_char}`) + 7) + const db_song = new Song({ + name: name.slice(0, name.lastIndexOf(".")), + artists: [], + url: new URL(`${window.location.href}${song_url}`.replaceAll("\\", "/")), + duration: 0, + remix_artists: [], + in_collection: new Ref(RefTo.Collections, song.collection_id) + }) + db.add([db_song]) + } + console.log(db) + return db } diff --git a/packages/preprocessor/src/media_process.js b/packages/preprocessor/src/media_process.js index 96706f3..9d8c122 100644 --- a/packages/preprocessor/src/media_process.js +++ b/packages/preprocessor/src/media_process.js @@ -9,144 +9,143 @@ import { execSync, exec } from 'child_process' import { fstat, unlinkSync } from "fs" function generate_new_photo_sizes(file, currentExtention) { - const path = file.substring(0, file.lastIndexOf("\\")) - file = file.substring(file.lastIndexOf("\\") + 1) - let command = "" - command += `cd "${path}" && start cmd /k "` - command += `ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -quality 90 -compression_level 6 "${file}_ogw.webp" ` - command += ` -vf scale=1000:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_1000p.webp"` - command += ` -vf scale=800:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_800p.webp"` - command += ` -vf scale=500:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_500p.webp"` - command += ` -vf scale=320:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_320p.webp"` - command += ` && exit"` - exec(command) + const path = file.substring(0, file.lastIndexOf("\\")) + file = file.substring(file.lastIndexOf("\\") + 1) + let command = "" + command += `cd "${path}" && konsole -e "` + command += `ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -quality 90 -compression_level 6 "${file}_ogw.webp" ` + command += ` -vf scale=1000:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_1000p.webp"` + command += ` -vf scale=800:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_800p.webp"` + command += ` -vf scale=500:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_500p.webp"` + command += ` -vf scale=320:-1 -lossless 0 -quality 90 -compression_level 6 "${file}_320p.webp"` + command += ` && exit"` + exec(command) } function generate_new_anim_photo_sizes(file, currentExtention) { - exec(`start ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_ogw_static.webp" -vf scale=1000:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_1000w_static.webp" -vf scale=800:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_800w_static.webp" -vf scale=500:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_500w_static.webp" -vf scale=320:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_320w_static.webp" -vf scale=-1:64,gblur=sigma=10:steps=2 -lossless 0 -frames:v 1 -r 1 -compression_level 6 -quality 85 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_placeholder_static.webp"`) - exec(`start ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_ogw.webp" -vf scale=1000:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_1000w.webp" -vf scale=800:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_800w.webp" -vf scale=500:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_500w.webp" -vf scale=320:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_320w.webp" -vf scale=-1:64,gblur=sigma=10:steps=2 -frames:v 1 -lossless 0 -c:v libwebp -compression_level 6 -quality 85 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_placeholder.webp"`) + exec(`start ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_ogw_static.webp" -vf scale=1000:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_1000w_static.webp" -vf scale=800:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_800w_static.webp" -vf scale=500:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_500w_static.webp" -vf scale=320:-1 -lossless 0 -frames:v 1 -r 1 -quality 85 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_320w_static.webp" -vf scale=-1:64,gblur=sigma=10:steps=2 -lossless 0 -frames:v 1 -r 1 -compression_level 6 -quality 85 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_placeholder_static.webp"`) + exec(`start ffmpeg -y -i "${file}.${currentExtention}" -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_ogw.webp" -vf scale=1000:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_1000w.webp" -vf scale=800:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_800w.webp" -vf scale=500:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_500w.webp" -vf scale=320:-1 -lossless 0 -quality 85 -loop 0 -compression_level 6 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_320w.webp" -vf scale=-1:64,gblur=sigma=10:steps=2 -frames:v 1 -lossless 0 -c:v libwebp -compression_level 6 -quality 85 -metadata author="Djkáťo" -metadata copyright="https://djkato.net" "${file}_placeholder.webp"`) } function generate_new_sounds_ogg(file, currentExtention) { - const path = file.substring(0, file.lastIndexOf("\\")) - file = file.substring(file.lastIndexOf("\\") + 1) + const path = file.substring(0, file.lastIndexOf("\\")) + file = file.substring(file.lastIndexOf("\\") + 1) - let command = "" - command += `cd "${path}" && start cmd /k "` - command += `ffmpeg -y -i "${file}.${currentExtention}" ` - //Adds 25ms of delay to all samples - command += `-af "adelay=25:all=true" ` - //So the demo is HQ - if (file.includes("Luna Lenta")) command += `-c:a libopus -b:a 256k "${file}.ogg"` - else command += `-c:a libopus -b:a 96k "${file}.ogg"` - command += ` && exit"` - exec(command) - // console.log(command) + let command = "" + command += `konsole -e "` + command += `ffmpeg -y -i '${file}.${currentExtention}' ` + //Adds 25ms of delay to all samples + command += `-af 'adelay=25:all=true' ` + //So the demo is HQ + if (file.includes("demo")) command += `-c:a libopus -b:a 256k '${file}.ogg'"` + else command += `-c:a libopus -b:a 96k '${file}.ogg'"` + exec(command) + console.log(command) } function generate_new_sounds_mp3(file, currentExtention) { - const path = file.substring(0, file.lastIndexOf("\\")) - file = file.substring(file.lastIndexOf("\\") + 1) + const path = file.substring(0, file.lastIndexOf("\\")) + file = file.substring(file.lastIndexOf("\\") + 1) - let command = "" - command += `cd "${path}" && start cmd /k "` - command += `ffmpeg -y -i "${file}.${currentExtention}" ` - //Adds 25ms of delay to all samples - command += `-af "adelay=25:all=true" ` - command += `-b:a 160k "${file}.mp3"` - command += ` && exit"` - exec(command) - // console.log(command) + let command = "" + command += `konsole -e "` + command += `ffmpeg -y -i '${file}.${currentExtention}' ` + //Adds 25ms of delay to all samples + command += `-af 'adelay=25:all=true' ` + if (file.includes("demo")) command += `-b:a 256k '${file}.mp3'"` + else command += `-b:a 160k '${file}.mp3'"` + exec(command) + // console.log(command) } function generate_new_video_sizes_mp4(file, currentExtention, width_resolutions) { - const path = file.substring(0, file.lastIndexOf("\\")) - file = file.substring(file.lastIndexOf("\\") + 1) + const path = file.substring(0, file.lastIndexOf("\\")) + file = file.substring(file.lastIndexOf("\\") + 1) - let command = "" - command += `cd "${path}" && ` - command += `del ffmpeg2pass-0.log && ` - command += `ffmpeg -y -i "${file}.${currentExtention}" ` - command += `-vcodec libx264 -g 240 -b:v 3M ` - command += `-pass 1 -f mp4 NUL && exit` + let command = "" + command += `cd "${path}" && ` + command += `del ffmpeg2pass-0.log && ` + command += `ffmpeg -y -i "${file}.${currentExtention}" ` + command += `-vcodec libx264 -g 240 -b:v 3M ` + command += `-pass 1 -f mp4 NUL && exit` - exec(command).once("exit", () => { - for (const resolution of width_resolutions) { - let res_command = "" - res_command += `start cmd /k "` - res_command += `cd "${path}" && ` - res_command += `ffmpeg -y -i "${file}.${currentExtention}" ` - res_command += `-vcodec libx264 -g 240 -b:v 3M -vf scale=${resolution}:-2 -pass 2 "${file}_${resolution}p.mp4"` - res_command += "&& exit\"" - exec(res_command) - } - }) + exec(command).once("exit", () => { + for (const resolution of width_resolutions) { + let res_command = "" + res_command += `konsole -e "` + res_command += `cd "${path}" && ` + res_command += `ffmpeg -y -i "${file}.${currentExtention}" ` + res_command += `-vcodec libx264 -g 240 -b:v 3M -vf scale=${resolution}:-2 -pass 2 "${file}_${resolution}p.mp4"` + res_command += "&& exit\"" + exec(res_command) + } + }) } function generate_new_video_sizes_webm(file, currentExtention, width_resolutions) { - const path = file.substring(0, file.lastIndexOf("\\")) - file = file.substring(file.lastIndexOf("\\") + 1) + const path = file.substring(0, file.lastIndexOf("\\")) + file = file.substring(file.lastIndexOf("\\") + 1) - let command = "" - command += `cd "${path}" && ` - command += `del ffmpeg2pass-0.log && ` - command += `ffmpeg -y -i "${file}.${currentExtention}" ` - command += `-vcodec libvpx-vp9 -cpu-used 0 -deadline good -quality good -g 240 -crf 42 -b:v 0 -c:a libopus -row-mt 1 -tile-rows 2 -tile-columns 4 -threads 16 -auto-alt-ref 6 ` - command += `-pass 1 -f webm NUL && exit` + let command = "" + command += `cd "${path}" && ` + command += `del ffmpeg2pass-0.log && ` + command += `ffmpeg -y -i "${file}.${currentExtention}" ` + command += `-vcodec libvpx-vp9 -cpu-used 0 -deadline good -quality good -g 240 -crf 42 -b:v 0 -c:a libopus -row-mt 1 -tile-rows 2 -tile-columns 4 -threads 16 -auto-alt-ref 6 ` + command += `-pass 1 -f webm NUL && exit` - exec(command).once("exit", () => { - for (const resolution of width_resolutions) { - let res_command = "" - res_command += `start cmd /k "` - res_command += `cd "${path}" && ` - res_command += `ffmpeg -y -i "${file}.${currentExtention}" ` - res_command += `-vcodec libvpx-vp9 -cpu-used 0 -deadline good -quality good -g 240 -vf scale=${resolution}:-1 -crf 42 -b:v 0 -c:a libopus -row-mt 1 -tile-rows 2 -tile-columns 4 -threads 16 -auto-alt-ref 6 -pass 2 "${file}_${resolution}p.webm"` - res_command += "&& exit\"" - exec(res_command) - } - }) + exec(command).once("exit", () => { + for (const resolution of width_resolutions) { + let res_command = "" + res_command += `konsole -e "` + res_command += `cd "${path}" && ` + res_command += `ffmpeg -y -i "${file}.${currentExtention}" ` + res_command += `-vcodec libvpx-vp9 -cpu-used 0 -deadline good -quality good -g 240 -vf scale=${resolution}:-1 -crf 42 -b:v 0 -c:a libopus -row-mt 1 -tile-rows 2 -tile-columns 4 -threads 16 -auto-alt-ref 6 -pass 2 "${file}_${resolution}p.webm"` + res_command += "&& exit\"" + exec(res_command) + } + }) } let dirs = filehound.create() - .path("../public/media") - .directory() - .findSync() + .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() + //gets current name file+ext + let current_folder_files = filehound.create() + .path(`${dirs[i]}`) + .findSync() - if (current_folder_files[0] != undefined) { - //if previous encode was cancelled and 2pass log not removed, remove it :) - if (current_folder_files[0].includes("ffmpeg2pass-0.log")) { - try { unlinkSync(`${dirs[i]}/ffmpeg2pass-0.log`) } catch (err) { } - current_folder_files = current_folder_files.slice(1) - } - for (let current_media of current_folder_files) { - current_media = [current_media.substring(0, current_media.lastIndexOf(".")), current_media.substring(current_media.lastIndexOf(".") + 1)] - if (current_media[1] == "wav" || current_media[1] == "mp3") { - console.log(`${current_media[0]}.${current_media[1]}\n`) + if (current_folder_files[0] != undefined) { + //if previous encode was cancelled and 2pass log not removed, remove it :) + if (current_folder_files[0].includes("ffmpeg2pass-0.log")) { + try { unlinkSync(`${dirs[i]}/ffmpeg2pass-0.log`) } catch (err) { } + current_folder_files = current_folder_files.slice(1) + } + for (let current_media of current_folder_files) { + current_media = [current_media.substring(0, current_media.lastIndexOf(".")), current_media.substring(current_media.lastIndexOf(".") + 1)] + if (current_media[1] == "wav") { + console.log(`${current_media[0]}.${current_media[1]}\n`) - generate_new_sounds_ogg(`${current_media[0]}`, `${current_media[1]}`) - if (current_media[1] == "mp3") continue - 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]}`) + } - if (current_media[1] == "webm" || current_media[1] == "mov" || current_media[1] == "avi" || current_media[1] == "mp4") { - if (/\_\d*p/.test(current_media[0])) continue - // console.log(`Video: ${current_media[0]}.${current_media[1]}\n`) - // generate_new_video_sizes_webm(`${current_media[0]}`, `${current_media[1]}`, [1440, 1080, 720, 480]) - // generate_new_video_sizes_mp4(`${current_media[0]}`, `${current_media[1]}`, [1440, 1080, 720, 480]) - } - if (current_media[1] == "png" || current_media[1] == "jpg") { - console.log(`.\\${current_media[0]}.${current_media[1]}\n`) + /* + if (current_media[1] == "webm" || current_media[1] == "mov" || current_media[1] == "avi" || current_media[1] == "mp4") { + if (/\_\d*p/.test(current_media[0])) continue + // console.log(`Video: ${current_media[0]}.${current_media[1]}\n`) + // generate_new_video_sizes_webm(`${current_media[0]}`, `${current_media[1]}`, [1440, 1080, 720, 480]) + // generate_new_video_sizes_mp4(`${current_media[0]}`, `${current_media[1]}`, [1440, 1080, 720, 480]) + } + if (current_media[1] == "png" || current_media[1] == "jpg") { + console.log(`.\\${current_media[0]}.${current_media[1]}\n`) - generate_new_photo_sizes(`.\\${current_media[0]}`, `${current_media[1]}`) - } - if (current_media[1] == "gif") { - console.log(`.\\${current_media[0]}.${current_media[1]}\n`) + generate_new_photo_sizes(`.\\${current_media[0]}`, `${current_media[1]}`) + } + if (current_media[1] == "gif") { + console.log(`.\\${current_media[0]}.${current_media[1]}\n`) - generate_new_anim_photo_sizes(`.\\${current_media[0]}`, `${current_media[1]}`) - } - } - } + generate_new_anim_photo_sizes(`.\\${current_media[0]}`, `${current_media[1]}`) + }*/ + } + } } diff --git a/packages/preprocessor/src/songs_list.ts b/packages/preprocessor/src/songs_list.ts index 43a5a84..6471b09 100644 --- a/packages/preprocessor/src/songs_list.ts +++ b/packages/preprocessor/src/songs_list.ts @@ -1 +1 @@ -export const songs = ["..\\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\\Demo by Luna Lenta and Nuphory.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\\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\\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"] \ No newline at end of file -- 2.45.2 From ed2d865f2d93d44e1003e7f41625bbccc3fbe638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djk=C3=A1=C5=A5o?= Date: Mon, 12 Feb 2024 16:28:54 +0100 Subject: [PATCH 2/4] fix formatting --- babel.config.json | 6 +- nx.json | 148 +- package.json | 85 +- packages/dj/src/db_extend.ts | 140 +- packages/dj/src/euterpe_extend.ts | 260 ++-- packages/dj/src/index.ts | 2 +- packages/euterpe-web-test/src/db.ts | 144 +- packages/euterpe-web-test/src/main.ts | 168 +-- packages/euterpe/src/index.ts | 835 ++++++------ packages/music-library-web-test/src/db.ts | 118 +- packages/music-library-web-test/src/main.ts | 231 ++-- packages/music-library/src/index.ts | 581 ++++---- packages/player-web-test/src/main.ts | 133 +- packages/player/src/index.ts | 847 ++++++------ packages/preprocessor/src/crawler.mjs | 15 +- packages/preprocessor/src/generate_db.ts | 31 +- packages/preprocessor/src/main.ts | 245 ++-- packages/preprocessor/src/media_process.js | 60 +- packages/preprocessor/src/songs_list.ts | 34 +- packages/visualizer-web-test/src/main.ts | 243 ++-- .../visualizer-web-test/src/waveform_data.ts | 29 +- packages/visualizer/src/index.ts | 1196 ++++++++++------- test.js | 81 +- tsconfig.base.json | 80 +- 24 files changed, 3217 insertions(+), 2495 deletions(-) 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" + ] } -- 2.45.2 From cd97233bce5755259375489d6421006cfcb37e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djk=C3=A1=C5=A5o?= Date: Mon, 12 Feb 2024 16:36:02 +0100 Subject: [PATCH 3/4] formatting, play_next when song ended --- .prettierrc | 23 +- packages/dj/src/db_extend.ts | 37 +-- packages/dj/src/euterpe_extend.ts | 43 +--- packages/euterpe-web-test/src/db.ts | 47 +--- packages/euterpe-web-test/src/main.ts | 22 +- packages/euterpe/src/index.ts | 118 +++------- packages/music-library-web-test/src/db.ts | 35 +-- packages/music-library-web-test/src/main.ts | 77 +++--- packages/music-library/src/index.ts | 89 ++----- packages/player-web-test/src/main.ts | 139 +++++------ packages/player/src/index.ts | 95 ++------ packages/preprocessor/src/crawler.mjs | 17 +- packages/preprocessor/src/generate_db.ts | 13 +- packages/preprocessor/src/main.ts | 65 ++---- packages/preprocessor/src/media_process.js | 30 +-- packages/visualizer-web-test/src/main.ts | 199 ++++++---------- .../visualizer-web-test/src/waveform_data.ts | 19 +- packages/visualizer/src/index.ts | 221 ++++-------------- tools/scripts/publish.mjs | 16 +- 19 files changed, 375 insertions(+), 930 deletions(-) diff --git a/.prettierrc b/.prettierrc index 841d5a2..5ee78f4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,12 +1,13 @@ { - "parser": "typescript", - "trailingComma": "none", - "useTabs": true, - "tabWidth": 4, - "semi": false, - "singleQuote": false, - "endOfLine": "lf", - "bracketSpacing": true, - "bracketSameLine": false, - "arrowParens": "always" -} \ No newline at end of file + "parser": "typescript", + "trailingComma": "none", + "useTabs": true, + "tabWidth": 4, + "semi": false, + "singleQuote": false, + "endOfLine": "lf", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "printWidth": 150 +} diff --git a/packages/dj/src/db_extend.ts b/packages/dj/src/db_extend.ts index 3525403..77cb57e 100644 --- a/packages/dj/src/db_extend.ts +++ b/packages/dj/src/db_extend.ts @@ -1,11 +1,4 @@ -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 @@ -34,11 +27,9 @@ class DJSong extends Song { try { fetch(data.url).then((file) => { file.arrayBuffer().then((buffer) => { - audio_context - .decodeAudioData(buffer) - .then((audio_buffer) => { - this.audio_buffer = audio_buffer - }) + audio_context.decodeAudioData(buffer).then((audio_buffer) => { + this.audio_buffer = audio_buffer + }) }) }) } catch (e) { @@ -46,17 +37,13 @@ class DJSong extends Song { } } public async analyze(url: URL, audio_context: AudioContext) { - this.audio_buffer = await audio_context.decodeAudioData( - await (await fetch(url)).arrayBuffer() - ) + this.audio_buffer = await audio_context.decodeAudioData(await (await fetch(url)).arrayBuffer()) } } class DJDB extends DB { dj_add(dj_songs: DJSong[]): void { let inputs - typeof dj_songs[Symbol.iterator] == "function" - ? (inputs = dj_songs) - : (inputs = [dj_songs]) + typeof dj_songs[Symbol.iterator] == "function" ? (inputs = dj_songs) : (inputs = [dj_songs]) for (const input of inputs) { if (input instanceof DJSong) { const song = input as DJSong @@ -65,16 +52,8 @@ class DJDB extends DB { 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!) - ) - ) + 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) { diff --git a/packages/dj/src/euterpe_extend.ts b/packages/dj/src/euterpe_extend.ts index 16f868b..461602f 100644 --- a/packages/dj/src/euterpe_extend.ts +++ b/packages/dj/src/euterpe_extend.ts @@ -11,22 +11,13 @@ class DJ { /**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 - ) { + 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)) + 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!!!! @@ -92,17 +83,10 @@ class Track { gain: GainNode audio_context: AudioContext | BaseAudioContext - constructor( - public player: MusicPlayer | Euterpe, - public current_song?: Song, - public should_loop?: boolean - ) { + constructor(public player: MusicPlayer | Euterpe, public current_song?: Song, public should_loop?: boolean) { this.audio_context = player.audio_context this.gain = this.audio_context.createGain() - if (current_song) - this.change_song(current_song).catch((e) => - console.error("error during track construction - " + e) - ) + if (current_song) this.change_song(current_song).catch((e) => console.error("error during track construction - " + e)) } async #prepare() { @@ -110,10 +94,7 @@ class Track { 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() - ) + this.audio_buffer = await this.audio_context.decodeAudioData(await file.arrayBuffer()) resolve(this) }, (reason) => reject(reason) @@ -121,10 +102,7 @@ class Track { }) } #connect() { - if (!this.audio_buffer) - throw new Error( - "Somehow buffer not in track even though it analyzed properly. Report this as a bug" - ) + if (!this.audio_buffer) throw new Error("Somehow buffer not in track even though it analyzed properly. Report this as a bug") this.buffer_source = this.audio_context.createBufferSource() this.buffer_source.buffer = this.audio_buffer! this.buffer_source.connect(this.gain) @@ -149,11 +127,8 @@ class Track { */ 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) - ) + 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/euterpe-web-test/src/db.ts b/packages/euterpe-web-test/src/db.ts index 9eca910..049c436 100644 --- a/packages/euterpe-web-test/src/db.ts +++ b/packages/euterpe-web-test/src/db.ts @@ -1,11 +1,4 @@ -import { - DB, - Song, - Artist, - Ref, - RefTo, - Platforms -} from "@euterpe.js/music-library" +import { DB, Song, Artist, Ref, RefTo, Platforms } from "@euterpe.js/music-library" export const db = new DB() db.add([ @@ -28,14 +21,7 @@ db.add([ }), new Artist({ name: "IMANU", - links: [ - [ - Platforms.Spotify, - new URL( - "https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1" - ) - ] - ] + links: [[Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]] }), new Artist({ name: "toe", @@ -45,33 +31,18 @@ db.add([ 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) - ], + 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" - ) + 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! - ) - ], + 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" - ) + url: new URL("http://" + window.location.host + "/Jamie xx - Sleep Sound.mp3") }), new Song({ artists: [new Ref(RefTo.Artists, 1)], @@ -83,10 +54,6 @@ db.add([ artists: [new Ref(RefTo.Artists, 10)], duration: 4 * 60 + 5, name: "サニーボーイ・ラプソディ", - url: new URL( - "http://" + - window.location.host + - "/16.サニーボーイ・ラプソディ.ogg" - ) + 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 cecfec4..ef4fffe 100644 --- a/packages/euterpe-web-test/src/main.ts +++ b/packages/euterpe-web-test/src/main.ts @@ -3,16 +3,12 @@ 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() + document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() }, (e) => console.log(e + " Failed to preload") ) @@ -46,8 +42,7 @@ euterpe.on_time_tick((time) => { document.querySelector("#previous")?.addEventListener("click", () => { euterpe.try_previous_song_looping().then( () => { - document.querySelector("#text-playing")!.innerHTML = - euterpe.format_current_song() + document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() }, (e) => alert(e + "Failed to change song") ) @@ -55,8 +50,7 @@ document.querySelector("#previous")?.addEventListener("click", () => { document.querySelector("#next")?.addEventListener("click", () => { euterpe.try_next_song_looping().then( () => { - document.querySelector("#text-playing")!.innerHTML = - euterpe.format_current_song() + document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song() }, (e) => alert(e + "Failed to change song") ) @@ -78,9 +72,7 @@ document.querySelector("#toggle-mute")?.addEventListener("click", () => { 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) @@ -119,9 +111,7 @@ function add_library_to_dom() { 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()), + () => (document.querySelector("#text-playing")!.innerHTML = euterpe.format_current_song()), (e) => alert(e) ) } diff --git a/packages/euterpe/src/index.ts b/packages/euterpe/src/index.ts index ddab7e6..4b6fb36 100644 --- a/packages/euterpe/src/index.ts +++ b/packages/euterpe/src/index.ts @@ -20,14 +20,15 @@ class Euterpe extends MusicPlayer { 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) + + audio_element.addEventListener("ended", () => { + audio_element.currentTime = 0 + audio_element.pause() + try { + this.try_next_song() + } catch (e) { } + }) } /** @@ -57,13 +58,10 @@ class Euterpe extends MusicPlayer { 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") + 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() + 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) @@ -87,9 +85,7 @@ class Euterpe extends MusicPlayer { 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() + 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) @@ -113,9 +109,7 @@ class Euterpe extends MusicPlayer { 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() + 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) @@ -137,9 +131,7 @@ class Euterpe extends MusicPlayer { 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() + 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) @@ -168,9 +160,7 @@ class Euterpe extends MusicPlayer { 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() + 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) @@ -192,13 +182,10 @@ class Euterpe extends MusicPlayer { 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") + 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() + 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) @@ -219,13 +206,10 @@ class Euterpe extends MusicPlayer { 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! + 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() + 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) @@ -246,13 +230,10 @@ class Euterpe extends MusicPlayer { 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") + 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() + 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) @@ -273,13 +254,10 @@ class Euterpe extends MusicPlayer { 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! + 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() + 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) @@ -341,8 +319,7 @@ class Euterpe extends MusicPlayer { 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`) + if (this.queue.find((song) => song.id == id)) throw new Error(`Song of id "${id}" already queued`) this.queue.push(curr_song) } /** @@ -391,18 +368,12 @@ class EuterpeBuilder { * 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") + 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.#track = this.#audio_context.createMediaElementSource(audio_element) this.#gain = this.#audio_context.createGain() } /** @@ -411,9 +382,7 @@ class EuterpeBuilder { */ add_analyser() { const analyser = this.#audio_context.createAnalyser() - !this.#prev_node - ? this.#track.connect(analyser) - : this.#prev_node.connect(analyser) + !this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser) this.#prev_node = analyser return analyser } @@ -423,9 +392,7 @@ class EuterpeBuilder { */ 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 ? this.#track.connect(panner) : this.#prev_node.connect(panner) this.#prev_node = panner return panner } @@ -435,9 +402,7 @@ class EuterpeBuilder { */ 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 ? this.#track.connect(shaper) : this.#prev_node.connect(shaper) this.#prev_node = shaper return shaper } @@ -445,9 +410,7 @@ class EuterpeBuilder { * 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 ? this.#track.connect(node) : this.#prev_node.connect(node) this.#prev_node = node } /** @@ -455,9 +418,7 @@ class EuterpeBuilder { * 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.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) this.#prev_node = this.#gain this.#is_gain_connected = true } @@ -467,21 +428,10 @@ class EuterpeBuilder { */ build() { if (!this.#is_gain_connected) { - !this.#prev_node - ? this.#track.connect(this.#gain) - : this.#prev_node.connect(this.#gain) + !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) this.#prev_node = this.#gain } this.#prev_node.connect(this.#audio_context.destination) - return new Euterpe( - this.db, - this.#audio_context, - this.audio_element, - this.#track, - this.#gain, - this.#volume, - undefined, - this.options - ) + return new Euterpe(this.db, this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume, undefined, this.options) } } diff --git a/packages/music-library-web-test/src/db.ts b/packages/music-library-web-test/src/db.ts index 9dcf1b7..b864e79 100644 --- a/packages/music-library-web-test/src/db.ts +++ b/packages/music-library-web-test/src/db.ts @@ -1,11 +1,4 @@ -import { - DB, - Artist, - Song, - RefTo, - Ref, - Platforms -} from "@euterpe.js/music-library" +import { DB, Artist, Song, RefTo, Ref, Platforms } from "@euterpe.js/music-library" export const db = new DB() db.add([ @@ -28,39 +21,21 @@ db.add([ }), new Artist({ name: "IMANU", - links: [ - [ - Platforms.Spotify, - new URL( - "https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1" - ) - ] - ] + links: [[Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")]] }) ]) db.add([ 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) - ], + 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" - ) + 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! - ) - ], + 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") diff --git a/packages/music-library-web-test/src/main.ts b/packages/music-library-web-test/src/main.ts index 576e493..9ae4bc3 100644 --- a/packages/music-library-web-test/src/main.ts +++ b/packages/music-library-web-test/src/main.ts @@ -18,36 +18,32 @@ music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then( 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) - } - ) + 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) - } - ) + 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", () => { @@ -67,19 +63,15 @@ music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then( 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("#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) }) @@ -100,8 +92,7 @@ music_player.try_new_song_async(db.songs[curr_song_id].url.pathname).then( // 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() + document.querySelector("#seek").max = "" + music_player.get_current_duration() }) music_player.subscribe_to_formatted_current_time_tick((time) => { document.querySelector("#current").innerHTML = time @@ -133,9 +124,7 @@ function change_current_song_text(db: DB) { 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]] - ) + console.log("found a link! " + Platforms[curr_artist.links[0][0]]) const url = curr_artist.links[0][1] final_text += `${curr_artist.name}, ` diff --git a/packages/music-library/src/index.ts b/packages/music-library/src/index.ts index c0441bf..83dd037 100644 --- a/packages/music-library/src/index.ts +++ b/packages/music-library/src/index.ts @@ -1,14 +1,4 @@ -export { - RefTo, - Ref, - Song, - Collection, - DB, - Artist, - Platforms, - CollectionType, - from_json -} +export { RefTo, Ref, Song, Collection, DB, Artist, Platforms, CollectionType, from_json } type ID = number enum RefTo { Artists, @@ -179,9 +169,7 @@ class DB { add(artist: Artist[]): void add(collection: Collection[]): void add(mix: (Song | Artist | Collection)[]): void - add( - stuff: Artist[] | Collection[] | Song[] | (Song | Artist | Collection)[] - ) { + add(stuff: Artist[] | Collection[] | Song[] | (Song | Artist | Collection)[]) { /** All of this adds refrences to the other side of whatever is being added. * eg. adding song with refrence to artist, adds refrence of song to artist * and adds incremental ids @@ -217,9 +205,7 @@ class DB { } 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) - ) + curr_artist.collections.push(new Ref(RefTo.Collections, col.id)) } this.collections.push(col) } else if (input instanceof Song) { @@ -229,16 +215,8 @@ class DB { 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!) - ) - ) + 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) { @@ -258,20 +236,12 @@ class DB { this.artists.sort((a, b) => a.id! - b.id!) } } -function from_json(db_stringified: { - artists?: any - songs?: any - collections?: any -}): DB { +function from_json(db_stringified: { artists?: any; songs?: any; collections?: any }): DB { const db = new DB() 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.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 { @@ -280,8 +250,7 @@ function from_json(db_stringified: { console.log(e) } }) - if (artist.publish_date) - artist.publish_date = new Date(JSON.parse(artist.publish_date)) + 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) @@ -298,22 +267,16 @@ function from_json(db_stringified: { } 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) + 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)) + 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") } @@ -323,32 +286,18 @@ function from_json(db_stringified: { } 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 - ) + 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) - ) + 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) + if (collection.cover) collection.cover = new URL(collection.cover) } catch (e) { - console.error(e), - console.error("failed to parse collection cover url") + console.error(e), console.error("failed to parse collection cover url") } if (collection.id) collection.id = collection.id as ID db.collections.push(collection) diff --git a/packages/player-web-test/src/main.ts b/packages/player-web-test/src/main.ts index 5a6ad56..5f705b7 100644 --- a/packages/player-web-test/src/main.ts +++ b/packages/player-web-test/src/main.ts @@ -4,82 +4,63 @@ 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) - ) +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 cba671f..c19ff8c 100644 --- a/packages/player/src/index.ts +++ b/packages/player/src/index.ts @@ -29,28 +29,19 @@ class PubSub { 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 - ) + 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 - ) + 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 - ) + this.el_formatted_duration_tick.splice(this.el_formatted_duration_tick.indexOf(func), 1) } break } @@ -114,10 +105,10 @@ export class MusicPlayer { mute() { this.#volume_cache = this.gain.gain.value /* Gentler mute, doesn't pop - gain.gain.linearRampToValueAtTime( - 0, - audio_context.currentTime + 0.1 - );*/ + gain.gain.linearRampToValueAtTime( + 0, + audio_context.currentTime + 0.1 + );*/ this.volume = this.gain.gain.value = 0 } unmute() { @@ -128,18 +119,14 @@ export class MusicPlayer { } /** * 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) + * will not resume playback * @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 - ) + 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 } @@ -235,10 +222,7 @@ export class MusicPlayer { try { await this.audio_context.resume() } catch (e) { - console.log( - "loading new song - couldn't resume context before hand", - e - ) + console.log("loading new song - couldn't resume context before hand", e) } } return new Promise((resolve, reject) => { @@ -273,8 +257,7 @@ export class MusicPlayer { //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)) + if (typeof controller.signal.reason == "string") reject(new Error(controller.signal.reason)) resolve() }) this.is_playing = false @@ -341,24 +324,19 @@ export class MusicPlayer { 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) + 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 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) + 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) + if (this.#pub_sub.el_formatted_current_time_tick.length == 0) cancelAnimationFrame(request_id) this.#pub_sub.emit(SubscribeEvents.FormattedCurrentTimeTick, time) } /** @@ -373,10 +351,7 @@ export class MusicPlayer { * 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.#pub_sub.subscribe(SubscribeEvents.FormattedCurrentTimeTick, callback) this.#emit_time_fmt() } /** @@ -401,13 +376,11 @@ export class MusicPlayerBuilder { * 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") + 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.#track = this.#audio_context.createMediaElementSource(audio_element) this.#gain = this.#audio_context.createGain() } /** @@ -416,9 +389,7 @@ export class MusicPlayerBuilder { */ add_analyser() { const analyser = this.#audio_context.createAnalyser() - !this.#prev_node - ? this.#track.connect(analyser) - : this.#prev_node.connect(analyser) + !this.#prev_node ? this.#track.connect(analyser) : this.#prev_node.connect(analyser) this.#prev_node = analyser return analyser } @@ -428,9 +399,7 @@ export class MusicPlayerBuilder { */ 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 ? this.#track.connect(panner) : this.#prev_node.connect(panner) this.#prev_node = panner return panner } @@ -440,9 +409,7 @@ export class MusicPlayerBuilder { */ 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 ? this.#track.connect(shaper) : this.#prev_node.connect(shaper) this.#prev_node = shaper return shaper } @@ -450,9 +417,7 @@ export class MusicPlayerBuilder { * 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 ? this.#track.connect(node) : this.#prev_node.connect(node) this.#prev_node = node } /** @@ -460,9 +425,7 @@ export class MusicPlayerBuilder { * 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.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) this.#prev_node = this.#gain this.#is_gain_connected = true } @@ -472,18 +435,10 @@ export class MusicPlayerBuilder { */ build() { if (!this.#is_gain_connected) { - !this.#prev_node - ? this.#track.connect(this.#gain) - : this.#prev_node.connect(this.#gain) + !this.#prev_node ? this.#track.connect(this.#gain) : this.#prev_node.connect(this.#gain) this.#prev_node = this.#gain } this.#prev_node.connect(this.#audio_context.destination) - return new MusicPlayer( - this.#audio_context, - this.audio_element, - this.#track, - this.#gain, - this.#volume - ) + return new MusicPlayer(this.#audio_context, this.audio_element, this.#track, this.#gain, this.#volume) } } diff --git a/packages/preprocessor/src/crawler.mjs b/packages/preprocessor/src/crawler.mjs index 6fc09f6..c6f3cd9 100644 --- a/packages/preprocessor/src/crawler.mjs +++ b/packages/preprocessor/src/crawler.mjs @@ -1,15 +1,6 @@ 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 - } -) +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 6552e7a..8e6fda5 100644 --- a/packages/preprocessor/src/generate_db.ts +++ b/packages/preprocessor/src/generate_db.ts @@ -13,10 +13,7 @@ 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("\\") @@ -44,15 +41,11 @@ 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 d26ecfe..6fd22fd 100644 --- a/packages/preprocessor/src/main.ts +++ b/packages/preprocessor/src/main.ts @@ -1,12 +1,6 @@ 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 @@ -23,10 +17,7 @@ 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() @@ -50,11 +41,7 @@ document.getElementById("upload")!.addEventListener("change", (ev) => { 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 + if (song.fft_data[i] === null || song.fft_data[i] === undefined) song.fft_data[i] = -Infinity } } } @@ -68,26 +55,18 @@ async function svg() { return } console.log("Creating svgs...") - const canvas_wrapper = document.querySelector( - ".canvas-wrapper" - ) as HTMLElement + 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 + 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 - ) + 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, @@ -95,15 +74,11 @@ async function svg() { }) .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 - } - ) + 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 @@ -125,21 +100,13 @@ async function analyze(): Promise { 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 - }` - ) + 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 - ) { + for (let curSecond = 0; curSecond < song.duration; curSecond += song.duration / samplingRate) { console.log("working...") audioEl.currentTime = curSecond await audioEl.play() @@ -149,9 +116,7 @@ async function analyze(): Promise { FFTDataArray.forEach((element) => { volume += element }) - currentFFTData.push( - Math.round((volume / FFTDataArray.length) * 100) / 100 - ) + currentFFTData.push(Math.round((volume / FFTDataArray.length) * 100) / 100) } song.fft_data = currentFFTData console.log(song.fft_data) diff --git a/packages/preprocessor/src/media_process.js b/packages/preprocessor/src/media_process.js index b890b54..1af51c9 100644 --- a/packages/preprocessor/src/media_process.js +++ b/packages/preprocessor/src/media_process.js @@ -39,8 +39,7 @@ 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) @@ -59,11 +58,7 @@ 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) @@ -86,11 +81,7 @@ function generate_new_video_sizes_mp4( } }) } -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) @@ -129,21 +120,12 @@ for (let i = 0; i < dirs.length; i++) { 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]}`) } /* diff --git a/packages/visualizer-web-test/src/main.ts b/packages/visualizer-web-test/src/main.ts index 0368e21..b119c99 100644 --- a/packages/visualizer-web-test/src/main.ts +++ b/packages/visualizer-web-test/src/main.ts @@ -1,12 +1,6 @@ import { MusicPlayerBuilder } from "@euterpe.js/player" import { fft_data } from "./waveform_data" -import { - AudioVisualBuilder, - SmoothingAlgorythm, - ShapeType, - WaveformOrientation, - WaveformShape -} from "@euterpe.js/visualizer" +import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType, WaveformOrientation, WaveformShape } from "@euterpe.js/visualizer" const audio_el = document.querySelector("#audio") as HTMLAudioElement const music_player_builder = new MusicPlayerBuilder(audio_el) const trapnation_analyser_node = music_player_builder.add_analyser() @@ -14,19 +8,14 @@ const bar_analyser_node = music_player_builder.add_analyser() const music_player = music_player_builder.build() 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 -) +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 @@ -39,15 +28,9 @@ const trapnation_visual_builder = new AudioVisualBuilder( .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 = trapnation_visual_builder.build(ShapeType.Circle, false) -const bar_visual_builder = new AudioVisualBuilder( - bar_analyser_node, - document.querySelector("#bar-canvas") as SVGSVGElement -) +const bar_visual_builder = new AudioVisualBuilder(bar_analyser_node, document.querySelector("#bar-canvas") as SVGSVGElement) .set_fft_data_tresholds({ point_count_i: 50, fft_multiplier_i: 2, @@ -57,10 +40,7 @@ const bar_visual_builder = new AudioVisualBuilder( .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 -) +const waveform_visual_builder = new AudioVisualBuilder(bar_analyser_node, waveform_canvas) .set_fft_data_tresholds({ point_count_i: 100, fft_multiplier_i: 1, @@ -68,15 +48,11 @@ const waveform_visual_builder = new AudioVisualBuilder( }) .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 = 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() @@ -128,11 +104,7 @@ function convert_range(value: number, r1: number[], r2: number[]) { 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 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) }) @@ -143,86 +115,67 @@ waveform_canvas.addEventListener("mouseleave", (e) => { /* * 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) - ) +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 6c62797..6c8938c 100644 --- a/packages/visualizer-web-test/src/waveform_data.ts +++ b/packages/visualizer-web-test/src/waveform_data.ts @@ -1,17 +1,10 @@ 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 + -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 6e16ab3..0230e59 100644 --- a/packages/visualizer/src/index.ts +++ b/packages/visualizer/src/index.ts @@ -88,16 +88,9 @@ export class AudioVisual { } 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 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++) { @@ -117,27 +110,16 @@ export class AudioVisual { 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 [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] + 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] + x: arr[curPoint].x - pointDistance * perpendicular[0], + y: arr[curPoint].y - pointDistance * perpendicular[1] } }) } @@ -194,16 +176,7 @@ export class AudioVisual { 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) - ] + path += "C" + [cp1x.toFixed(2), cp1y.toFixed(2), cp2x.toFixed(2), cp2y.toFixed(2), x2.toFixed(2), y2.toFixed(2)] } return path } @@ -217,16 +190,10 @@ export class AudioVisual { 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 - ) + ? 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)*/, + x: this.#shape.points[i].x /** ((Math.max(FFTDataArray[i] + 100)) * 4)*/, y: this.#shape.points[i].y - mutator }) } @@ -234,30 +201,13 @@ export class AudioVisual { } 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 + 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.#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.#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 @@ -268,23 +218,12 @@ export class AudioVisual { break } case ShapeType.Waveform: { - if ( - this.#shape.waveform_options!.shape_type == - WaveformShape.LineLike - ) { + 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 - ) + 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 - ) { + if (this.#shape.waveform_options!.orientation == WaveformOrientation.Horizontal) { mutated_points.push({ x: this.#shape.points[i].x, y: this.#shape.points[i].y - mutator @@ -306,16 +245,8 @@ export class AudioVisual { } } 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 - ) { + 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 @@ -363,36 +294,26 @@ export class AudioVisual { 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)} ` + 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 ${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)} ` + 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)} ` + 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 += `L ${arr[i].x.toFixed(2)},${arr[i].y.toFixed(2)} ` } } } @@ -404,15 +325,9 @@ export class AudioVisual { 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[ + 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)} ` + ].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} ` @@ -432,10 +347,7 @@ export class AudioVisual { break } case SmoothingAlgorythm.CatmullRom: { - if ( - this.#shape.shape_type == ShapeType.Waveform && - this.#shape.symmetry == true - ) { + 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 }] @@ -500,10 +412,7 @@ export class AudioVisualBuilder { #from_fft_range #to_fft_range #point_count: number - constructor( - analyzer_node: AnalyserNode, - svg_injecting_element: SVGSVGElement - ) { + 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 @@ -515,10 +424,7 @@ export class AudioVisualBuilder { 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) - ) + 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. @@ -537,8 +443,7 @@ export class AudioVisualBuilder { * @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") + 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 @@ -571,9 +476,7 @@ export class AudioVisualBuilder { 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) - ), + 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 }) { @@ -614,11 +517,7 @@ export class AudioVisualBuilder { * @param shape_type Circle = 0; Line = 1; * @returns `new AudioVisual` */ - build( - shape_type: ShapeType, - symmetry: boolean, - waveform_options?: WaveformOptions - ) { + 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, @@ -632,11 +531,7 @@ export class AudioVisualBuilder { this.#point_count ) } - #create_shape( - shape_type: ShapeType, - symmetry: boolean, - waveform_options?: WaveformOptions - ): Shape { + #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) { @@ -657,20 +552,11 @@ export class AudioVisualBuilder { } case ShapeType.Circle: { const points = [] - const radius = - this.#canvas_height > this.#canvas_width - ? this.#canvas_height / 5 - : this.#canvas_width / 5 + 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 + 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 }) } @@ -683,16 +569,10 @@ export class AudioVisualBuilder { } 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!" - ) + 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 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 @@ -702,16 +582,10 @@ export class AudioVisualBuilder { } 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 }) + 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 }) + waveform_options.orientation == WaveformOrientation.Horizontal ? points.push({ x: x, y: y }) : points.push({ x: y, y: x }) } } new_shape = { @@ -731,16 +605,9 @@ export class AudioVisualBuilder { 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 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++) { diff --git a/tools/scripts/publish.mjs b/tools/scripts/publish.mjs index 352ddb1..95ae7ae 100644 --- a/tools/scripts/publish.mjs +++ b/tools/scripts/publish.mjs @@ -35,16 +35,10 @@ invariant( const graph = readCachedProjectGraph() const project = graph.nodes[name] -invariant( - project, - `Could not find project "${name}" in the workspace. Is the project.json configured correctly?` -) +invariant(project, `Could not find project "${name}" in the workspace. Is the project.json configured correctly?`) const outputPath = project.data?.targets?.build?.options?.outputPath -invariant( - outputPath, - `Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?` -) +invariant(outputPath, `Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?`) process.chdir(outputPath) @@ -54,11 +48,7 @@ try { json.version = version writeFileSync(`package.json`, JSON.stringify(json, null, 2)) } catch (e) { - console.error( - chalk.bold.red( - `Error reading package.json file from library build output.` - ) - ) + console.error(chalk.bold.red(`Error reading package.json file from library build output.`)) } // Execute "npm publish" to publish -- 2.45.2 From eb4e4f91d54c1cbfd50051d09810fbf68ffa2363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djk=C3=A1=C5=A5o?= Date: Tue, 13 Feb 2024 16:17:20 +0100 Subject: [PATCH 4/4] fixed all demos, readmes, formatting, some bugs --- README.md | 23 +-- packages/euterpe-web-test/src/main.ts | 47 +++-- packages/euterpe/README.md | 164 ++++++++++++++- packages/euterpe/package.json | 64 +++--- packages/euterpe/src/index.ts | 87 +++++--- packages/music-library-web-test/src/main.ts | 193 +++++++++--------- packages/music-library/README.md | 10 +- packages/music-library/package.json | 64 +++--- packages/player-web-test/src/main.ts | 129 ++++++------ packages/player/README.md | 36 ++-- packages/player/package.json | 4 +- packages/player/src/index.ts | 15 +- packages/visualizer-web-test/src/main.ts | 160 ++++++++------- packages/visualizer-web-test/src/styles.css | 6 +- .../visualizer-web-test/src/waveform_data.ts | 1 + packages/visualizer/README.md | 29 ++- packages/visualizer/package.json | 4 +- packages/visualizer/src/index.ts | 47 ++--- 18 files changed, 652 insertions(+), 431 deletions(-) mode change 100644 => 120000 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 80fe6d8..0000000 --- a/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Euterpe - -The sole mono-repo for all things Euterpe. - -## Understand this workspace - -Run `npx nx graph` to see a diagram of the dependencies of the projects. -All programs go like this: `npx nx {package script} {package}` - -## Test this workspace - -Run `npx nx server player-web-test` to see the music player in a minimal demo. -Run `npx nx server visualizer-web-test` to see the music player in a minimal demo. - -## Build - -Run `npx nx build player` to build the player. -Run `npx nx build visualizer` to build the player. - -## Publish - -First build, then run `npm run publish-[package]`(scripts) \ No newline at end of file diff --git a/README.md b/README.md new file mode 120000 index 0000000..90b0e84 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +./packages/euterpe/README.md \ No newline at end of file diff --git a/packages/euterpe-web-test/src/main.ts b/packages/euterpe-web-test/src/main.ts index ef4fffe..b9d641a 100644 --- a/packages/euterpe-web-test/src/main.ts +++ b/packages/euterpe-web-test/src/main.ts @@ -3,16 +3,13 @@ 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, { + auto_play_after_seek: true, + auto_play_after_changing_song: true, + use_only_pathname_url: true +}).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") -) - document.querySelector("#seek")?.addEventListener("mouseup", (e) => { try { euterpe.try_seek(e.target?.valueAsNumber) @@ -23,7 +20,7 @@ document.querySelector("#seek")?.addEventListener("mouseup", (e) => { is_seeking = false }) -// Subscriptions to AudioContext changes, eg. time.. +// Subscriptions to song and AudioContext changes, eg. time, name.. euterpe.on_duration_formatted((time) => { document.querySelector("#duration")!.innerHTML = time document.querySelector("#seek")!.max = "" + euterpe.current_song_duration @@ -32,6 +29,7 @@ euterpe.on_duration_formatted((time) => { euterpe.on_time_tick_formatted((time) => { document.querySelector("#current")!.innerHTML = time }) + euterpe.on_time_tick((time) => { if (is_seeking) return document.querySelector("#seek")!.value = "" + time @@ -39,44 +37,49 @@ euterpe.on_time_tick((time) => { 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.on_song_change((_, song_name) => { + document.querySelector("#text-playing")!.innerHTML = song_name }) + +//preload after setting all listeners to make sure you capture the song update! +euterpe.try_preload_song(0).catch((e) => console.log(e + " Failed to preload")) + +document.querySelector("#previous")?.addEventListener("click", () => { + euterpe.try_previous_song_looping().catch((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().catch((e) => alert(e + "Failed to change song")) }) document.querySelector("#play")?.addEventListener("click", () => { euterpe.try_play().catch((e) => alert("Failed to play, " + e)) }) + document.querySelector("#pause")?.addEventListener("click", () => { euterpe.pause() }) + document.querySelector("#mute")?.addEventListener("click", () => { euterpe.mute() }) + document.querySelector("#unmute")?.addEventListener("click", () => { euterpe.unmute() }) + document.querySelector("#toggle-mute")?.addEventListener("click", () => { euterpe.mute_toggle() }) + document.querySelector("#toggle-play")?.addEventListener("click", () => { euterpe.try_play_toggle().catch((e) => alert("failed to toggle pause/play!" + e)) }) + document.querySelector("#volume")?.addEventListener("input", (e) => { euterpe.change_volume(e.target?.valueAsNumber) }) + //disables time updates so the time slider doesn't slip away from user document.querySelector("#seek")?.addEventListener("mousedown", () => { is_seeking = true diff --git a/packages/euterpe/README.md b/packages/euterpe/README.md index 31725b2..ca189ef 100644 --- a/packages/euterpe/README.md +++ b/packages/euterpe/README.md @@ -2,13 +2,16 @@ Fully featured AudioContext music player for the web. +## Euterpe in production: +- Hypertrance ( [site](https://hypertrance.eu/), [repository](https://github.com/nuphory/hypertrance.eu) ) + Features: - - "Local" library/Database for songs, collections etc. - - Queue - - History + - "Local" library/database for songs, collections, artists, waveforms, artist links and much more! + - Queue and history - Easy way to create Vector based audio visuals + - Library automatization based on folder/file structure, preprocessing and encoding media files for all platforms - Safe. Provides wrappers for all functions that are either unsafe or don't give a success return. (very Rust inspired, yes.) - - Async / Await or simple funcions. + - Frontend library agnostic ## How to use: @@ -19,4 +22,155 @@ Since this package is just a compilation of our smaller modules, you can read in - [Euterpe Visualizer](https://www.npmjs.com/package/@euterpe.js/visualizer) - [Euterpe Music Library](https://www.npmjs.com/package/@euterpe.js/music-library) -This module builds on those, and further adds functions for playing backwards, forwards and managing the queue. \ No newline at end of file + You can further check out how to automate database creation from folder structure, auto encode media for all platforms and create waveform svgs for songs here: +- [Euterpe Preprocessor](https://www.npmjs.com/package/@euterpe.js/preprocessor) + +This module builds on those, and further adds functions for playing backwards, forwards and managing the queue. + +First we create a database with our songs + +`db.ts` +```ts +import { DB, Song, Artist, Ref, RefTo, Platforms } from "@euterpe.js/music-library" +export const db = new DB + +db.add([ + //The IDs are added incrementally & are 0 based., so first artists ID added is 0, next 1 etc... + //You can specify the ID manually if you want + new Artist({ + name: "Machinedrum", + }), + new Artist({ + name: "Tanerélle", + }), + new Artist({ + name: "Mono/Poly", + }), + new Artist({ + name: "IMANU", + links: [ + [Platforms.Spotify, new URL("https://open.spotify.com/artist/5Y7rFm0tiJTVDzGLMzz0W1?si=DRaZyugTTIqlBHDkMGKVqA&nd=1")] + ] + }), +]) +db.add([ + new Song({ + //Refrences are constructed as such. This allows to get to the artist from either collection or song + artists: [new Ref(RefTo.Artists, 2), new Ref(RefTo.Artists, 3), new Ref(RefTo.Artists, 4)], + duration: 252, + name: "Star", + remix_artists: [new Ref(RefTo.Artists, 5)], + url: new URL("http://" + window.location.host + "/Machinedrum, Tanerelle & Mono Poly - Star (IMANU Remix) final.mp3") + }), +]) + +``` + +Then we build our Euterpe player and assign the db to it. Then it's just a matter of creating event listeners to the dom and binding them to Euterpes functions. + +`main.ts` +```ts +import { db } from "./db"; +import { EuterpeBuilder } from "@euterpe.js/euterpe" + +let is_seeking = false +const euterpe = new EuterpeBuilder(document.querySelector("#audio")!, db) + .build() + +document.querySelector("#seek")?.addEventListener("mouseup", (e) => { + try { + euterpe.try_seek(e.target?.valueAsNumber) + } catch { + alert("Failed seeking! " + e) + } + is_seeking = false +}) + +euterpe.on_song_change((_, song_name) => { + document.querySelector("#text-playing")!.innerHTML = song_name +}) + +document.querySelector("#previous")?.addEventListener("click", () => { + euterpe.try_previous_song_looping().catch((e) => alert(e + "Failed to change song")) +}) + +document.querySelector("#next")?.addEventListener("click", () => { + euterpe.try_next_song_looping().catch((e) => alert(e + "Failed to change song")) +}) + +document.querySelector("#mute")?.addEventListener("click", () => { + euterpe.mute() +}) + +document.querySelector("#unmute")?.addEventListener("click", () => { + euterpe.unmute() +}) + +document.querySelector("#toggle-play")?.addEventListener("click", () => { + euterpe.try_play_toggle().catch((e) => alert("failed to toggle pause/play!" + e)) +}) + +document.querySelector("#volume")?.addEventListener("input", (e) => { + euterpe.change_volume(e.target?.valueAsNumber) +}) + +//disables time updates so the time slider doesn't slip away from user +document.querySelector("#seek")?.addEventListener("mousedown", () => { + is_seeking = true +}) + +``` + +Then we can set up listeners to Euterpes events to keep the UI up todate as well + +`main.ts` +```ts +//... +// Subscriptions to song and AudioContext changes, eg. time, name.. +euterpe.on_duration_formatted((time) => { + document.querySelector("#duration")!.innerHTML = time + document.querySelector("#seek")!.max = "" + euterpe.current_song_duration +}) + +euterpe.on_time_tick_formatted((time) => { + document.querySelector("#current")!.innerHTML = time +}) + +euterpe.on_time_tick((time) => { + if (is_seeking) return + document.querySelector("#seek")!.value = "" + time + dev_queue_update() + dev_history_update() +}) + +euterpe.on_song_change((_, song_name) => { + document.querySelector("#text-playing")!.innerHTML = song_name +}) + +//preload after setting all listeners to make sure you capture the song update! +euterpe.try_preload_song(0).catch((e) => console.log(e + " Failed to preload")) + +//.. +function dev_queue_update() { + const p = document.querySelector("#queue-info") as HTMLParagraphElement + const dev_arr = [] + for (const song of euterpe.queue) { + dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`) + } + p.innerHTML = dev_arr.toString() +} + +function dev_history_update() { + const p = document.querySelector("#history-info") as HTMLParagraphElement + const dev_arr = [] + for (const song of euterpe.played_history) { + dev_arr.push(`Name: ${song.name}, ID: ${song.id} |`) + } + p.innerHTML = dev_arr.toString() +} + +``` +and it's done! +For vizualizer demo, or how to use the core parts of the Euterpe libraries separately, check out the individual repos readmes. + + diff --git a/packages/euterpe/package.json b/packages/euterpe/package.json index a72127a..81c4173 100644 --- a/packages/euterpe/package.json +++ b/packages/euterpe/package.json @@ -1,33 +1,33 @@ { - "name": "@euterpe.js/euterpe", - "version": "2.0.1", - "type": "module", - "description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!", - "main": "./src/index.js", - "author": { - "name": "Djkáťo", - "email": "djkatovfx@gmail.com" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/euterpe-js/euterpe-source.git" - }, - "homepage": "https://github.com/euterpe-js/euterpe-source/tree/master/packages/euterpe#readme", - "keywords": [ - "audio", - "library", - "music-database", - "audio-player", - "webaudio", - "database", - "db" - ], - "exports": { - ".": { - "types": "./src/index.d.ts", - "import": "./src/index.js", - "require": "./src/lib/euterpe.js" - } - } -} \ No newline at end of file + "name": "@euterpe.js/euterpe", + "version": "2.1.0", + "type": "module", + "description": "Fully featured solution for playing music on the web. Support for local library, audio visuals and more!", + "main": "./src/index.js", + "author": { + "name": "Djkáťo", + "email": "djkatovfx@gmail.com" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/euterpe-js/euterpe-source.git" + }, + "homepage": "https://github.com/euterpe-js/euterpe-source/tree/master/packages/euterpe#readme", + "keywords": [ + "audio", + "library", + "music-database", + "audio-player", + "webaudio", + "database", + "db" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./src/index.js", + "require": "./src/lib/euterpe.js" + } + } +} diff --git a/packages/euterpe/src/index.ts b/packages/euterpe/src/index.ts index 4b6fb36..3e0a398 100644 --- a/packages/euterpe/src/index.ts +++ b/packages/euterpe/src/index.ts @@ -10,6 +10,7 @@ class Euterpe extends MusicPlayer { current_song_id = 0 queue: Song[] = [] played_history: Song[] = [] + #sub_list: Array<(song_id: number, song_name: string) => void> = [] constructor( public db: DB, audio_context: AudioContext, @@ -22,15 +23,34 @@ class Euterpe extends MusicPlayer { ) { super(audio_context, audio_element, track, gain, volume, current_song_path) - audio_element.addEventListener("ended", () => { - audio_element.currentTime = 0 - audio_element.pause() + audio_element.addEventListener("ended", async () => { try { - this.try_next_song() - } catch (e) { } + await this.try_next_song() + } catch (e) { + audio_element.currentTime = 0 + this.pause() + } }) } + /** + * Unsafe, throws error if failed. Use try_seek or seek unless you don't care about the result. + */ + override seek(new_time: number) { + super.seek(new_time) + if (this.options?.auto_play_after_seek) this.play() + } + + /** + * Safer seek. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed. + * won't resume playback by default unless `EuterpeBuilderOptions.auto_play_after_seek == true` + * @throws if "Can't seek - Audiocontext is not running" + */ + override async try_seek(new_time: number) { + await super.try_seek(new_time) + if (this.options?.auto_play_after_seek) this.play() + } + /** * Use to load song on page load. * @throws if song with ID doesn't exist @@ -39,8 +59,9 @@ class Euterpe extends MusicPlayer { 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 + this.#emit_on_song_change() + await this.try_new_song(next.url.pathname) } } @@ -63,10 +84,11 @@ class Euterpe extends MusicPlayer { } 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.options?.auto_play_after_changing_song) 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! + this.#emit_on_song_change() } /** @@ -87,14 +109,15 @@ class Euterpe extends MusicPlayer { } 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.options?.auto_play_after_changing_song) 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! + this.#emit_on_song_change() } /** - * Won't tell you if the playback was successsful & wil loop back if already on last song. Best use try_next_song_async() + * Won't tell you if the playback was successsful & will loop back if already on last song. Best use try_next_song() * If queue present, uses that, if not, relies on Song ID directly from DB */ next_song_looping() { @@ -111,14 +134,15 @@ class Euterpe extends MusicPlayer { } const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() this.new_song(url) - this.play() + if (this.options?.auto_play_after_changing_song) this.play() if (this.current_song) this.played_history.push(this.current_song) this.current_song = new_song this.current_song_id = new_song.id! + this.#emit_on_song_change() } /** - * 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() + * 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() * If queue present, uses that, if not, relies on Song ID directly from DB */ next_song() { @@ -133,39 +157,42 @@ class Euterpe extends MusicPlayer { } const url = this.options?.use_only_pathname_url ? new_song.url.pathname : new_song.url.toString() this.new_song(url) - this.play() + if (this.options?.auto_play_after_changing_song) this.play() if (this.current_song) this.played_history.push(this.current_song) this.current_song = new_song this.current_song_id = new_song.id! + this.#emit_on_song_change() } /** - * Uses safer try_play_async. Normal play / play_async will try to start the player even if the track hasn't started yet, or was previously suspended/closed + * Uses safer try_play. Normal play / play will try to start the player even if the track hasn't started yet, or was previously suspended/closed */ 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.options?.auto_play_after_changing_song) 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! + this.#emit_on_song_change() } } /** - * 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() + * Won't throw an error if new ID not found. Won't tell you if the play was successful, best use specific_song() or try_specific_song() */ 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.options?.auto_play_after_changing_song) this.play() if (this.current_song) this.played_history.push(this.current_song) this.current_song = new_song this.current_song_id = new_song.id! + this.#emit_on_song_change() } /** @@ -187,10 +214,10 @@ class Euterpe extends MusicPlayer { } 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) + if (this.options?.auto_play_after_changing_song) await this.try_play() this.current_song = new_song this.current_song_id = new_song.id! + this.#emit_on_song_change() } /** @@ -211,10 +238,10 @@ class Euterpe extends MusicPlayer { } 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) + if (this.options?.auto_play_after_changing_song) await this.try_play() this.current_song = new_song this.current_song_id = new_song.id! + this.#emit_on_song_change() } /** @@ -235,9 +262,9 @@ class Euterpe extends MusicPlayer { } 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) + if (this.options?.auto_play_after_changing_song) this.play() this.current_song_id = new_song.id! + this.#emit_on_song_change() this.current_song = new_song } @@ -259,10 +286,10 @@ class Euterpe extends MusicPlayer { } 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) + if (this.options?.auto_play_after_changing_song) this.play() this.current_song_id = new_song.id! this.current_song = new_song + this.#emit_on_song_change() } /** @@ -350,9 +377,21 @@ class Euterpe extends MusicPlayer { if (i == -1) return return this.queue.splice(i, 1) } + + on_song_change(callback: (song_id: number, song_name: string) => void) { + this.#sub_list.push(callback) + } + + #emit_on_song_change() { + for (const func of this.#sub_list) { + func(this.current_song_id, this.format_current_song(this.current_song_id)) + } + } } type BuilderOptions = { use_only_pathname_url?: boolean + auto_play_after_seek?: boolean + auto_play_after_changing_song?: boolean } class EuterpeBuilder { diff --git a/packages/music-library-web-test/src/main.ts b/packages/music-library-web-test/src/main.ts index 9ae4bc3..a3f1ca6 100644 --- a/packages/music-library-web-test/src/main.ts +++ b/packages/music-library-web-test/src/main.ts @@ -10,100 +10,109 @@ music_player.change_volume(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) +let is_seeking = false - 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("#previous")?.addEventListener("click", () => { + curr_song_id-- + if (curr_song_id < 0) curr_song_id = 2 + music_player.try_new_song(db.songs[curr_song_id].url.pathname).then( + () => { + change_current_song_text(db) + music_player.try_play().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("#next")?.addEventListener("click", () => { + curr_song_id++ + if (curr_song_id > 2) curr_song_id = 0 + music_player.try_new_song(db.songs[curr_song_id].url.pathname).then( + () => { + change_current_song_text(db) + music_player.try_play().catch((err) => { + console.log(err) + }) + }, + (e) => { + console.log(e) + } + ) +}) + +document.querySelector("#play")?.addEventListener("click", () => { + music_player.try_play().then( + () => { + console.log("Playing!") + }, + (e) => alert("Failed to play, " + e) + ) +}) + +document.querySelector("#pause")?.addEventListener("click", () => { + music_player.pause() +}) + +document.querySelector("#mute")?.addEventListener("click", () => { + music_player.mute() +}) + +document.querySelector("#unmute")?.addEventListener("click", () => { + music_player.unmute() +}) + +document.querySelector("#toggle-mute")?.addEventListener("click", () => { + music_player.mute_toggle() +}) + +document.querySelector("#toggle-play")?.addEventListener("click", () => { + music_player.try_play_toggle().then( + (s) => console.log("toggled play/pause"), + (e) => alert("failed to toggle pause/play!" + e) + ) +}) + +document.querySelector("#volume")?.addEventListener("input", (e) => { + music_player.change_volume(e.target?.valueAsNumber) +}) + +document.querySelector("#seek")?.addEventListener("mousedown", (e) => { + is_seeking = true +}) + +document.querySelector("#seek")?.addEventListener("mouseup", (e) => { + music_player.try_seek(e.target?.valueAsNumber).then( + () => { + console.log("seeked to " + e.target?.valueAsNumber) + }, + () => { + alert("Failed seeking! " + e) + } + ) + is_seeking = false +}) + +// Subscriptions to AudioContext changes, eg. time.. +music_player.on_duration_formatted((time) => { + document.querySelector("#duration")!.innerHTML = time + document.querySelector("#seek")!.max = "" + music_player.current_song_duration +}) + +music_player.on_time_tick_formatted((time) => { + document.querySelector("#current")!.innerHTML = time +}) + +music_player.on_time_tick((time) => { + if (is_seeking) return + document.querySelector("#seek")!.value = "" + time +}) + +music_player.try_new_song(db.songs[curr_song_id].url.pathname).then(() => {}) +change_current_song_text(db) function change_current_song_text(db: DB) { const curr_song = db.songs[curr_song_id] diff --git a/packages/music-library/README.md b/packages/music-library/README.md index 00537cb..2e40d00 100644 --- a/packages/music-library/README.md +++ b/packages/music-library/README.md @@ -62,17 +62,17 @@ let curr_song_id = 1; 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) => { + music_player.try_new_song(db.songs[curr_song_id].url.pathname).then((s) => { change_current_song_text(db) - music_player.play_async().catch((err) => { console.log(err) }) + music_player.try_play().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) => { + music_player.try_new_song(db.songs[curr_song_id].url.pathname).then((s) => { change_current_song_text(db) - music_player.play_async().catch((err) => { console.log(err) }) + music_player.try_play().catch((err) => { console.log(err) }) }, (e) => { console.log(e) }) }) ``` @@ -151,4 +151,4 @@ class Collection { publish_date?: Date id?: ID } -``` \ No newline at end of file +``` diff --git a/packages/music-library/package.json b/packages/music-library/package.json index 50cbf7a..f1fc313 100644 --- a/packages/music-library/package.json +++ b/packages/music-library/package.json @@ -1,33 +1,33 @@ { - "name": "@euterpe.js/music-library", - "version": "1.0.11", - "type": "module", - "description": "A simple music library, acting as a Local DB as JS Object. Contains everything a person would need to store their music data for website playback.", - "main": "./src/index.js", - "author": { - "name": "Djkáťo", - "email": "djkatovfx@gmail.com" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/euterpe-js/euterpe-source.git" - }, - "homepage": "https://github.com/euterpe-js/euterpe-source/tree/master/packages/music-library#readme", - "keywords": [ - "audio", - "library", - "music-database", - "audio-player", - "webaudio", - "database", - "db" - ], - "exports": { - ".": { - "types": "./src/index.d.ts", - "import": "./src/index.js", - "require": "./src/lib/music-library.js" - } - } -} \ No newline at end of file + "name": "@euterpe.js/music-library", + "version": "1.0.12", + "type": "module", + "description": "A simple music library, acting as a Local DB as JS Object. Contains everything a person would need to store their music data for website playback.", + "main": "./src/index.js", + "author": { + "name": "Djkáťo", + "email": "djkatovfx@gmail.com" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/euterpe-js/euterpe-source.git" + }, + "homepage": "https://github.com/euterpe-js/euterpe-source/tree/master/packages/music-library#readme", + "keywords": [ + "audio", + "library", + "music-database", + "audio-player", + "webaudio", + "database", + "db" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./src/index.js", + "require": "./src/lib/music-library.js" + } + } +} diff --git a/packages/player-web-test/src/main.ts b/packages/player-web-test/src/main.ts index 5f705b7..7fc3e30 100644 --- a/packages/player-web-test/src/main.ts +++ b/packages/player-web-test/src/main.ts @@ -4,63 +4,72 @@ 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) -) +let is_seeking = false + +document.querySelector("#play")?.addEventListener("click", () => { + //const analyser_node = music_player_builder.add_analyser() + music_player.try_play().then( + () => { + console.log("Playing!") + }, + (e) => alert("Failed to play, " + e) + ) +}) + +document.querySelector("#pause")?.addEventListener("click", () => { + music_player.pause() +}) + +document.querySelector("#mute")?.addEventListener("click", () => { + music_player.mute() +}) + +document.querySelector("#unmute")?.addEventListener("click", () => { + music_player.unmute() +}) + +document.querySelector("#toggle-mute")?.addEventListener("click", () => { + music_player.mute_toggle() +}) + +document.querySelector("#toggle-play")?.addEventListener("click", () => { + music_player.try_play_toggle().then( + (s) => console.log("toggled play/pause"), + (e) => alert("failed to toggle pause/play!" + e) + ) +}) + +document.querySelector("#volume")?.addEventListener("input", (e) => { + music_player.change_volume(e.target?.valueAsNumber) +}) + +document.querySelector("#seek")?.addEventListener("mousedown", (e) => { + is_seeking = true +}) + +document.querySelector("#seek")?.addEventListener("mouseup", (e) => { + try { + music_player.try_seek(e.target?.valueAsNumber) + console.log("seeked to " + e.target?.valueAsNumber) + } catch (e) { + alert("Failed seeking! " + e) + } + is_seeking = false +}) + +// Subscriptions to AudioContext changes, eg. time.. +music_player.on_duration_formatted((time) => { + document.querySelector("#duration")!.innerHTML = time + document.querySelector("#seek")!.max = "" + music_player.current_song_duration +}) + +music_player.on_time_tick_formatted((time) => { + document.querySelector("#current")!.innerHTML = time +}) + +music_player.on_time_tick((time) => { + if (is_seeking) return + document.querySelector("#seek")!.value = "" + time +}) + +music_player.try_new_song(encodeURI("http://" + window.location.host + "/nuphory - NVISION (EXTENDED MIX).ogg")) diff --git a/packages/player/README.md b/packages/player/README.md index 472dcf4..a84f09c 100644 --- a/packages/player/README.md +++ b/packages/player/README.md @@ -5,13 +5,12 @@ A simple, safe AudioContext web music player. ##### Full demo at [github link](https://github.com/euterpe-js/euterpe-source/tree/master/packages/player-web-test) -Euterpe player is very Rust inspired, meaning there's always a safer function. `play()`,`play_async()` and `try_play_async()` for example. The goal is to provide the developer with knowledge of what happened when they ran the function, so they can decide what to do if it failed or succeeded. -```js +All we need to do is import the player builder and build it +```ts import { MusicPlayerBuilder } from "@euterpe/player"; const audio_el = document.querySelector("#audio") const music_player_builder = MusicPlayerBuilder(audio_el) -music_player_builder.start() // Builder allows for attaching custom nodes if necessary, eg. const panning_node = music_player_builder.add_stereo_panner_node() @@ -22,24 +21,24 @@ waves_shaper_node.oversample = '4x' const music_player = music_player_builder.build() //Next we add a song URL to the Audio Element, -music_player.try_new_song_async(encodeURI("my_song.ogg")) - .then(() => { - //and wait for the user input to resume the AudioContext - document.querySelector("#play")?.addEventListener("click", () => { - music_player.play_async() - .then( - //Easily follow up with what to do next - () => { console.log("Playing!") }, - (e) => alert("Failed to play, " + e) - ) - }) - }) +music_player.try_new_song(encodeURI("my_song.ogg")) +//and wait for the user input to resume the AudioContext +document.querySelector("#play")?.addEventListener("click", () => { + music_player.try_play() + .then( + //Easily follow up with what to do next + () => { console.log("Playing!") }, + (e) => alert("Failed to play, " + e) + ) +}) ``` + It's quite easy to give user the control in UI -```js + +```ts // Play when user clicks a document.querySelector("#play-button")?.addEventListener("click", () => { - music_player.play_async() + music_player.try_play() .then(() => { console.log("Playing!") }, (e) => alert("Failed to play, " + e)) }) // Mute when user clicks another @@ -53,7 +52,8 @@ document.querySelector("#volume")?.addEventListener("input", (e) => { ``` Euterpe Player also provides functions to easily track the status of playback. It does this via Subscription/Publisher pattern which publishes every frame ( Using `requestAnimationFrame()`). This allows for always up todate values reflecting on the UI. -```js + +```ts // Subscriptions to AudioContext changes, eg. time.. music_player.on_duration_formatted((time) => { //time == "4:53, "15:59", "1756:15:59"... diff --git a/packages/player/package.json b/packages/player/package.json index 51b67ef..83784fe 100644 --- a/packages/player/package.json +++ b/packages/player/package.json @@ -1,6 +1,6 @@ { "name": "@euterpe.js/player", - "version": "2.0.1", + "version": "2.1.0", "type": "module", "description": "A simple, safe AudioContext web music player", "main": "./src/index.js", @@ -29,4 +29,4 @@ "require": "./src/lib/player.js" } } -} \ No newline at end of file +} diff --git a/packages/player/src/index.ts b/packages/player/src/index.ts index c19ff8c..daf137e 100644 --- a/packages/player/src/index.ts +++ b/packages/player/src/index.ts @@ -118,7 +118,7 @@ export class MusicPlayer { 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. + * Safer seek. Normal seek will try to start the player even if the track hasn't started yet, or was previously suspended/closed. * will not resume playback * @throws if "Can't seek - Audiocontext is not running" */ @@ -131,14 +131,14 @@ export class MusicPlayer { } /** - * Unsafe, throws error if failed. Use try_seek_async or seek_async unless you don't care about the result. + * Unsafe, throws error if failed. Use try_seek or seek 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 + * Safer play_toggle. Normal play_toggle will try to start the player even if the track hasn't started yet, or was previously suspended/closed * @throws Error if playback failed */ async try_play_toggle() { @@ -196,7 +196,7 @@ export class MusicPlayer { } /** - * Unsafe, can just fail. Use play_async or try_play_async unless you don't care about the result. + * Unsafe, can just fail. Use play or try_play unless you don't care about the result. */ play() { if (this.is_playing) return @@ -214,7 +214,7 @@ export class MusicPlayer { } /** - * Will only load metadata of the upcoming song. Need to call try_play_async() afterwards to start the playback + * Will only load metadata of the upcoming song and change audio dom elements url. Need to call try_play() afterwards to start the playback * @throws Error if adding element throwed Error or Stalled */ async try_new_song(path: string) { @@ -255,16 +255,17 @@ export class MusicPlayer { ) //once aborted, try to set current_song_duration - controller.signal.addEventListener("abort", (r) => { + controller.signal.addEventListener("abort", () => { 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 + * Won't tell if you if the song actually got loaded or if it failed. For a safer version use try_new_song() unless you don't care about the result */ new_song(path: string) { this.audio_element.src = this.current_song_path = path diff --git a/packages/visualizer-web-test/src/main.ts b/packages/visualizer-web-test/src/main.ts index b119c99..f95aed7 100644 --- a/packages/visualizer-web-test/src/main.ts +++ b/packages/visualizer-web-test/src/main.ts @@ -1,6 +1,7 @@ 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() @@ -12,6 +13,7 @@ const waveform_canvas = document.querySelector("#waveform-canvas") as SVGSVGElem 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 */ @@ -26,28 +28,32 @@ const trapnation_visual_builder = new AudioVisualBuilder(trapnation_analyser_nod 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_size(4096) .set_fft_data_tresholds({ - point_count_i: 50, - fft_multiplier_i: 2, - fft_offset_i: -100 + point_count_i: 20, + fft_multiplier_i: 3, + fft_offset_i: -130 }) - .set_fft_time_smoothing(0.8) + .set_fft_time_smoothing(0.5) .set_smoothing_algorythm(SmoothingAlgorythm.BezierPerpendicular) + +//currently broken, duno why... const bar_visual = bar_visual_builder.build(ShapeType.Line, false) const 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 + fft_offset_i: -50 }) .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, @@ -98,9 +104,11 @@ waveform_canvas.appendChild(seek_clip_use) 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] } + waveform_canvas.addEventListener("mousemove", (e) => { const rect = e.target.getBoundingClientRect() const x = e.clientX - rect.left @@ -108,74 +116,86 @@ waveform_canvas.addEventListener("mousemove", (e) => { 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) }) + /* * 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) -) + +let is_seeking = false + +document.querySelector("#play")?.addEventListener("click", () => { + music_player.try_play().then( + () => { + console.log("Playing!") + }, + (e) => alert("Failed to play, " + e) + ) +}) + +document.querySelector("#pause")?.addEventListener("click", () => { + music_player.pause() +}) + +document.querySelector("#mute")?.addEventListener("click", () => { + music_player.mute() +}) + +document.querySelector("#unmute")?.addEventListener("click", () => { + music_player.unmute() +}) + +document.querySelector("#toggle-mute")?.addEventListener("click", () => { + music_player.mute_toggle() +}) + +document.querySelector("#toggle-play")?.addEventListener("click", () => { + music_player.try_play_toggle().then( + (s) => console.log("toggled play/pause"), + (e) => alert("failed to toggle pause/play!" + e) + ) +}) + +document.querySelector("#volume")?.addEventListener("input", (e) => { + music_player.change_volume(e.target?.valueAsNumber) +}) + +document.querySelector("#seek")?.addEventListener("mousedown", (e) => { + is_seeking = true +}) + +document.querySelector("#seek")?.addEventListener("mouseup", (e) => { + music_player.try_seek(e.target?.valueAsNumber).then( + () => { + console.log("seeked to " + e.target?.valueAsNumber) + }, + () => { + alert("Failed seeking! " + e) + } + ) + is_seeking = false +}) + +// Subscriptions to AudioContext changes, eg. time.. +music_player.on_duration_formatted((time) => { + duration_element.innerHTML = time + seek_element.max = "" + music_player.current_song_duration +}) + +music_player.on_time_tick_formatted((time) => { + current_time_element.innerHTML = time +}) + +music_player.on_time_tick((time) => { + if (is_seeking) return + seek_element.value = "" + time + const x = `${(time / music_player.current_song_duration) * 100}%` + const polygon = `polygon(0 0, ${x} 0, ${x} 100%, 0 100%)` + document.documentElement.style.setProperty("--clip-time-path", polygon) +}) + +music_player.try_new_song(encodeURI("http://localhost:4200/nuphory - NVISION (EXTENDED MIX).ogg")) diff --git a/packages/visualizer-web-test/src/styles.css b/packages/visualizer-web-test/src/styles.css index d2e7b7a..98a9f7a 100644 --- a/packages/visualizer-web-test/src/styles.css +++ b/packages/visualizer-web-test/src/styles.css @@ -31,9 +31,9 @@ body { } .waveform-time { clip-path: var(--clip-time-path); - fill: rgba(50,145,200,0.3); + fill: rgba(0,255,50,0.5); } .waveform-seek { clip-path: var(--clip-seek-path); - fill: rgba(50,145,200,0.3); -} \ No newline at end of file + fill: rgba(0,50,255,0.5); +} diff --git a/packages/visualizer-web-test/src/waveform_data.ts b/packages/visualizer-web-test/src/waveform_data.ts index 6c8938c..6afb772 100644 --- a/packages/visualizer-web-test/src/waveform_data.ts +++ b/packages/visualizer-web-test/src/waveform_data.ts @@ -1,3 +1,4 @@ +//To see how to get these, check the preprocessor repo! 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, diff --git a/packages/visualizer/README.md b/packages/visualizer/README.md index e6e061c..1acf4df 100644 --- a/packages/visualizer/README.md +++ b/packages/visualizer/README.md @@ -5,7 +5,8 @@ Euterpe Visualizer is a unique audio visual library for the web. Using SVG eleme ### How to use This library relies on AudioContext API, especially on AnalyserNode. To use the Visualizer without our player, first we need to create an AudioContext, from which we can create and connect an AnalyserNode: -```js + +```ts import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer" // Refrence to an element inside your HTML const audio_element = document.querySelector("#audio") as HTMLAudioElement @@ -15,8 +16,10 @@ const track = audio_context.createMediaElementSource(audio_element) const analyzer = audio_context.createAnalyser() track.connect(analyzer).connect(audio_context.destination) ``` + Now that the AudioContext is ready, we start constructing our Visualizer -```js + +```ts // For more options during the building process, I recommend reading the docs /* Refrence an * "" @@ -25,11 +28,14 @@ Now that the AudioContext is ready, we start constructing our Visualizer const visualizer = new AudioVisualBuilder(analyser_node, document.querySelector("#canvas") as SVGSVGElement) .build(ShapeType.Circle) ``` + And it's ready! Once the audio context starts playing, the visualizer should start generating the path inside our SVG element. -```js + +```ts visualizer.draw() audio_element.play() ``` + This is a minimal setup, and more options during the build process are recommended. Especially the `.set_fft_data_tresholds()` method, as each setting affects another. There's really no scientific method to this for now unfortunately, so some trial and error will be necessary. #### How to use with Euterpe Player @@ -37,7 +43,8 @@ This is a minimal setup, and more options during the build process are recommend ##### Full demo on how to use together with Euterpe player at [github](https://github.com/euterpe-js/euterpe-source/tree/master/packages/visualizer-web-test). First we need to create our [Euterpe Player](https://www.npmjs.com/package/@euterpe.js/player) -```js + +```ts import { MusicPlayerBuilder } from "@euterpe/player"; import { AudioVisualBuilder, SmoothingAlgorythm, ShapeType } from "@euterpe/visualizer" @@ -49,8 +56,9 @@ const analyzer_node = music_player_builder.add_analyser() const music_player = music_player_builder.build() ``` + Now that the AudioContext is ready, we start constructing our Visualizer -```js +```ts // For more options during the building process, I recommend reading the docs /* Refrence an * "" @@ -62,19 +70,20 @@ const visual_builder = new AudioVisualBuilder(analyzer_node, document.querySelec //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 visualizer = visual_builder.build(ShapeType.Circle) ``` + And it's ready! Once the audio context starts playing, the visualizer should start generating the path inside our SVG element. -```js + +```ts visualizer.draw() -music_player.try_new_song_async(url) - .then( music_player.play_async() +music_player.try_new_song(url) + .then( music_player.try_play() .then( console.log("It's working!") ) ) -``` \ No newline at end of file +``` diff --git a/packages/visualizer/package.json b/packages/visualizer/package.json index cf5189e..1787bf9 100644 --- a/packages/visualizer/package.json +++ b/packages/visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@euterpe.js/visualizer", - "version": "1.1.0", + "version": "1.1.1", "type": "module", "description": "Music visualizer based on SVG and AudioContext", "main": "./src/index.js", @@ -27,4 +27,4 @@ "require": "./src/lib/visualizer.js" } } -} \ No newline at end of file +} diff --git a/packages/visualizer/src/index.ts b/packages/visualizer/src/index.ts index 0230e59..ab7e6e0 100644 --- a/packages/visualizer/src/index.ts +++ b/packages/visualizer/src/index.ts @@ -9,9 +9,9 @@ export enum ShapeType { Line, Waveform /*To be Implmeneted - Custom, - FullSongWaveForm - */ + Custom, + FullSongWaveForm + */ } export enum WaveformOrientation { Vertical, @@ -211,8 +211,8 @@ export class AudioVisual { 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})` - */ + this.injectingHTMLElement.parentElement.style.transform = `scale(${(100 + Math.max((frequencyData[2] * 2 + 130) / 5, 1)) / 100})` + */ } break @@ -296,10 +296,7 @@ export class AudioVisual { 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} ` - } + path += `L ${this.#canvas_width} ${this.#canvas_height} ` break } case ShapeType.Circle: { @@ -339,10 +336,10 @@ export class AudioVisual { 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]}` - }*/ + 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 } @@ -488,19 +485,19 @@ export class AudioVisualBuilder { return this } /** - * Defines what the svg d attribute path command will use. - * - * `Linear` - Uses no interpolation between points, jagged but very fast - * - * `BezierPerpendicular` - Sets the Cubic Bézier anchors perpendicular to the point. Great for Line shapes with no curves. - * - * `CatmullRom` - Uses Centripetal Catmull–Rom spline under the hood, then translates them to Cubic Bézier points. Best quality, worst performance. - * - * `BezierWeighted` - DO NOT USE! It's broken at the moment :/. Sets the Cubic Bézier anchors halfway between the next and previous point. Better than Linear on Circular shapes and doesn't have the "invards bulding" side effect of Catmull Rom + * Defines what the svg d attribute path command will use. + * + * `Linear` - Uses no interpolation between points, jagged but very fast + * + * `BezierPerpendicular` - Sets the Cubic Bézier anchors perpendicular to the point. Great for Line shapes with no curves. + * + * `CatmullRom` - Uses Centripetal Catmull–Rom spline under the hood, then translates them to Cubic Bézier points. Best quality, worst performance. + * + * `BezierWeighted` - DO NOT USE! It's broken at the moment :/. Sets the Cubic Bézier anchors halfway between the next and previous point. Better than Linear on Circular shapes and doesn't have the "invards bulding" side effect of Catmull Rom - * @param algorythm Linear = 0; BezierPerpendicular = 1; CatmullRom = 2; BezierWeighted = 3; - * @returns - */ + * @param algorythm Linear = 0; BezierPerpendicular = 1; CatmullRom = 2; BezierWeighted = 3; + * @returns + */ set_smoothing_algorythm(algorythm: SmoothingAlgorythm) { this.#smoothing_algorythm = algorythm return this -- 2.45.2