Refactored repo file structure.

This commit is contained in:
2022-04-17 21:54:43 -05:00
parent 8856980532
commit 0d77276572
33 changed files with 157 additions and 22 deletions

View File

@@ -1,13 +0,0 @@
// TODO: basic playlist display.
// TODO: Demo functions.
// TODO: Detect annotated elements.
// TODO: Create getting started.
import * as mappings from "./mapping/mappings.js";
import * as player from "./player/player.js";
import * as visualization from "./visualization/visualization.js";
import * as support from "./support/support.js";
export { mappings, player, visualization, support };

View File

@@ -1,77 +0,0 @@
import { mapBinNumerical, mapRangedAvgNumerical } from "./numeric.js";
/**
* Maps the width of an element to frequency bin(s).
*
* @param {object} conf The configuration for mapping amplitudes to a width CSS property.
* @param {HTMLElement} conf.element The html element to map the width to.
* @param {number} conf.growLower The lower limit of the width.
* @param {number} conf.growUpper The upper limit of the width.
* @param {string} conf.unit The unit the upper and lower bounds are measured in.
* @param {number} conf.lowerBin The lower boundary of bins to be mapped (or the single bin to be mapped if the upper bin is undefined).
* @param {VisualizerUpdateManager} conf.visUpdateManager The visualizer update manager to be mapped to.
* @param {interpolator} conf.interpolator The interpolation function to be used to transition from one value to the next.
* @param {number} [conf.upperBin] The upper bin or undefined, which results in mapping to a single bin.
* @param {boolean} [conf.reversed=false] If true, then high amplitudes are mapped to lower values and vice versa.
* @returns {{bin: number, listener: VisualizerUpdateManager.visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}} The listener that was added (ranged if an upper bound was provided, binned otherwise).
*/
export function mapWidth({ element, growLower, growUpper, unit, lowerBin, visUpdateManager, interpolator, upperBin = undefined, reversed = false }) {
const getter = () => element.style.width;
const setter = (value) => element.style.width = value + unit;
const conf = {
minVal: growLower,
maxVal: growUpper,
bin: lowerBin,
getter: getter,
setter: setter,
interpolator: interpolator,
visUpdateManager: visUpdateManager,
reversed: reversed
};
if (upperBin) {
conf.bin = undefined;
conf.lowerBin = lowerBin;
conf.upperBin = upperBin;
return mapRangedAvgNumerical(conf);
}
return mapBinNumerical(conf);
}
/**
* Maps the height of an element to frequency bin(s).
*
* @param {object} conf The configuration for mapping amplitudes to a height CSS property.
* @param {HTMLElement} conf.element The html element to map the height to.
* @param {number} conf.growLower The lower limit of the height.
* @param {number} conf.growUpper The upper limit of the height.
* @param {string} conf.unit The unit the upper and lower bounds are measured in.
* @param {number} conf.lowerBin The lower boundary of bins to be mapped (or the single bin to be mapped if the upper bin is undefined).
* @param {VisualizerUpdateManager} conf.visUpdateManager The visualizer update manager to be mapped to.
* @param {interpolator} conf.interpolator The interpolation function to be used to transition from one value to the next.
* @param {number} [conf.upperBin] The upper bin or undefined, which results in mapping to a single bin.
* @param {boolean} [conf.reversed=false] If true, then high amplitudes are mapped to lower values and vice versa.
* @returns {{bin: number, listener: VisualizerUpdateManager.visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}} The listener that was added (ranged if an upper bound was provided, binned otherwise).
*/
export function mapHeight({ element, growLower, growUpper, unit, lowerBin, visUpdateManager, interpolator, upperBin = undefined, reversed = false }) {
const getter = () => element.style.height;
const setter = (value) => element.style.height = value + unit;
const conf = {
minVal: growLower,
maxVal: growUpper,
bin: lowerBin,
getter: getter,
setter: setter,
interpolator: interpolator,
visUpdateManager: visUpdateManager,
reversed: reversed
};
if (upperBin) {
conf.bin = undefined;
conf.lowerBin = lowerBin;
conf.upperBin = upperBin;
return mapRangedAvgNumerical(conf);
}
return mapBinNumerical(conf);
}

View File

@@ -1,4 +0,0 @@
import * as dimensions from "./dimensions.js";
import * as numeric from "./numeric.js";
export { dimensions, numeric };

View File

