Init monorepo n move player to it, add test

This commit is contained in:
Djkato 2023-05-21 23:10:32 +02:00
parent b3c176946c
commit 5de339ff47
30 changed files with 5929 additions and 30 deletions

View file

@ -3,8 +3,8 @@ root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
indent_style = tab
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
node_modules

56
.eslintrc.json Normal file
View file

@ -0,0 +1,56 @@
{
"root": true,
"ignorePatterns": [
"**/*"
],
"plugins": [
"@nx"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": [
"*"
]
}
]
}
]
}
},
{
"files": [
"*.ts",
"*.tsx"
],
"extends": [
"plugin:@nx/typescript"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"extends": [
"plugin:@nx/javascript"
],
"rules": {}
}
]
}

2
.gitignore vendored
View file

@ -7,7 +7,7 @@ tmp
# dependencies
node_modules
package-lock.json
# IDEs and editors
/.idea
.project

View file

@ -1,3 +1,12 @@
{
"singleQuote": true
}
"parser": "typescript",
"trailingComma": "none",
"useTabs": true,
"tabWidth": 4,
"semi": false,
"singleQuote": false,
"endOfLine": "lf",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always"
}

View file

@ -1,3 +1,7 @@
{
"recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode"]
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
}

View file

@ -1,17 +1,20 @@
# Euterpe
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
**This workspace has been generated by [Nx, a Smart, fast and extensible build system.](https://nx.dev)**
The sole mono-repo for all things Euterpe.
## Understand this workspace
Run `nx graph` to see a diagram of the dependencies of the projects.
Run `npx nx graph` to see a diagram of the dependencies of the projects.
All programs go like this: `npx nx {package script} {package}`
## Remote caching
## Test this workspace
Run `npx nx connect-to-nx-cloud` to enable [remote caching](https://nx.app) and make CI faster.
Run `npx nx server player-web-test` to see the music player in a minimal demo.
## Further help
## Build
Visit the [Nx Documentation](https://nx.dev) to learn more.
Run `npx nx build player` to build the player.
## Publish
First build, then `npm publish --access=public`

5
babel.config.json Normal file
View file

@ -0,0 +1,5 @@
{
"babelrcRoots": [
"*"
]
}

59
nx.json
View file

@ -7,23 +7,70 @@
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"]
"cacheableOperations": [
"build",
"lint",
"test",
"e2e"
]
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"]
"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"],
"sharedGlobals": []
"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"
}
}
}

4764
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,36 @@
{
"name": "@euterpe/source",
"name": "@euterpe.js/source",
"version": "0.0.0",
"license": "MIT",
"scripts": {},
"private": true,
"dependencies": {},
"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",
"@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",
"typescript": "~5.0.2"
"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"
}
}
}

View file

