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:
Harrison Deng 2022-04-15 00:11:46 -05:00
parent 77f309e6f5
commit 507de1f3c0
13 changed files with 7021 additions and 149 deletions

35
.eslintrc.json Normal file
View 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
View File

@ -0,0 +1,5 @@
{
"cSpell.words": [
"linebreak"
]
}

6711
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,6 @@
export default class PlayButton {
constructor() {
}
}

View File

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

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