@@ -1,84 +0,0 @@
// TODO: add for width
// TODO: add for height
// TODO: add for background
/**
*
* @callback numericalGetter
* @returns {number} The number this value currently represents.
*/
/**
* @callback numericalSetter
* @param {number} value The new numerical value to update to.
*/
/**
* Maps the average of a range of bins to numerical value defined by a getter and a setter.
*
* @param {object} conf A configuration for how to map a numerical value.
* @param {number} conf.minVal The minimum value of the numerical value.
* @param {number} conf.maxVal The maximum value of the numerical value.
* @param {number} conf.lowerBin The lower bin of the range of bins this value is to be mapped to.
* @param {number} conf.upperBin The upper bin of the range of bins this value si to be mapped to.
* @param {numericalGetter} conf.getter The getter callback to be used to get the current number.
* @param {numericalSetter} conf.setter The setter callback to be used to set the new number.
* @param {interpolator} conf.interpolator The interpolation function to use.
* @param {VisualizerUpdateManager} conf.visUpdateManager the visualizer update manager this mapping corresponds with.
* @param {boolean} [conf.reversed = false] If true, then high amplitudes will be mapped to low values and vice versa.
* @returns {{lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}} An object containing the lower and upper bounds for the range of a listener, which is also in the object.
*/
export function mapRangedAvgNumerical({ minVal, maxVal, lowerBin = undefined, upperBin, getter, setter, interpolator, visUpdateManager, reversed = false }) {
const rangedListener = {
lower: lowerBin,
upper: upperBin,
listener: (timeDelta, bins) => {
const normalBins = [...bins];
// TODO: Future: add weighting / distribution system?
let average = 0;
for (let i = 0; i < normalBins.length; i++) {
normalBins[i] = normalBins[i] / 255.0;
average += normalBins[i];
}
average /= normalBins.length;
const range = maxVal - minVal;
let interpolated = interpolator(getter(), average, timeDelta); // TODO: May be optimized by allowing for transferring of previous style state.
if (reversed) interpolated = 1 - interpolated;
setter(minVal + range * interpolated);
}
};
if (visUpdateManager.addVisualizerRangedUpdateListener(rangedListener)) {
return rangedListener;
}
return null; // Technically doesn't occur since the functions are anonymous?
}
/**
* Maps a single bin of frequency amplitudes to a numerical value defined by a getter and a setter.
*
* @param {object} conf The configuration for mapping a single bin to a numerical value.
* @param {number} conf.minVal The minimum value the mapping can produce.
* @param {number} conf.maxVal The maximum value the mapping can produce.
* @param {number} conf.bin The bin to map this number to.
* @param {numericalGetter} conf.getter The callback to be used to get the current number.
* @param {numericalSetter} conf.setter The callback to be used to set the new number.
* @param {interpolator} conf.interpolator The interpolation function to use.
* @param {VisualizerUpdateManager} conf.visUpdateManager The update manager to map to.
* @param {boolean} [conf.reversed] If true, then high amplitudes will be mapped to lower values and vice versa.
* @returns {{bin: number, listener: VisualizerUpdateManager.visualizerBinUpdateListener}} The bin listener that was added.
*/
export function mapBinNumerical({ minVal, maxVal, bin, getter, setter, interpolator, visUpdateManager, reversed = false }) {
const listener = {
bin: bin,
listener: (timeDelta, amp) => {
const range = maxVal - minVal;
let interpolated = interpolator(getter(), amp, timeDelta);
if (reversed) interpolated = 1 - interpolated;
setter(minVal + range * interpolated);
}
};
if (visUpdateManager.AddVisualizerBinUpdateListener(listener)) {
return listener;
}
return null; // Technically doesn't occur since the functions are anonymous?
}

View File

@@ -1,84 +0,0 @@
import { parseColor, rgbaToHexRgba } from "../support/colors.js";
import VisualizerUpdateManager from "../visualization/VisualizerUpdateManager.js";
import { mapBinNumerical, mapRangedAvgNumerical } from "./numeric.js";
/**
* Maps the red component of the text color to a certain range of bins.
*
* @param {object} rgbaMapConfiguration The configuration of the mapping.
* @param {HTMLElement} rgbaMapConfiguration.element The element whose text's red value should be mapped.
* @param {number} rgbaMapConfiguration.color Where r for red, g, for green, b for blue, and a for alpha.
* @param {number} rgbaMapConfiguration.lowerBin The lower bound of the bins to be mapped.
* @param {VisualizerUpdateManager} rgbaMapConfiguration.visUpdateManager The visualizer update manager associated with the audio playback you would like the mapping with.
* @param {interpolator} rgbaMapConfiguration.interpolator The interpolation function to use.
* @param {number} [rgbaMapConfiguration.upperBin=undefined] The upper bound of the bins to be mapped. If left undefined, then only the bin defined by the min value is used.
* @param {boolean} [rgbaMapConfiguration.reversed=true] If true, then the quieter, the greater the red value.
* @returns {{bin: number, listener: VisualizerUpdateManager.visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}} The ranged listener that was added.
*/
export function mapRgba({ element, color, lowerBin, visUpdateManager, interpolator, upperBin = undefined, reversed = false }) {
const rgbaStr = "rgba";
color = rgbaStr.indexOf(color);
if (color < 0) throw new Error("Invalid color parameter provided.");
const getter = () => parseColor(element.style.color)[color];
const setter = (value) => {
const changed = parseColor(element.style.color);
changed[color] = value;
element.style.color = rgbaToHexRgba(changed);
};
const conf = {
minVal: 0,
maxVal: 255,
bin: lowerBin,
getter: getter,
setter: setter,
interpolator: interpolator,
visUpdateManager: visUpdateManager,
reversed: reversed
};
if (upperBin) {
conf.bin = undefined;
conf.lowerBin = lowerBin;
conf.upperBin = upperBin;
return mapRangedAvgNumerical(conf);
}
return mapBinNumerical(conf);
}
/**
*
* @param {object} fontSizeMapConfiguration The configuration of the font size mapping.
* @param {HTMLElement} fontSizeMapConfiguration.element The element whose font sizes will be mapped to the amplitude.
* @param {number} fontSizeMapConfiguration.growLower The lower limit of the font size.
* @param {number} fontSizeMapConfiguration.growUpper The upper limit of the font size.
* @param {string} fontSizeMapConfiguration.unit The unit the upper and lower bounds are measured in.
* @param {number} fontSizeMapConfiguration.lowerBin The lower boundary of bins to be mapped (or the single bin to be mapped if the upper bin is undefined).
* @param {VisualizerUpdateManager} fontSizeMapConfiguration.visUpdateManager the visualizer update manager to be mapped to.
* @param {interpolator} fontSizeMapConfiguration.interpolator The interpolation function to be used to transition from one value to the next.
* @param {number} [fontSizeMapConfiguration.upperBin=undefined] The upper bin, or undefined, which results in mapping to a single bin.
* @param {boolean} [fontSizeMapConfiguration.reversed=false] If true, then high amplitudes are mapped to lower values and vice versa.
* @returns {{bin: number, listener: VisualizerUpdateManager.visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}} The listener that was added (ranged if an upper bound was provided, binned otherwise).
*/
export function mapFontSize({ element, growLower, growUpper, unit, lowerBin, visUpdateManager, interpolator, upperBin = undefined, reversed = false }) {
const getter = () => parseInt(element.style.fontSize);
const setter = (value) => element.style.fontSize = value + unit;
const conf = {
minVal: growLower,
maxVal: growUpper,
bin: lowerBin,
getter: getter,
setter: setter,
interpolator: interpolator,
visUpdateManager: visUpdateManager,
reversed: reversed
};
if (upperBin) {
conf.bin = undefined;
conf.lowerBin = lowerBin;
conf.upperBin = upperBin;
return mapRangedAvgNumerical(conf);
}
return mapBinNumerical(conf);
}
// TODO: Future: map hue