@ -0,0 +1,33 @@
{
"extends": [
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>PlayerWebTest</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div class="wrapper">
<audio src="" id="audio"></audio>
<button id="play">Play</button>
<button id="pause">Pause</button>
<button id="toggle-play">Toggle Pause/Play</button>
<p id="current">-:--</p>
<input type="range" min="0" max="10" value="0" id="seek" step="0.01">
<p id="duration">-:--</p>
<span>
<input type="range" min="0" max="1" value="1" id="volume" step="0.01">
</span>
<button id="mute">Mute</button>
<button id="unmute">Unmute</button>
<button id="toggle-mute">Toggle Mute</button>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -0,0 +1,59 @@
{
"name": "player-web-test",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "packages/player-web-test/src",
"tags": [],
"targets": {
"build": {
"executor": "@nx/vite:build",
"outputs": [
"{options.outputPath}"
],
"defaultConfiguration": "production",
"options": {
"outputPath": "dist/packages/player-web-test"
},
"configurations": {
"development": {
"mode": "development"
},
"production": {
"mode": "production"
}
}
},
"serve": {
"executor": "@nx/vite:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "player-web-test:build"
},
"configurations": {
"development": {
"buildTarget": "player-web-test:build:development",
"hmr": true
},
"production": {
"buildTarget": "player-web-test:build:production",
"hmr": false
}
}
},
"preview": {
"executor": "@nx/vite:preview-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "player-web-test:build"
},
"configurations": {
"development": {
"buildTarget": "player-web-test:build:development"
},
"production": {
"buildTarget": "player-web-test:build:production"
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,56 @@
import { MusicPlayerBuilder } from "@euterpe/player";
const audio_el = document.querySelector("#audio") as HTMLAudioElement
const music_player_builder = MusicPlayerBuilder(audio_el)
music_player_builder.start()
const music_player = music_player_builder.build()
music_player.change_volume(1)
music_player.try_new_song_async(encodeURI("http://127.0.0.1:4200/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.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))

View file

@ -0,0 +1,17 @@
#volume{
transform: rotate(270deg);
}
body {
width: 100vw;
height:100vh;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
}
.wrapper{
width:50vw;
display: flex;
justify-content: space-between;
}

View file

@ -0,0 +1,30 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ESNext",
"DOM"
],
"moduleResolution": "Node",
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"types": [
"vite/client",
"vitest"
]
},
"include": [
"src"
],
}

View file

@ -0,0 +1,42 @@
/// <reference types="vitest" />
import { defineConfig } from "vite"
import viteTsConfigPaths from "vite-tsconfig-paths"
export default defineConfig({
cacheDir: "../../node_modules/.vite/player-web-test",
server: {
port: 4200,
host: "localhost"
},
preview: {
port: 4300,
host: "localhost"
},
plugins: [
viteTsConfigPaths({
root: "../../"
})
],
// Uncomment this if you are using workers.
// worker: {
// plugins: [
// viteTsConfigPaths({
// root: '../../',
// }),
// ],
// },
test: {
globals: true,
cache: {
dir: "../../node_modules/.vitest"
},
environment: "jsdom",
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"]
}
})

View file

@ -0,0 +1,33 @@
{
"extends": [
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View file

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

View file

@ -0,0 +1,41 @@
{
"name": "player",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/player/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/packages/player",
"main": "packages/player/src/index.ts",
"tsConfig": "packages/player/tsconfig.lib.json",
"assets": []
}
},
"publish": {
"command": "node tools/scripts/publish.mjs player {args.ver} {args.tag}",
"dependsOn": [
"build"
]
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": [
"{options.outputFile}"
],
"options": {
"lintFilePatterns": [
"packages/player/**/*.ts"
]
}
}
},
"tags": [
"'audioContext",
""
]
}

View file

@ -0,0 +1,527 @@
export enum SubscribeEvents {
CurrentTimeTick,
FormattedDurationTick,
FormattedCurrentTimeTick,
}
const PubSub = () => {
//el = event listener
const el_current_time_tick: Array<(data: any) => void> = []
const el_formatted_duration_tick: Array<(data: any) => void> = []
const el_formatted_current_time_tick: Array<(data: any) => void> = []
function subscribe(event_name: SubscribeEvents, func: (data: any) => void) {
switch (event_name) {
case SubscribeEvents.CurrentTimeTick: {
el_current_time_tick.push(func)
break
}
case SubscribeEvents.FormattedDurationTick: {
el_formatted_duration_tick.push(func)
break
}
case SubscribeEvents.FormattedCurrentTimeTick: {
el_formatted_current_time_tick.push(func)
break
}
}
}
function unsubscribe(event_name: SubscribeEvents, func: (data: any) => void) {
switch (event_name) {
case SubscribeEvents.CurrentTimeTick: {
if (el_current_time_tick.includes(func)) {
el_current_time_tick.splice(el_current_time_tick.indexOf(func), 1)
}
break
}
case SubscribeEvents.FormattedDurationTick: {
if (el_formatted_duration_tick.includes(func)) {
el_formatted_duration_tick.splice(el_formatted_duration_tick.indexOf(func), 1)
}
break
}
case SubscribeEvents.FormattedCurrentTimeTick: {
if (el_formatted_duration_tick.includes(func)) {
el_formatted_duration_tick.splice(el_formatted_duration_tick.indexOf(func), 1)
}
break
}
}
}
function emit(event_name: SubscribeEvents, data: any) {
switch (event_name) {
case SubscribeEvents.CurrentTimeTick: {
el_current_time_tick.forEach((func) => {
func(data)
})
break
}
case SubscribeEvents.FormattedDurationTick: {
el_formatted_duration_tick.forEach((func) => {
func(data)
})
break
}
case SubscribeEvents.FormattedCurrentTimeTick: {
el_formatted_current_time_tick.forEach((func) => {
func(data)
})
break
}
}
}
return {
el_current_time_tick,
el_formatted_duration_tick,
el_formatted_current_time_tick,
subscribe,
unsubscribe,
emit
}
}
/* For old browsers */
declare global {
interface Window {
webkitAudioContext: typeof AudioContext
}
}
export const MusicPlayer = (audio_context_i: AudioContext, audio_element_i: HTMLAudioElement, track_i: MediaElementAudioSourceNode, gain_i: GainNode, volume_i: number, current_song_path_i?: string) => {
const audio_element: HTMLAudioElement = audio_element_i
const audio_context: AudioContext = audio_context_i
const track: MediaElementAudioSourceNode = track_i
const gain: GainNode = gain_i
let current_song_path: string | undefined = current_song_path_i
let current_song_duration: number
let volume_cache: number = volume_i
let volume: number = volume_i
let is_playing = false
let time = 0
const pub_sub = PubSub()
function mute_toggle() {
if (gain.gain.value == 0) {
unmute()
} else {
mute()
}
}
function mute() {
volume_cache = gain.gain.value
/* Gentler mute, doesn't pop
gain.gain.linearRampToValueAtTime(
0,
audio_context.currentTime + 0.1
);*/
volume = gain.gain.value = 0
}
function unmute() {
volume = gain.gain.value = volume_cache
}
function change_volume(volume_i: number) {
volume = 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
*/
function try_seek_async(new_time: number) {
return new Promise((resolve, reject) => {
if (track.context.state == "closed" || track.context.state == "suspended") {
is_playing = false
reject("Can't seek - track not playing")
}
audio_element.currentTime = new_time
resolve(null)
/*audio_element.play().then((s) => resolve(s), (r) => {
is_playing = false
reject(r)
})*/
})
}
/**
* Can try to seek even if the audio context was suspended or closed. Best to use try_seek_async()
*/
function seek_async(new_time: number) {
return new Promise((resolve, reject) => {
audio_element.currentTime = new_time
resolve(null)
/* audio_element.play().then((s) => resolve(s), (r) => {
is_playing = false
reject(r)
})*/
})
}
/**
* Unsafe, throws error if failed. Use try_seek_async or seek_async unless you don't care about the result.
*/
function seek(new_time: number) {
audio_element.currentTime = new_time
audio_element.play().catch((e) => { throw e })
}
/**
* 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
*/
function try_play_toggle_async() {
return new Promise((resolve, reject) => {
if (audio_context.state === "suspended" || audio_context.state === "closed") {
reject("Context closed or suspended")
}
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)
}
})
}
/**
* Can try to play even if the audio context was suspended or closed. Best to use try_play_toggle_async()
*/
function play_toggle_async() {
return new Promise((resolve, reject) => {
if (audio_context.state === "suspended" || audio_context.state === "closed") {
audio_context.resume()
}
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)
}
})
}
/**
* Unsafe, throws error if failed. Use play_toggle_async or try_play_toggle_async unless you don't care about the result.
*/
function play_toggle() {
if (audio_element.paused) {
is_playing = true
audio_element.play().catch((r) => {
is_playing = false
throw r
})
} else {
is_playing = false
audio_element.pause()
}
}
/**
* Safer play_async. Normal play will try to start the player even if the track hasn't started yet, or was previously suspended/closed
*/
function try_play_async() {
return new Promise((resolve, reject) => {
if (is_playing) reject(Error("Already playing"))
if (audio_context.state === "suspended" || audio_context.state === "closed") {
reject("Context closed or suspended")
}
audio_element.play().then((s) => {
is_playing = true
resolve(s)
}, (r) => {
is_playing = false
reject(r)
})
})
}
/**
* Will try to play even if the audio context was suspended or closed. Best to use try_play_async()
*/
function play_async() {
return new Promise((resolve, reject) => {
if (is_playing) resolve(null)
if (audio_context.state === "suspended" || audio_context.state === "closed") {
audio_context.resume()
}
audio_element.play().then((s) => {
is_playing = true
resolve(s)
}, (r) => {
is_playing = false
reject(r)
})
})
}
/**
* Unsafe, throws error if failed. Use play_async or try_play_async unless you don't care about the result.
*/
function play() {
if (is_playing) return
audio_element.play().catch((r) => {
is_playing = false
throw r
})
}
/**
* Safe technically. Even if audioContext is suspended or closed it will pretend that it paused.
*/
function pause() {
audio_element.pause()
is_playing = false
}
/**
* Will only load metadata of the upcoming song. Need to call try_play_async() afterwards to start the playback
*/
function try_new_song_async(path: string) {
return new Promise((resolve, reject) => {
audio_element.src = current_song_path = path
//Found out today about this. Such a nice new way to mass remove event listeners!
const controller = new AbortController();
audio_element.addEventListener("canplaythrough", function canplay_listener(s) {
//current_song_duration = audio_element.duration
controller.abort()
resolve(s)
}, { signal: controller.signal })
audio_element.addEventListener("error", function error_listener(e) {
controller.abort()
reject(e)
}, { signal: controller.signal })
audio_element.addEventListener("abort", function abort_listener(e) {
controller.abort()
reject(e)
}, { signal: controller.signal })
audio_element.addEventListener("stalled", function stalled_listener(e) {
controller.abort()
reject(e)
}, { signal: controller.signal })
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
*/
function new_song(path: string) {
audio_element.src = current_song_path = path
//current_song_duration = 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"
*/
function get_formatted_duration() {
const dur = audio_element.duration
current_song_duration = audio_element.duration
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;
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"
*/
function get_formatted_current_time() {
const curr = 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;
let ret = ""
if (hrs > 0) {
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
}
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
ret += "" + secs;
return ret;
}
/**
* Will give current time every animation frame
*/
function subscribe_to_time_tick(callback: (data: any) => void) {
pub_sub.subscribe(SubscribeEvents.CurrentTimeTick, callback)
emit_current_time()
}
function emit_current_time() {
const request_id = requestAnimationFrame(emit_current_time.bind(MusicPlayer))
if (audio_element.ended) is_playing = false
if (audio_element.paused) is_playing == false
// if use reactively changes volume directly
gain.gain.value = volume
time = audio_element.currentTime
if (pub_sub.el_current_time_tick.length == 0) cancelAnimationFrame(request_id)
pub_sub.emit(SubscribeEvents.CurrentTimeTick, time)
}
/**
* Will give formatted current time via get_formatted_current_time() every animation frame
*/
function subscribe_to_formatted_current_time_tick(callback: (data: any) => void) {
pub_sub.subscribe(SubscribeEvents.FormattedCurrentTimeTick, callback)
emit_formatted_current_time()
}
function emit_formatted_current_time() {
const request_id = requestAnimationFrame(emit_formatted_current_time.bind(MusicPlayer))
const time = get_formatted_current_time()
//if (pub_sub.el_formatted_current_time_tick.length == 0) cancelAnimationFrame(request_id)
pub_sub.emit(SubscribeEvents.FormattedCurrentTimeTick, time)
}
/**
* Will give formatted duration time via get_formatted_duration() every animation frame
*/
function subscribe_to_formatted_duration_time(callback: (data: any) => void) {
pub_sub.subscribe(SubscribeEvents.FormattedDurationTick, callback)
emit_formatted_duration_time()
}
function emit_formatted_duration_time() {
const request_id = requestAnimationFrame(emit_formatted_duration_time.bind(MusicPlayer))
const time = get_formatted_duration()
//if (pub_sub.el_formatted_duration_tick.length == 0) cancelAnimationFrame(request_id)
pub_sub.emit(SubscribeEvents.FormattedDurationTick, time)
}
return {
track,
get_is_playing: () => is_playing,
get_current_path: () => current_song_path,
get_current_duration: () => current_song_duration,
volume,
time,
mute,
unmute,
mute_toggle,
change_volume,
try_seek_async,
seek_async,
seek,
play,
pause,
play_toggle,
play_toggle_async,
try_play_toggle_async,
try_new_song_async,
new_song,
get_formatted_duration,
get_formatted_current_time,
subscribe_to_formatted_current_time_tick,
subscribe_to_formatted_duration_time,
subscribe_to_time_tick,
try_play_async,
play_async,
}
}
export function MusicPlayerBuilder(audio_element: HTMLAudioElement) {
let audio_context: AudioContext
let gain: GainNode
let track: MediaElementAudioSourceNode
const volume = 1
let prev_node: any;
let 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)
*/
function start() {
if (audio_element === undefined) throw Error("audio_element was undefined")
// ↓ For old browsers
const AudioContext = window.AudioContext || window.webkitAudioContext;
audio_context = new AudioContext()
track = audio_context.createMediaElementSource(audio_element)
gain = audio_context.createGain()
}
/**
* For external use, not kept inside player after connection.
* @returns {AnalyserNode}
*/
function add_analyser() {
const analyser = audio_context.createAnalyser()
!prev_node ? track.connect(analyser) : prev_node.connect(analyser)
prev_node = analyser
return analyser
}
/**
* For external use, not kept inside player after connection.
* @returns {StereoPannerNode}
*/
function add_stereo_panner_node() {
const panner = audio_context.createStereoPanner()
!prev_node ? track.connect(panner) : prev_node.connect(panner)
prev_node = panner
return panner
}
/**
* For external use, not kept inside player after connection.
* @returns {StereoPannerNode}
*/
function add_wave_shaper_node() {
const shaper = audio_context.createWaveShaper()
!prev_node ? track.connect(shaper) : prev_node.connect(shaper)
prev_node = shaper
return shaper
}
/**
* For additional trickery, you can connect your own node.
*/
function connect_custom_node(node: AudioNode) {
!prev_node ? track.connect(node) : prev_node.connect(node)
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
*/
function connect_gain() {
!prev_node ? track.connect(gain) : prev_node.connect(gain)
prev_node = gain
is_gain_connected = true
}
/**
* Finishes the build
* @returns {MusicPlayer: () => void}
*/
function build() {
if (!is_gain_connected) {
!prev_node ? track.connect(gain) : prev_node.connect(gain)
prev_node = gain
}
prev_node.connect(audio_context.destination)
audio_element.preload = "metadata"
return MusicPlayer(audio_context, audio_element, track, gain, volume)
}
return {
start,
add_analyser,
add_stereo_panner_node,
add_wave_shaper_node,
connect_gain,
connect_custom_node,
build
}
}

View file

@ -0,0 +1,3 @@
export function player(): string {
return "player"
}

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View file

@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": [
"node"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

65
tools/scripts/publish.mjs Normal file
View file

@ -0,0 +1,65 @@
/**
* This is a minimal script to publish your package to "npm".
* This is meant to be used as-is or customize as you see fit.
*
* This script is executed on "dist/path/to/library" as "cwd" by default.
*
* You might need to authenticate with NPM before running this script.
*/
import { execSync } from "child_process"
import { readFileSync, writeFileSync } from "fs"
import chalk from "chalk"
import devkit from "@nx/devkit"
const { readCachedProjectGraph } = devkit
function invariant(condition, message) {
if (!condition) {
console.error(chalk.bold.red(message))
process.exit(1)
}
}
// Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag}
// Default "tag" to "next" so we won't publish the "latest" tag by accident.
const [, , name, version, tag = "next"] = process.argv
// A simple SemVer validation to validate the version
const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/
invariant(
version && validVersion.test(version),
`No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got ${version}.`
)
const graph = readCachedProjectGraph()
const project = graph.nodes[name]
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?`
)
process.chdir(outputPath)
// Updating the version in "package.json" before publishing
try {
const json = JSON.parse(readFileSync(`package.json`).toString())
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.`
)
)
}
// Execute "npm publish" to publish
execSync(`npm publish --access public --tag ${tag}`)

View file

@ -10,11 +10,21 @@
"importHelpers": true,
"target": "es2015",
"module": "esnext",
"lib": ["es2020", "dom"],
"lib": [
"es2020",
"dom"
],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"baseUrl": ".",
"paths": {}
"paths": {
"@euterpe/player": [
"packages/player/src/index.ts"
]
}
},
"exclude": ["node_modules", "tmp"]
"exclude": [
"node_modules",
"tmp"
]
}