Completed untested versions of playlist and playlist song classes.
Restructured for improved development and deployment pipeline. Added webpack and configurations for development and production. Began adding JSDocs. Added eslint.
This commit is contained in:
parent
77f309e6f5
commit
507de1f3c0
35
.eslintrc.json
Normal file
35
.eslintrc.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:jsdoc/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"jsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"linebreak"
|
||||
]
|
||||
}
|
6711
server/package-lock.json
generated
6711
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,14 @@
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.17.3"
|
||||
"devDependencies": {
|
||||
"css-loader": "^6.7.1",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-plugin-jsdoc": "^39.2.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"webpack": "^5.72.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.8.1",
|
||||
"webpack-merge": "^5.8.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,52 @@
|
||||
// TODO: Reorganize such that audioshowkit.js uses VisualizerCore.js.
|
||||
export default class VisualizerCore {
|
||||
constructor(mediaSource, fftSize = 1024) {
|
||||
this._stream = mediaSource;
|
||||
this._analyzing = false;
|
||||
this._updateListeners = [];
|
||||
this._audioCtx = new window.AudioContext();
|
||||
if (mediaSource instanceof HTMLMediaElement) {
|
||||
this._source = this._audioCtx.createMediaElementSource(this._stream);
|
||||
} else {
|
||||
this._source = this._audioCtx.createMediaStreamSource(this._stream);
|
||||
}
|
||||
this._analyzer = this._audioCtx.createAnalyser();
|
||||
this._analyzer.fftSize = fftSize;
|
||||
this._source.connect(this._analyzer);
|
||||
this._analyzer.connect(this._audioCtx.destination);
|
||||
this._buffer = new Uint8Array(this._analyzer.frequencyBinCount);
|
||||
|
||||
this.lastUpdate = null;
|
||||
}
|
||||
|
||||
analyze() {
|
||||
if (this._analyzing) {
|
||||
return;
|
||||
}
|
||||
this._analyzing = true;
|
||||
let self = this; // since calling from requestAnimationFrame means "this" is no longer set to produced object.
|
||||
requestAnimationFrame((timestamp) => {
|
||||
if (!self._analyzing) return;
|
||||
|
||||
requestAnimationFrame(self.update);
|
||||
if (!self.lastUpdate) {
|
||||
self.lastUpdate = timestamp;
|
||||
}
|
||||
let delta = timestamp - self.lastUpdate;
|
||||
self._analyzer.getByteFrequencyData(self._buffer);
|
||||
|
||||
self._updateListeners.forEach(listener => {
|
||||
listener(delta, self._buffer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._analyzing = false;
|
||||
}
|
||||
addUpdateListener(listener) {
|
||||
this._updateListeners.push(listener);
|
||||
}
|
||||
getNumberOfBins() {
|
||||
return this._buffer.length;
|
||||
}
|
||||
}
|
||||
|
@ -1,54 +1,4 @@
|
||||
"use strict";
|
||||
|
||||
function VisualizerCore(mediaSource, fftSize = 1024) {
|
||||
this._stream = mediaSource;
|
||||
this._analyzing = false;
|
||||
this._updateListeners = [];
|
||||
this._audioCtx = new window.AudioContext();
|
||||
this._source = null; // Prepare for either a MediaStream or a MediaElement.
|
||||
try {
|
||||
this.source = this._audioCtx.createMediaStreamSource(this._stream);
|
||||
} catch (e) {
|
||||
this._source = this._audioCtx.createMediaElementSource(this._stream);
|
||||
}
|
||||
this._analyzer = this._audioCtx.createAnalyser();
|
||||
this._analyzer.fftSize = fftSize;
|
||||
this._source.connect(this._analyzer);
|
||||
this._analyzer.connect(this._audioCtx.destination);
|
||||
this._buffer = new Uint8Array(this._analyzer.frequencyBinCount);
|
||||
|
||||
this.lastUpdate = null;
|
||||
|
||||
this.analyze = function () {
|
||||
if (this._analyzing) {
|
||||
return;
|
||||
}
|
||||
this._analyzing = true;
|
||||
requestAnimationFrame(this.update);
|
||||
};
|
||||
|
||||
let vcore = this; // since calling from requestAnimationFrame means "this" is no longer set to produced object.
|
||||
this.update = function (timestamp) {
|
||||
if (!vcore._analyzing) return;
|
||||
|
||||
requestAnimationFrame(vcore.update);
|
||||
if (!vcore.lastUpdate) {
|
||||
vcore.lastUpdate = timestamp;
|
||||
}
|
||||
let delta = timestamp - vcore.lastUpdate;
|
||||
vcore._analyzer.getByteFrequencyData(vcore._buffer);
|
||||
|
||||
vcore._updateListeners.forEach(listener => {
|
||||
listener(delta, vcore._buffer);
|
||||
});
|
||||
};
|
||||
this.stop = function () {
|
||||
this._analyzing = false;
|
||||
};
|
||||
this.addUpdateListener = function (listener) {
|
||||
this._updateListeners.push(listener);
|
||||
};
|
||||
this.getNumberOfBins = function () {
|
||||
return this._buffer.length;
|
||||
};
|
||||
}
|
||||
// TODO: Detect annotated elements.
|
||||
// TODO: Set up global scoping.
|
6
src/player/PlayButton.js
Normal file
6
src/player/PlayButton.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default class PlayButton {
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
}
|
@ -1,26 +1,165 @@
|
||||
export default function PlaylistSong(url, name, author, playlist) {
|
||||
this._displayName = name;
|
||||
this._author = author;
|
||||
this._url = url;
|
||||
this._mediaStream = null;
|
||||
this._playlist = playlist;
|
||||
import SongPlaylist from "./SongPlaylist";
|
||||
|
||||
this.getAudio = function () {
|
||||
if (this.mediaStream) {
|
||||
return this.mediaStream;
|
||||
/**
|
||||
* A song with metadata that can be used as part of a {@link SongPlaylist}.
|
||||
*/
|
||||
export default class PlayListSong {
|
||||
|
||||
/**
|
||||
* Constructs a song for a {@link SongPlaylist}.
|
||||
*
|
||||
* @param {string} url the url to fetch the song from.
|
||||
* @param {string} name the name of the song.
|
||||
* @param {string} author the author of the song.
|
||||
* @param {SongPlaylist} playlist the {@link SongPlaylist} this song is part of.
|
||||
*/
|
||||
constructor(url, name, author, playlist) {
|
||||
this._displayName = name;
|
||||
this._author = author;
|
||||
this._url = url;
|
||||
this._audio = null;
|
||||
this._playlist = playlist;
|
||||
|
||||
/**
|
||||
* Whether or not this song is ready to be played.
|
||||
*/
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback AudioEventCallback
|
||||
* @param {HTMLAudioElement} audio
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AudioEventCallback} [onReady] called when the song is ready, including right away if the song is already ready.
|
||||
* @returns {HTMLAudioElement} The audio element that represents this song.
|
||||
*/
|
||||
getAudio(onReady) {
|
||||
if (this._audio) {
|
||||
if (this.ready) {
|
||||
if (onReady) onReady(this._audio);
|
||||
return this._audio;
|
||||
}
|
||||
return this._audio;
|
||||
}
|
||||
this.mediaStream = new Audio(url);
|
||||
};
|
||||
this.getDisplayName = function () {
|
||||
this._audio = new Audio(this._url);
|
||||
|
||||
this._audio.addEventListener("canplaythrough", () => {
|
||||
this.ready = true;
|
||||
if (onReady) {
|
||||
onReady(this._audio);
|
||||
}
|
||||
});
|
||||
return this._audio;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {string} representing the name of the song to be displayed.
|
||||
*/
|
||||
getDisplayName = function () {
|
||||
return this._displayName;
|
||||
};
|
||||
this.getAuthor = function () {
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {string} representing the author of the song.
|
||||
*/
|
||||
getAuthor = function () {
|
||||
return this._getAuthor;
|
||||
};
|
||||
this.getUrl = function () {
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {string} representing the url at which the file for this song can be found.
|
||||
*/
|
||||
getUrl = function () {
|
||||
return this._url;
|
||||
};
|
||||
this.getPlaylist = function () {
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {SongPlaylist} the playlist this song is part of.
|
||||
*/
|
||||
getPlaylist = function () {
|
||||
return this._playlist;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {boolean} true if and only if there is audio data that is either already loaded or is being loaded.
|
||||
*/
|
||||
audioInstantiated() {
|
||||
return this._audio ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins audio playback as soon as possible.
|
||||
*/
|
||||
play() {
|
||||
this.getAudio((audio) => {
|
||||
audio.play();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the audio playback, unless the audio data has yet to be instantiated.
|
||||
*/
|
||||
pause() {
|
||||
if (!this.audioInstantiated()) return;
|
||||
this.getAudio((audio) => {
|
||||
audio.pause();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {number} the volume on a scale of 0 to 1.
|
||||
*/
|
||||
getVolume() {
|
||||
return this.getAudio().volume;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} volume a normalized volume on a scale of 0 to 1.
|
||||
*/
|
||||
setVolume(volume) {
|
||||
this.getAudio().volume = volume;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @returns {number} the number of seconds into the song.
|
||||
*/
|
||||
getCurrentTime() {
|
||||
return this.getAudio().currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} currentTime the time position in the song to jump to in seconds.
|
||||
*/
|
||||
setCurrentTime(currentTime) {
|
||||
this.getAudio().currentTime = currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {number} the duration of the song.
|
||||
*/
|
||||
getDuration() {
|
||||
return this.getAudio().duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads the audio data.
|
||||
*/
|
||||
unloadAudio() {
|
||||
if (!this.audioInstantiated()) return;
|
||||
this._audio.pause();
|
||||
this._audio = null;
|
||||
this.ready = false;
|
||||
}
|
||||
}
|
78
src/player/SongPlaylist.js
Normal file
78
src/player/SongPlaylist.js
Normal file
@ -0,0 +1,78 @@
|
||||
import PlaylistSong from "./PlaylistSong";
|
||||
|
||||
/**
|
||||
* A playlist that holds a multitude of songs.
|
||||
*/
|
||||
export default class SongPlaylist {
|
||||
|
||||
/**
|
||||
* Instantiates a playlist for songs.
|
||||
*
|
||||
* @param {string} name The name of the playlist.
|
||||
*/
|
||||
constructor(name) {
|
||||
this._list = [];
|
||||
this._name = name;
|
||||
this._current = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {string} the name of the string.
|
||||
*/
|
||||
getName() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} index the index of the song to retrieve.
|
||||
* @returns {PlaylistSong} the song at the given index.
|
||||
*/
|
||||
songAtIndex(index) {
|
||||
if (index >= this._list.length) {
|
||||
return null;
|
||||
}
|
||||
return this.list[index];
|
||||
}
|
||||
|
||||
songsWithName(name) {
|
||||
return this._list.filter((item) => item.getDisplayName() == name);
|
||||
}
|
||||
|
||||
add(url, name, author) {
|
||||
this._list.push(new PlaylistSong(url, name, author, this, this._list.length));
|
||||
}
|
||||
|
||||
remove(index) {
|
||||
if (index >= this._list.length) {
|
||||
return null;
|
||||
}
|
||||
let removed = this._list.splice(index, 1);
|
||||
if (removed.length > 0) {
|
||||
return removed[0];
|
||||
}
|
||||
}
|
||||
|
||||
findSongIndex(name) {
|
||||
// TODO: Could probably be optimized.
|
||||
return this._list.findIndex((item) => item.getDisplayName() == name);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {number} total number of songs in this playlist.
|
||||
*/
|
||||
total() {
|
||||
return this._list.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads the audio data of all songs in this playlist.
|
||||
*/
|
||||
unloadAllAudio() {
|
||||
this._list.forEach(playlistSong => {
|
||||
playlistSong.unloadAudio();
|
||||
});
|
||||
}
|
||||
}
|
21
webpack.common.js
Normal file
21
webpack.common.js
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
entry: {
|
||||
audioshowkit: "./src/audioshowkit.js"
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.html$/i,
|
||||
loader: "html-loader",
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: "./dist"
|
||||
}
|
||||
};
|
13
webpack.dev.js
Normal file
13
webpack.dev.js
Normal file
@ -0,0 +1,13 @@
|
||||
import merge from "webpack-merge";
|
||||
import webpackCommon from "./webpack.common";
|
||||
|
||||
devConfig = {
|
||||
mode: "development",
|
||||
devtools: "inline-source-map",
|
||||
|
||||
devServer: {
|
||||
static: "./dist"
|
||||
}
|
||||
}
|
||||
|
||||
export default merge(webpackCommon, devConfig);
|
12
webpack.prod.js
Normal file
12
webpack.prod.js
Normal file
@ -0,0 +1,12 @@
|
||||
import merge from "webpack-merge"
|
||||
import webpackCommon from "./webpack.common"
|
||||
|
||||
prodConfig = {
|
||||
mode: "production",
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: "./public"
|
||||
}
|
||||
}
|
||||
|
||||
export default merge(webpackCommon, prodConfig);
|
Loading…
Reference in New Issue
Block a user