View File

@@ -1,27 +0,0 @@
import Visualizer from "../visualization/Visualizer.js";
/**
* Accepts a canvas and uses the entire canvas to draw a horizontal visualizer.
*
* @param {HTMLCanvasElement} canvasElement the canvas element to use to draw this horizontal visualizer.
* @param {Visualizer} visualizer the visualizer in which the data to display is obtained.
*/
export function horizontalVisualizer(canvasElement, visualizer) {
let _width = canvasElement.width;
let _height = canvasElement.height;
let _canvasCtx = canvasElement.getContext("2d");
let _visualizer = visualizer;
let update = function (delta, bins) {
_canvasCtx.clearRect(0, 0, _width, _height); // clear canvas.
let barWidth = Math.floor(_width / bins.length) - 1; // -1 for 1 pixel gap between bars.
let barIndex = 0;
bins.forEach(bin => {
let normalBin = bin / 255.0;
_canvasCtx.fillStyle = "rgb(" + 0 + "," + bin + "," + bin + ")";
let barHeight = _height * normalBin;
_canvasCtx.fillRect(barIndex * barWidth, _height - barHeight, barWidth, barHeight);
barIndex += 1;
});
};
_visualizer.addUpdateListener(update);
}

View File

@@ -1,190 +0,0 @@
import Visualizer from "../visualization/Visualizer.js";
/**
* 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.
*/
constructor(url, name, author) {
this._displayName = name;
this._author = author;
this._url = url;
this._audio = null;
this._visualizer = null;
/**
* 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;
}
this._audio = new Audio(this._url);
this._audio.addEventListener("canplaythrough", () => {
this.ready = true;
if (onReady && this.isAudioInstantiated()) {
onReady(this._audio);
}
});
return this._audio;
}
/**
*
* @returns {string} representing the name of the song to be displayed.
*/
getDisplayName() {
return this._displayName;
}
/**
*
* @returns {string} representing the author of the song.
*/
getAuthor() {
return this._getAuthor;
}
/**
*
* @returns {string} representing the url at which the file for this song can be found.
*/
getUrl() {
return this._url;
}
/**
*
* @returns {boolean} true if and only if there is audio data that is either already loaded or is being loaded.
*/
isAudioInstantiated() {
return this._audio ? true : false;
}
/**
* Begins audio playback as soon as possible.
*
* This function uses the {@link PlaylistSong#getAudio} function and specifically, the {@link AudioEventCallback}.
*/
play() {
this.getAudio((audio) => {
audio.play();
});
}
/**
* Pauses the audio playback, unless the audio data has yet to be instantiated.
*/
pause() {
if (!this.isAudioInstantiated()) 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.
* Makes sure the audio is paused.
*
* Also calls {@link PlaylistSong#unloadVisualizer}.
*/
unloadAudio() {
if (!this.isAudioInstantiated()) return;
this._audio.pause();
this.unloadVisualizer();
this._audio = null;
this.ready = false;
}
/**
*
* @param {number} [fftSize=1024] the size of the FFT window.
* @returns {Visualizer} returns the visualizer.
*/
getVisualizer(fftSize = 1024) {
if (this._visualizer && this._visualizer.getFftSize() === fftSize) return this._visualizer;
this._visualizer = new Visualizer(this.getAudio(), fftSize);
return this._visualizer;
}
/**
*
* @returns {boolean} returns true if and only if the visualizer is instantiated.
*/
isVisualizerInstantiated() {
return this._visualizer ? true : false;
}
/**
* Unloads the visualizer.
* Stops the visualizer.
*/
unloadVisualizer() {
this._visualizer.stop();
this._visualizer = null;
}
}

View File

@@ -1,368 +0,0 @@
import "../styles/songplayer.css";
/**
* A player that keeps track of global properties for playback as well as traversing a playlist. Additionally keeps track of song audio data and attempts to reduce memory usage when possible.
*/
export default class SongPlayer {
/**
*
* @param {SongPlaylist} playlist the playlist of songs that this player is in charge of.
*/
constructor(playlist) {
this._playlist = playlist;
this._current = 0;
this._volume = 1;
this._playing = true;
this._playlistChangeListeners = [];
this._currentSongChangeListeners = [];
this._volumeChangeListeners = [];
this._playingChangeListener = [];
this._allAudioEventListeners = {};
}
/**
*
* @param {SongPlaylist} playlist the new playlist of songs that this player is in charge of.
*/
setPlaylist(playlist) {
this._playlist.unloadAllAudio();
const old = this._playlist;
this._playlist = playlist;
this._current = 0;
this._playlistChangeListeners.forEach(playlistChangeListener => {
playlistChangeListener(old, this.getPlaylist());
});
}
/**
*
* @returns {SongPlaylist} the current playlist of songs that this player is in charge of.
*/
getPlaylist() {
return this._playlist;
}
/**
*
* @returns {boolean} true if and only if successful in going to the next song.
*/
next() {
return this.changeCurrentSongIndex(this.getCurrentSongIndex() + 1);
}
/**
* attempts to go to the previous song.
*
* @returns {boolean} true if and only if successful in going to the previous song.
*/
previous() {
return this.changeCurrentSongIndex(this.getCurrentSongIndex() - 1);
}
/**
*
* @param {number} index the index of the song to jump to.
* @returns {boolean} true if and only if successful jumping to the given index.
*/
changeCurrentSongIndex(index) {
if (index >= this._playlist.total()) return false;
if (index <= 0) return false;
Object.keys(this._allAudioEventListeners).forEach(key => {
const listeners = this._allAudioEventListeners[key];
listeners.forEach(listener => {
this.getCurrentSong().getAudio().removeEventListener(key, listener);
});
});
this.getCurrentSong().unloadAudio();
const old = this.getCurrentSong();
this._current = index;
this._currentSongChangeListeners.forEach(currentChangeListener => {
currentChangeListener(old, this.getCurrentSong());
});
Object.keys(this._allAudioEventListeners).forEach(key => {
const listeners = this._allAudioEventListeners[key];
listeners.forEach(listener => {
this.getCurrentSong().getAudio().addEventListener(key, listener);
});
});
if (this._playing) {
this.playCurrent();
}
return true;
}
/**
*
* @returns {number} the current song's index in the playlist.
*/
getCurrentSongIndex() {
return this._current;
}
playCurrent() {
this.getCurrentSong().getAudio((audio) => {
audio.volume = this._volume;
audio.play();
});
this._playing = true;
}
pauseCurrent() {
this.getCurrentSong().getAudio().pause();
this._playing = true;
}
/**
* Toggles whether or not the current song is playing.
*/
togglePlay() {
if (this._playing) {
this.pauseCurrent();
} else {
this.playCurrent();
}
}
/**
*
* @param {number} volume a number between 0 to 1 inclusive representing the volume of the player.
* @returns {boolean} true if and only if the volume was successfully set.
*/
setVolume(volume) {
if (volume > 1) return false;
if (volume < 0) return false;
this._volume = volume;
this.getCurrentSong().getAudio().volume = this._volume;
return true;
}
/**
*
* @returns {number} the current volume of the player represented by a number between 0 and 1 inclusive.
*/
getVolume() {
return this._volume;
}
/**
* Attempts to seek to the given position.
*
* @param {number} position the position to seek to.
* @returns {boolean} true if and only if the position to seek to is within the duration of the track. This also means that if the track has not finished loading the duration data, than this will always return false.
*/
seek(position) {
if (position > this.getCurrentSong().getAudio().duration || position < 0) return false;
this.getCurrentSong().getAudio(audio => {
audio.currentTime = position;
});
return true;
}
/**
*
* @returns {number} how many seconds into the audio track has been played in seconds.
*/
getCurrentPosition() {
return this.getCurrentSong().getAudio().currentTime;
}
/**
*
* @returns {number} the total length of the song, or NaN if this information has not loaded yet.
*/
getCurrentDuration() {
return this.getCurrentSong().getAudio().duration;
}
/**
* Convenience method for creating a play button complete with CSS for transitioning to a paused button.
*
* Automatically hooked up to the player's state change updates and play and pause functions.
*
* @returns {HTMLElement} the play button element that can be added to a DOM.
*/
generatePlayElement() {
const playButton = document.createElement("button");
playButton.classList.add("player");
playButton.classList.add("play-btn");
if (!this._playing) playButton.classList.add("paused");
playButton.addEventListener("click", () => {
this.togglePlay();
});
this.addPlayChangeListener((old, current) => {
if (current) {
playButton.classList.remove("paused");
} else {
playButton.classList.add("paused");
}
});
return playButton;
}
generateNextElement() {
const nextButton = document.createElement("button");
nextButton.classList.add("player");
nextButton.classList.add("next");
nextButton.addEventListener("click", () => {
this.next();
});
return nextButton;
}
generatePreviousElement() {
const previousButton = document.createElement("button");
previousButton.classList.add("player");
previousButton.classList.add("previous");
previousButton.addEventListener("click", () => {
this.previous();
});
return previousButton;
}
getCurrentSong() {
return this._playlist.songAtIndex(this._current);
}
/**
* @callback changeListener
* @param {*} old the previous value.
* @param {*} current the the current (new) value.
*/
/**
*
* @param {changeListener} listener the listener to receive the updates.
* @returns {boolean} true if and only if successfully added the listener.
*/
addCurrentSongChangeListener(listener) {
if (this._currentSongChangeListeners.includes(listener)) return false;
this._currentChangeListener.push(listener);
return true;
}
/**
*
* @param {changeListener} listener the song change listener to remove.
* @returns {boolean} true if and only if the song change listener given was successfully removed.
*/
removeCurrentSongChangeListener(listener) {
const removeIndex = this._currentSongChangeListeners.indexOf(listener);
if (removeIndex < 0) return false;
this._currentSongChangeListeners.splice(removeIndex, 1);
return true;
}
/**
*
* @param {changeListener} listener the playlist change listener to add to the callback list.
* @returns {boolean} true if and only if the given listener was successfully registered.
*/
addPlaylistChangeListener(listener) {
if (this._playlistChangeListeners.includes(listener)) return false;
this._playlistChangeListeners.push(listener);
return true;
}
/**
*
* @param {changeListener} listener the playlist change listener to remove.
* @returns {boolean} true if and only if a listener was successfully removed from the callback list.
*/
removePlaylistChangeListener(listener) {
const removeIndex = this._playlistChangeListeners.indexOf(listener);
if (removeIndex < 0) return false;
this.playlistChangeListener.splice(removeIndex, 1);
return true;
}
/**
*
* @param {changeListener} listener the listener that is called when the player's volume is changed.
* @returns {boolean} true if and only if the listener was successfully added.
*/
addVolumeChangeListener(listener) {
if (this._volumeChangeListeners.includes(listener)) return false;
this._volumeChangeListeners.push(listener);
return true;
}
/**
*
* @param {changeListener} listener the volume change listener to remove.
* @returns {boolean} true if and only if a listener was successfully removed from the callback list.
*/
removeVolumeChangeListener(listener) {
const removeIndex = this._volumeChangeListeners.indexOf(listener);
if (removeIndex < 0) return false;
this._volumeChangeListeners.splice(removeIndex, 1);
return true;
}
/**
*
* @param {changeListener} listener the listener that is called when the player's volume is changed.
* @returns {boolean} true if and only if the listener was successfully added.
*/
addPlayChangeListener(listener) {
if (this._playingChangeListener.includes(listener)) return false;
this._playingChangeListener.push(listener);
return true;
}
/**
*
* @param {changeListener} listener the play change listener to remove.
* @returns {boolean} true if and only if a listener was successfully removed from the callback list.
*/
removePlayChangeListener(listener) {
const removeIndex = this._playingChangeListener.indexOf(listener);
if (removeIndex < 0) return false;
this._playingChangeListener.splice(removeIndex, 1);
return true;
}
/**
*
* @param {string} type the type of the listener on the {@link HTMLAudioElement}.
* @param {EventListener|EventListenerObject} eventListener the event listener.
* @returns {boolean} true if and only if successfully registered event listener.
*/
addEventListenerToCurrentAudio(type, eventListener) {
let typeListeners = this._allAudioEventListeners[type];
if (!typeListeners) {
typeListeners = [];
this._allAudioEventListeners[type] = typeListeners;
}
if (typeListeners.includes(eventListener)) return false;
typeListeners.push(eventListener);
this.getCurrentSong().getAudio().addEventListener(type, eventListener);
return true;
}
/**
*
* @param {string} type the type of the listener on the {@link HTMLAudioElement}.
* @param {EventListener|EventListenerObject} eventListener the event listener.
* @returns {boolean} true if and only if the event listener was successfully added.
*/
removeEventListenerFromCurrentAudio(type, eventListener) {
let typeListeners = this._allAudioEventListeners[type];
if (!typeListeners) return false;
const removeIndex = typeListeners.indexOf(eventListener);
if (removeIndex < 0) return false;
typeListeners.splice(removeIndex, 1);
this.getCurrentSong().getAudio().removeEventListener(type, eventListener);
return true;
}
}

View File

@@ -1,92 +0,0 @@
import PlaylistSong from "./PlaylistSong.js";
/**
* A playlist that holds a multitude of songs in the form of {@see PlaylistSong}.
*/
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];
}
/**
* Automatically creates and adds a {@see PlaylistSong} to this playlist.
*
* @param {string} url where the audio data can be found.
* @param {string} name the name of the song.
* @param {string} author the author(s) of the song.
*/
add(url, name, author) {
this._list.push(new PlaylistSong(url, name, author, this));
}
/**
* Removes a {@see playlistSong} from this playlist.
*
* @param {number} index the index of the song to be removed.
* @returns {PlaylistSong} the song that was removed, or null if the index of was invalid.
*/
remove(index) {
if (index >= this._list.length) {
return null;
}
let removed = this._list.splice(index, 1)[0];
if (removed.length > 0) {
return removed[0];
}
}
/**
* Attempts to find a {@link PlaylistSong} given a name.
*
* @param {string} name the name of the song to be found.
* @returns {number} the index of the song found, or -1 if it was not found.
*/
findSongIndex(name) {
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();
});
}
}

View File

@@ -1,89 +0,0 @@
import VisualizerUpdateManager from "../visualization/VisualizerUpdateManager.js";
import SongPlayer from "./SongPlayer.js";
/**
* A song player that provides easier access to the current songs visualizer data.
*
* Additionally, automatically re-binds all the visualizer update listeners for song changes.
*
* Automatically loads the songs.
*/
export default class VisualizedSongPlayer extends SongPlayer {
/**
* Instantiates a song player with visualization features.
*
* @param {SongPlaylist} playlist the playlist this player manages.
* @param {number} [fftSize=1024] the size of the fft window for analysis.
*/
constructor(playlist, fftSize = 1024) {
super(playlist);
this._fftSize = fftSize;
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
}
/**
* Sets the the playlist.
*
* @param {SongPlaylist} playlist the new playlist of songs that this player is in charge of.
*/
setPlaylist(playlist) {
super.setPlaylist(playlist);
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
}
/**
*
* @returns {boolean} true if and only if successful in changing to the next song.
*/
next() {
const updateListeners = this._visualizerUpdateManager.getBinnedListeners();
if (!super.next()) return false;
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
this._visualizerUpdateManager.setBinnedListeners(updateListeners);
return true;
}
/**
* Jumps to the previous song if possible.
*
* @returns {boolean} true if and only if successful in switching to the previous song in the playlist.
*/
previous() {
const updateListeners = this._visualizerUpdateManager.getBinnedListeners();
if (!super.previous()) return false;
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
this._visualizerUpdateManager.setBinnedListeners(updateListeners);
return true;
}
/**
*
* @param {number} index the index of the song to change to.
* @returns {boolean} true if and only if successful in jumping to the given index.
*/
changeCurrent(index) {
const updateListeners = this.VisualizerUpdateManager.getBinnedListeners();
if (!super.changeCurrent(index)) return false;
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
this._visualizerUpdateManager.setBinnedListeners = updateListeners();
return true;
}
/**
*
* @returns {VisualizerUpdateManager} the current visualizer update manager.
*/
getCurrentVisualizerUpdateManager() {
return this._visualizerUpdateManager();
}
/**
*
* @returns {Visualizer} the current song's visualizer.
*/
getCurrentSongVisualizer() {
return this.getCurrentSong().getVisualizer();
}
}

View File

@@ -1,51 +0,0 @@
import SongPlayer from "./SongPlayer.js";
import SongPlaylist from "./SongPlaylist.js";
import VisualizedSongPlayer from "./VisualizedSongPlayer.js";
import PlayListSong from "./PlaylistSong.js";
/**
* Instantiates a song player with a given playlist.
*
* @see SongPlayer for more information about the returned object.
* @param {SongPlaylist} playlist the playlist this player will play through.
* @returns {SongPlayer} the instantiated song player.
*/
export function createSongPlayer(playlist) {
return new SongPlayer(playlist);
}
/**
* Instantiates a song playlist.
*
* @see SongPlaylist for more information about the returned object.
* @param {string} name the name of the playlist.
* @returns {SongPlaylist} the instantiated song playlist.
*/
export function createSongPlaylist(name) {
return new SongPlaylist(name);
}
/**
* Instantiates a song player that manages the visualizers for individual songs, as well as the listeners for the visualizations.
*
* @see VisualizedSongPlayer for more information about the returned object.
* @param {SongPlaylist} playlist the playlist this visualized song player will play through.
* @param {number} [fftSize=1024] the FFT window size.
* @returns {VisualizedSongPlayer} a song player capable of retrieving the visualizer for the current playing song.
*/
export function createVisualizedSongPlayer(playlist, fftSize = 1024) {
return new VisualizedSongPlayer(playlist, fftSize);
}
/**
* Instantiates a playlist song.
*
* @see PlaylistSong for more information about the returned object.
* @param {string} url The url at which the audio for the song can be retrieved.
* @param {string} name The name of the song.
* @param {string} author The name of the author(s).
* @returns {PlayListSong} The playlist song that is instantiated.
*/
export function createPlaylistSong(url, name, author) {
return new PlayListSong(url, name, author);
}

View File

@@ -1,29 +0,0 @@
.player.play-btn {
box-sizing: border-box;
height: 60px;
transition: 80ms all ease;
will-change: border-width;
cursor: pointer;
border-color: transparent transparent transparent black;
border-style: solid;
border-width: 30px 0px 30px 50px;
}
.player.play-btn.paused {
border-style: double;
border-width: 0px 0px 0px 60px;
}
.player.next {
border-style: none;
}
.player.previous {
border-style: none;
}
table.player {
border-collapse: collapse;
}

View File

@@ -1,102 +0,0 @@
/**
* The rg
*
* @param {string} hex the hex value.
* @returns {number[]} the resulting components of the hex value.
*/
export function rgbaHexToRgba(hex) {
if (hex.startsWith("#")) {
hex = hex.substring(1);
if (hex.length > 8) {
throw new Error("Invalid hex syntax (length).");
}
} else {
throw new Error("Invalid hex syntax (missing pound).");
}
for (let i = hex.length; i < 8; i++) {
hex = "0" + hex;
}
let remaining = hex.length;
let result = [0, 0, 0, 0];
result[3] = parseInt(hex.substring(remaining -= 2, remaining + 2), 16);
result[2] = parseInt(hex.substring(remaining -= 2, remaining + 2), 16);
result[1] = parseInt(hex.substring(remaining -= 2, remaining + 2), 16);
result[0] = parseInt(hex.substring(remaining -= 2, remaining + 2), 16);
return result;
}
/**
*
* @param {string} hex the hex value.
* @returns {number[]} the resulting r, g, and b components.
*/
export function rgbHexToRgba(hex) {
const result = rgbaHexToRgba(hex + "FF");
return result;
}
/**
*
* @param {string} rgba The CSS rgba(r,g,b,a) call.
* @returns {number[]} the rgba components.
*/
export function cssRgbaToRgba(rgba) {
const cssRgbaRegex = /rgba\((\d+),(\d+),(\d+),(\d+)\)/;
try {
const matches = rgba.match(cssRgbaRegex);
return [parseInt(matches[1]), parseInt(matches[2]), parseInt(matches[3]), parseInt(matches[4])];
} catch (error) {
throw new Error("Could not parse the given css rgba function call: " + error.message);
}
}
/**
*
* @param {string} rgb The CSS rgb(r,g,b) call.
* @returns {number[]} the rgba components.
*/
export function cssRgbToRgba(rgb) {
const cssRgbRegex = /rgb\((\d+),(\d+),(\d+)\)/;
try {
const matches = rgb.match(cssRgbRegex);
return [parseInt(matches[1]), parseInt(matches[2]), parseInt(matches[3]), 255];
} catch (error) {
throw new Error("Could not parse the given css rgb function call: " + error.message);
}
}
/**
* Converts a given array of r, g, b, and a components into a hex string.
*
* @param {number[]} rgba an array with red, green, blue, and alpha components in that order.
* @returns {string} The resulting hex value.
*/
export function rgbaToHexRgba(rgba) {
const filler = (hex) => hex.length < 2 ? "0" + hex : hex;
return "#" + filler(rgba[0].toString(16)) + filler(rgba[1].toString(16)) + filler(rgba[2].toString(16)) + filler(rgba[3].toString(16));
}
/**
* Converts a css rgb, rgba, hex, or rgba hex to a rgba array.
*
* For hex, we assume there is no alpha channel unless the hex value is not minimized.
*
* @param {string} color The string that contains the color information.
* @returns {number[]} an array that contains the r, g, b and a components.
*/
export function parseColor(color) {
if (color.startsWith("rgba(")) {
return cssRgbaToRgba(color);
} else if (color.startsWith("rgb(")) {
return cssRgbToRgba(color);
} else if (color.startsWith("#")) {
if (color.length === 9) {
return rgbaHexToRgba(color);
} else {
return rgbHexToRgba(color);
}
}
throw new Error("Could not parse to an rgba value.");
}

View File

@@ -1,25 +0,0 @@
/**
* @typedef {function} interpolator
* @param {number} current the current value.
* @param {number} dest the destination value.
* @param {number} frameDelta the time elapsed since the last update.
* @returns {number} the new current value.
*/
// TODO: Add some interpolation functions.
/**
*
* @param {number} rate the number of frequency values to shift by per second.
* @returns {interpolator} the interpolation function with the given rate.
*/
export function createEaseLinear(rate) {
return (current, dest, frameDelta) => {
let direction = 1;
if (dest < current) {
direction = -1;
}
return current + (Math.min(rate * frameDelta, dest) * direction);
};
}

View File

@@ -1,4 +0,0 @@
import * as colors from "./colors.js";
import * as easings from "./easings.js";
export { colors, easings };

View File

@@ -1,110 +0,0 @@
/**
* Provides a simplified access point to the frequency bins in the form of a visualization update listener.
*/
export default class Visualizer {
/**
* @callback visualizerUpdateListener
* @param {number} delta elapsed time since last update.
* @param {Uint8Array} bins the bins with varying frequency values.
*/
/**
*
* @param {MediaSource|HTMLMediaElement} mediaSource a media source to analyze.
* @param {number} [fftSize = 1024] the size of the fft window.
*/
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._buffer = new Uint8Array(this._analyzer.frequencyBinCount);
this._source.connect(this._analyzer);
this._analyzer.connect(this._audioCtx.destination);
this.lastUpdate = null;
}
/**
* Begins analyzing and sending out update pings.
*/
analyze() {
if (this._analyzing) {
return;
}
this._analyzing = true;
let self = this; // since calling from requestAnimationFrame means "this" is no longer set to produced object.
const update = (timestamp) => {
if (!self._analyzing) return;
if (!self.lastUpdate) {
self.lastUpdate = timestamp;
}
let delta = timestamp - self.lastUpdate;
self._analyzer.getByteFrequencyData(self._buffer);
self._updateListeners.forEach(listener => {
listener(delta, self._buffer);
});
requestAnimationFrame(update);
};
requestAnimationFrame(update);
}
/**
* Stops the analysis. Listeners will stop receiving bins.
*/
stop() {
this._analyzing = false;
}
/**
*
* @param {visualizerUpdateListener} listener the visualizer update listener to be registered.
* @returns {boolean} true if and only if the listener was successfully added.
*/
addUpdateListener(listener) {
if (this._updateListeners.includes(listener));
this._updateListeners.push(listener);
return true;
}
/**
*
* @param {visualizerUpdateListener} listener the visualizer update listener to remove.
* @returns {boolean} true if and only if the removal of the listener was a success.
*/
removeUpdateListener(listener) {
const removeIndex = this._updateListeners.indexOf(listener);
if (removeIndex < 0) return false;
this._updateListeners.splice(removeIndex, 1);
return true;
}
/**
*
* @returns {number} the number of bins based on the size of the FFT window.
*/
getNumberOfBins() {
return this._buffer.length;
}
/**
*
* @returns {number} the fft window size.
*/
getFftSize() {
return this._analyzer.fftSize;
}
}

View File

@@ -1,170 +0,0 @@
/**
* A visualizer update manager offers an extra layer of abstraction on top of the {@link Visualizer}'s update listener.
*
* Specifically, the update manager handles updates directly from the {@link Visualizer} and checks for changes in the individual bins. These changes are then broadcasted to the individual bin listeners.
* In the rare event that a bin has not changed, then it will not receive an update call.
*/
export default class VisualizerUpdateManager {
/**
*
* @param {Visualizer} visualizer the visualizer this manager obtains data from.
*/
constructor(visualizer) {
this._binnedListeners = [];
this._rangedListeners = [];
for (let i = 0; i < visualizer.getNumberOfBins(); i++) {
this._binnedListeners.push([]);
}
this._lastBins = new Uint8Array(this._binnedListeners.length);
this._visualizer = visualizer;
this._visualizerListener = (delta, bins) => {
const sortedCopyOfRangedListeners = [... this._rangedListeners]; // We assume this is sorted properly. A priority queue could be better.
for (let binInd = 0; binInd < this._lastBins.length; binInd++) {
const lastBin = this._lastBins[binInd];
if (lastBin !== bins[binInd]) {
this._binnedListeners[binInd].forEach(listener => {
listener(delta, bins[binInd], bins[binInd] - lastBin);
});
for (let rangedInd = 0; rangedInd < sortedCopyOfRangedListeners.length; rangedInd++) { // Could switch to a while loop.
const { lower, upper, listener } = sortedCopyOfRangedListeners[rangedInd];
if (lower > binInd) break; // Don't need to check the rest if the current lowest minimum is greater than the current bin index.
if (binInd <= upper) {
listener(delta, bins.slice(lower, upper));
sortedCopyOfRangedListeners.shift();
rangedInd--;
}
}
this._lastBins[binInd] = bins[binInd];
}
}
};
visualizer.addUpdateListener(this._visualizerListener);
}
/**
* @callback VisualizerUpdateManager.visualizerBinUpdateListener
* @param {number} timeDelta elapsed time since last update.
* @param {number} amplitude The amplitude of the associated bin.
* @param {number} ampDelta change in amplitude of the frequency bin.
*/
/**
* @callback VisualizerUpdateManager.visualizerRangedUpdateListener
* @param {number} timeDelta elapsed time since last update.
* @param {number} bins the bins of the range.
*/
/**
* Adds a listener to a specific frequency bin.
*
* @param {object} freqBinListener the listener for a specific frequency bin.
* @param {number} freqBinListener.freqBin the frequency bin this update listener should listen to.
* @param {VisualizerUpdateManager.visualizerBinUpdateListener} freqBinListener.listener the listener itself that will be called upon the bin updating.
* @returns {boolean} true if and only if the updater was added successfully.
*/
AddVisualizerBinUpdateListener({ freqBin, listener }) {
if (this._binnedListeners[freqBin].includes(listener)) return false;
this._binnedListeners[freqBin].push(listener);
return true;
}
/**
* Similar to {@link VisualizerUpdateManager#AddVisualizerBinUpdateListener}, this method adds a listener for to a range of bins.
*
* @param {object} rangedUpdateListener The ranged update listener to add.
* @param {number} rangedUpdateListener.lower The lower bound of the bins to listen to (inclusive).
* @param {number} rangedUpdateListener.upper The upper bound of the bins to listen to (inclusive).
* @param {VisualizerUpdateManager.visualizerRangedUpdateListener} rangedUpdateListener.listener The listener to register to the range.
* @returns {boolean} True if and only if the ranged listener was added successfully.
*/
addVisualizerRangedUpdateListener({ lower, upper, listener }) {
const rangedListener = {
lower: lower,
upper: upper,
listener: listener
};
if (this._rangedListeners.includes(rangedListener)) return false;
this._rangedListeners.push(rangedListener);
this._rangedListeners.sort((a, b) => a.lower - b.lower);
return true;
}
/**
*
* @param {object} binFreqListener The bin frequency listener to remove.
* @param {number} binFreqListener.freqBin the frequency bin the update listener to be removed from is in.
* @param {VisualizerUpdateManager.visualizerBinUpdateListener} binFreqListener.listener the listener that is to be removed.
* @returns {boolean} true if and only if the listener was successfully removed.
*/
removeVisualizerBinUpdateListener({ freqBin, listener }) {
const removeIndex = this._binnedListeners[freqBin].indexOf(listener);
if (removeIndex < 0) return false;
this._binnedListeners[freqBin].splice(removeIndex, 1);
return true;
}
/**
* Similar to {@link removeVisualizerBinUpdateListener}, this method removes the given listener from a range of bins.
*
* @param {object} rangedListener The ranged listener to remove.
* @param {number} rangedListener.lower The lower bound of bins to remove the listener from (inclusive).
* @param {number} rangedListener.upper The upper bound of bin to remove the listener from (inclusive.)
* @param {VisualizerUpdateManager.visualizerRangedUpdateListener} rangedListener.listener The update listener to remove from the bins.
* @returns {boolean} True if and only if the given listener was removed.
*/
removeVisualizerRangedUpdateListener({ lower, upper, listener }) {
const rangedListener = {
lower: lower,
upper: upper,
listener: listener
};
const removeIndex = this._rangedListeners.indexOf(rangedListener);
if (removeIndex < 0) return false;
this._binnedListeners.splice(removeIndex, 1);
return true;
}
/**
* @param {object} listeners The listeners that this visualizer update manager should be set to use.
* @param {VisualizerUpdateManager.visualizerBinUpdateListener[][]} listeners.binned The bin listeners.
* @param {object[]} listeners.ranged The array of ranged listeners.
* @param {number} listeners.ranged[].lower The lower bound of the listener.
* @param {number} listeners.ranged[].upper The upper bound of the listener.
* @param {VisualizerUpdateManager.visualizerRangedUpdateListener} listeners.ranged[].listener The listener for the previously mentioned ranges.
* @returns {boolean} true if and only if successfully loaded the new listeners.
*/
setBinnedListeners(listeners) {
if (listeners.binned.length !== this._binnedListeners.length) return false;
this._binnedListeners = listeners.binned;
this._rangedListeners = listeners.ranged.sort((a, b) => a.lower - b.lower);
return true;
}
/**
*
* @returns {object} All the listeners, both for binned, and ranged listeners. See {@link VisualizerUpdateManager#setBinnedListeners} to see the structure of the returned object.
*/
getAllListeners() {
return {
binned: this._binnedListeners,
ranged: this._rangedListeners
};
}
/**
* Clears this manager of all listeners.
*/
clearBinnedListeners() {
this._binnedListeners.forEach(bin => {
bin.length = 0;
});
}
/**
* Unbinds this update manager from the initial visualizer. Effectively meaning this manager will no longer be used.
*/
unbindVisualizer() {
this._visualizer.removeUpdateListener(this._visualizerListener);
}
}

View File

@@ -1,25 +0,0 @@
import Visualizer from "./Visualizer.js";
import VisualizerUpdateManager from "./VisualizerUpdateManager.js";
/**
* Instantiates a visualizer.
*
* @see Visualizer for information on the returned object.
* @param {HTMLMediaElement|MediaSource} mediaSource the source of audio to be visualized.
* @param {number} [fftSize=1024] the FFT window size.
* @returns {Visualizer} the visualizer with the given media source and the given FFT size.
*/
export function createVisualizer(mediaSource, fftSize = 1024) {
return new Visualizer(mediaSource, fftSize);
}
/**
* Instantiates a VisualizerUpdateManager.
*
* @see VisualizerUpdateManager for information on the returned object.
* @param {Visualizer} visualizer the visualizer this update manager manages.
* @returns {VisualizerUpdateManager} the instantiated visualizer update manager for the given visualizer.
*/
export function createVisualizerUpdateManager(visualizer) {
return new VisualizerUpdateManager(visualizer);
}