Changed all classes to using private fields and properties.

This commit is contained in:
Harrison Deng 2022-04-17 23:12:53 -05:00
parent b5a4a7dcb4
commit 2cb4ad2652
7 changed files with 234 additions and 229 deletions

View File

@ -1,5 +1,4 @@
import { parseColor, rgbaToHexRgba } from "../support/colors.js"; import { parseColor, rgbaToHexRgba } from "../support/colors.js";
import VisualizerUpdateManager from "../visualization/VisualizerUpdateManager.js";
import { mapBinNumerical, mapRangedAvgNumerical } from "./numeric.js"; import { mapBinNumerical, mapRangedAvgNumerical } from "./numeric.js";
@ -14,7 +13,7 @@ import { mapBinNumerical, mapRangedAvgNumerical } from "./numeric.js";
* @param {interpolator} rgbaMapConfiguration.interpolator The interpolation function to use. * @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 {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. * @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. * @returns {{bin: number, listener: VisualizerUpdateManager.visualizerBinUpdateListener}|{lower: number, upper: number, listener: visualizerRangedUpdateListener}} The ranged listener that was added.
*/ */
export function mapRgba({ element, color, lowerBin, visUpdateManager, interpolator, upperBin = undefined, reversed = false }) { export function mapRgba({ element, color, lowerBin, visUpdateManager, interpolator, upperBin = undefined, reversed = false }) {
const rgbaStr = "rgba"; const rgbaStr = "rgba";

View File

@ -5,6 +5,12 @@ import Visualizer from "../visualization/Visualizer.js";
* *
*/ */
export default class PlayListSong { export default class PlayListSong {
#displayName;
#author;
#url;
#audio;
#visualizer;
#ready = false;
/** /**
* Constructs a song for a {@link SongPlaylist}. * Constructs a song for a {@link SongPlaylist}.
@ -14,20 +20,13 @@ export default class PlayListSong {
* @param {string} author the author of the song. * @param {string} author the author of the song.
*/ */
constructor(url, name, author) { constructor(url, name, author) {
this._displayName = name; this.#displayName = name;
this._author = author; this.#author = author;
this._url = url; this.#url = url;
this._audio = null;
this._visualizer = null;
/**
* Whether or not this song is ready to be played.
*/
this.ready = false;
} }
/** /**
* @callback AudioEventCallback * @typedef {function} AudioEventCallback
* @param {HTMLAudioElement} audio * @param {HTMLAudioElement} audio
*/ */
@ -36,45 +35,45 @@ export default class PlayListSong {
* @param {AudioEventCallback} [onReady] called when the song is ready, including right away if the song is already ready. * @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. * @returns {HTMLAudioElement} The audio element that represents this song.
*/ */
getAudio(onReady) { fetchAudio(onReady) {
if (this._audio) { if (this.#audio) {
if (this.ready) { if (this.ready) {
if (onReady) onReady(this._audio); if (onReady) onReady(this.#audio);
} }
return this._audio; return this.#audio;
} }
this._audio = new Audio(this._url); this.#audio = new Audio(this._url);
this._audio.addEventListener("canplaythrough", () => { this.#audio.addEventListener("canplaythrough", () => {
this.ready = true; this.ready = true;
if (onReady && this.isAudioInstantiated()) { if (onReady && this.isAudioInstantiated()) {
onReady(this._audio); onReady(this.#audio);
} }
}); });
return this._audio; return this.#audio;
} }
/** /**
* *
* @returns {string} representing the name of the song to be displayed. * The name of the song to be displayed.
*/ */
getDisplayName() { get displayName() {
return this._displayName; return this._displayName;
} }
/** /**
* *
* @returns {string} representing the author of the song. * The author of the song.
*/ */
getAuthor() { get author() {
return this._getAuthor; return this._getAuthor;
} }
/** /**
* *
* @returns {string} representing the url at which the file for this song can be found. * The url at which the file for this song can be found.
*/ */
getUrl() { get url() {
return this._url; return this._url;
} }
@ -83,7 +82,7 @@ export default class PlayListSong {
* @returns {boolean} true if and only if there is audio data that is either already loaded or is being loaded. * @returns {boolean} true if and only if there is audio data that is either already loaded or is being loaded.
*/ */
isAudioInstantiated() { isAudioInstantiated() {
return this._audio ? true : false; return this.#audio ? true : false;
} }
/** /**
@ -92,7 +91,7 @@ export default class PlayListSong {
* This function uses the {@link PlaylistSong#getAudio} function and specifically, the {@link AudioEventCallback}. * This function uses the {@link PlaylistSong#getAudio} function and specifically, the {@link AudioEventCallback}.
*/ */
play() { play() {
this.getAudio((audio) => { this.fetchAudio((audio) => {
audio.play(); audio.play();
}); });
} }
@ -102,48 +101,48 @@ export default class PlayListSong {
*/ */
pause() { pause() {
if (!this.isAudioInstantiated()) return; if (!this.isAudioInstantiated()) return;
this.getAudio((audio) => { this.fetchAudio((audio) => {
audio.pause(); audio.pause();
}); });
} }
/** /**
* *
* @returns {number} the volume on a scale of 0 to 1. * The volume on a scale of 0 to 1.
*/ */
getVolume() { get volume() {
return this.getAudio().volume; return this.fetchAudio().volume;
} }
/** /**
* *
* @param {number} volume a normalized volume on a scale of 0 to 1. * The normalized volume on a scale of 0 to 1.
*/ */
setVolume(volume) { set volume(volume) {
this.getAudio().volume = volume; this.fetchAudio().volume = volume;
} }
/** /**
* *
* @returns {number} the number of seconds into the song. * The number of seconds into the song.
*/ */
getCurrentTime() { get currentTime() {
return this.getAudio().currentTime; return this.fetchAudio().currentTime;
} }
/** /**
* *
* @param {number} currentTime the time position in the song to jump to in seconds. * The time position in the song to jump to in seconds.
*/ */
setCurrentTime(currentTime) { set currentTime(currentTime) {
this.getAudio().currentTime = currentTime; this.fetchAudio().currentTime = currentTime;
} }
/** /**
* *
* @returns {number} the duration of the song. * The duration of the song.
*/ */
getDuration() { get duration() {
return this.getAudio().duration; return this.fetchAudio().duration;
} }
/** /**
@ -154,9 +153,9 @@ export default class PlayListSong {
*/ */
unloadAudio() { unloadAudio() {
if (!this.isAudioInstantiated()) return; if (!this.isAudioInstantiated()) return;
this._audio.pause(); this.#audio.pause();
this.unloadVisualizer(); this.unloadVisualizer();
this._audio = null; this.#audio = null;
this.ready = false; this.ready = false;
} }
@ -167,7 +166,7 @@ export default class PlayListSong {
*/ */
getVisualizer(fftSize = 1024) { getVisualizer(fftSize = 1024) {
if (this._visualizer && this._visualizer.getFftSize() === fftSize) return this._visualizer; if (this._visualizer && this._visualizer.getFftSize() === fftSize) return this._visualizer;
this._visualizer = new Visualizer(this.getAudio(), fftSize); this._visualizer = new Visualizer(this.fetchAudio(), fftSize);
return this._visualizer; return this._visualizer;
} }

View File

@ -4,45 +4,43 @@ 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. * 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 { export default class SongPlayer {
#playlist;
#current = 0;
#volume = 1;
#playing = true;
#playlistChangeListeners = [];
#currentSongChangeListeners = [];
#volumeChangeListeners = [];
#playingChangeListeners = [];
#allAudioEventListeners = {};
/** /**
* *
* @param {SongPlaylist} playlist the playlist of songs that this player is in charge of. * @param {SongPlaylist} playlist the playlist of songs that this player is in charge of.
*/ */
constructor(playlist) { constructor(playlist) {
this._playlist = playlist; this.#playlist = playlist;
this._current = 0;
this._volume = 1;
this._playing = true;
this._playlistChangeListeners = [];
this._currentSongChangeListeners = [];
this._volumeChangeListeners = [];
this._playingChangeListener = [];
this._allAudioEventListeners = {};
} }
/** /**
* * The new playlist of songs that this player is in charge of.
* @param {SongPlaylist} playlist the new playlist of songs that this player is in charge of.
*/ */
setPlaylist(playlist) { set playlist(playlist) {
this._playlist.unloadAllAudio(); this.#playlist.unloadAllAudio();
const old = this._playlist; const old = this.#playlist;
this._playlist = playlist; this.#playlist = playlist;
this._current = 0; this.#current = 0;
this._playlistChangeListeners.forEach(playlistChangeListener => { this.#playlistChangeListeners.forEach(playlistChangeListener => {
playlistChangeListener(old, this.getPlaylist()); playlistChangeListener(old, this.getPlaylist());
}); });
} }
/** /**
* *
* @returns {SongPlaylist} the current playlist of songs that this player is in charge of. * The current playlist of songs that this player is in charge of.
*/ */
getPlaylist() { get playlist() {
return this._playlist; return this.#playlist;
} }
/** /**
@ -68,27 +66,27 @@ export default class SongPlayer {
* @returns {boolean} true if and only if successful jumping to the given index. * @returns {boolean} true if and only if successful jumping to the given index.
*/ */
changeCurrentSongIndex(index) { changeCurrentSongIndex(index) {
if (index >= this._playlist.total()) return false; if (index >= this.#playlist.total()) return false;
if (index <= 0) return false; if (index <= 0) return false;
Object.keys(this._allAudioEventListeners).forEach(key => { Object.keys(this.#allAudioEventListeners).forEach(key => {
const listeners = this._allAudioEventListeners[key]; const listeners = this.#allAudioEventListeners[key];
listeners.forEach(listener => { listeners.forEach(listener => {
this.getCurrentSong().getAudio().removeEventListener(key, listener); this.getCurrentSong().getAudio().removeEventListener(key, listener);
}); });
}); });
this.getCurrentSong().unloadAudio(); this.getCurrentSong().unloadAudio();
const old = this.getCurrentSong(); const old = this.getCurrentSong();
this._current = index; this.#current = index;
this._currentSongChangeListeners.forEach(currentChangeListener => { this.#currentSongChangeListeners.forEach(currentChangeListener => {
currentChangeListener(old, this.getCurrentSong()); currentChangeListener(old, this.getCurrentSong());
}); });
Object.keys(this._allAudioEventListeners).forEach(key => { Object.keys(this.#allAudioEventListeners).forEach(key => {
const listeners = this._allAudioEventListeners[key]; const listeners = this.#allAudioEventListeners[key];
listeners.forEach(listener => { listeners.forEach(listener => {
this.getCurrentSong().getAudio().addEventListener(key, listener); this.getCurrentSong().getAudio().addEventListener(key, listener);
}); });
}); });
if (this._playing) { if (this.#playing) {
this.playCurrent(); this.playCurrent();
} }
return true; return true;
@ -96,30 +94,30 @@ export default class SongPlayer {
/** /**
* *
* @returns {number} the current song's index in the playlist. * The current song's index in the playlist.
*/ */
getCurrentSongIndex() { get currentSongIndex() {
return this._current; return this.#current;
} }
playCurrent() { playCurrent() {
this.getCurrentSong().getAudio((audio) => { this.getCurrentSong().getAudio((audio) => {
audio.volume = this._volume; audio.volume = this.#volume;
audio.play(); audio.play();
}); });
this._playing = true; this.#playing = true;
} }
pauseCurrent() { pauseCurrent() {
this.getCurrentSong().getAudio().pause(); this.getCurrentSong().getAudio().pause();
this._playing = true; this.#playing = true;
} }
/** /**
* Toggles whether or not the current song is playing. * Toggles whether or not the current song is playing.
*/ */
togglePlay() { togglePlay() {
if (this._playing) { if (this.#playing) {
this.pauseCurrent(); this.pauseCurrent();
} else { } else {
this.playCurrent(); this.playCurrent();
@ -127,24 +125,21 @@ export default class SongPlayer {
} }
/** /**
* * A number between 0 to 1 inclusive representing the volume of the player. Values out of these bounds will be rounded to the nearest bound.
* @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) { set volume(volume) {
if (volume > 1) return false; if (volume > 1) volume = 1;
if (volume < 0) return false; if (volume < 0) volume = 0;
this._volume = volume; this.#volume = volume;
this.getCurrentSong().getAudio().volume = this._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. * The current volume of the player represented by a number between 0 and 1 inclusive.
*/ */
getVolume() { get volume() {
return this._volume; return this.#volume;
} }
/** /**
@ -163,17 +158,17 @@ export default class SongPlayer {
/** /**
* *
* @returns {number} how many seconds into the audio track has been played in seconds. * How many seconds into the audio track has been played in seconds.
*/ */
getCurrentPosition() { get currentPosition() {
return this.getCurrentSong().getAudio().currentTime; return this.getCurrentSong().getAudio().currentTime;
} }
/** /**
* *
* @returns {number} the total length of the song, or NaN if this information has not loaded yet. * The total length of the song, or NaN if this information has not loaded yet.
*/ */
getCurrentDuration() { get currentDuration() {
return this.getCurrentSong().getAudio().duration; return this.getCurrentSong().getAudio().duration;
} }
@ -188,7 +183,7 @@ export default class SongPlayer {
const playButton = document.createElement("button"); const playButton = document.createElement("button");
playButton.classList.add("player"); playButton.classList.add("player");
playButton.classList.add("play-btn"); playButton.classList.add("play-btn");
if (!this._playing) playButton.classList.add("paused"); if (!this.#playing) playButton.classList.add("paused");
playButton.addEventListener("click", () => { playButton.addEventListener("click", () => {
this.togglePlay(); this.togglePlay();
@ -224,8 +219,8 @@ export default class SongPlayer {
return previousButton; return previousButton;
} }
getCurrentSong() { get currentSong() {
return this._playlist.songAtIndex(this._current); return this.#playlist.songAtIndex(this.#current);
} }
/** /**
@ -240,8 +235,8 @@ export default class SongPlayer {
* @returns {boolean} true if and only if successfully added the listener. * @returns {boolean} true if and only if successfully added the listener.
*/ */
addCurrentSongChangeListener(listener) { addCurrentSongChangeListener(listener) {
if (this._currentSongChangeListeners.includes(listener)) return false; if (this.#currentSongChangeListeners.includes(listener)) return false;
this._currentChangeListener.push(listener); this.#currentSongChangeListeners.push(listener);
return true; return true;
} }
@ -251,10 +246,10 @@ export default class SongPlayer {
* @returns {boolean} true if and only if the song change listener given was successfully removed. * @returns {boolean} true if and only if the song change listener given was successfully removed.
*/ */
removeCurrentSongChangeListener(listener) { removeCurrentSongChangeListener(listener) {
const removeIndex = this._currentSongChangeListeners.indexOf(listener); const removeIndex = this.#currentSongChangeListeners.indexOf(listener);
if (removeIndex < 0) return false; if (removeIndex < 0) return false;
this._currentSongChangeListeners.splice(removeIndex, 1); this.#currentSongChangeListeners.splice(removeIndex, 1);
return true; return true;
} }
@ -264,8 +259,8 @@ export default class SongPlayer {
* @returns {boolean} true if and only if the given listener was successfully registered. * @returns {boolean} true if and only if the given listener was successfully registered.
*/ */
addPlaylistChangeListener(listener) { addPlaylistChangeListener(listener) {
if (this._playlistChangeListeners.includes(listener)) return false; if (this.#playlistChangeListeners.includes(listener)) return false;
this._playlistChangeListeners.push(listener); this.#playlistChangeListeners.push(listener);
return true; return true;
} }
@ -276,7 +271,7 @@ export default class SongPlayer {
* @returns {boolean} true if and only if a listener was successfully removed from the callback list. * @returns {boolean} true if and only if a listener was successfully removed from the callback list.
*/ */
removePlaylistChangeListener(listener) { removePlaylistChangeListener(listener) {
const removeIndex = this._playlistChangeListeners.indexOf(listener); const removeIndex = this.#playlistChangeListeners.indexOf(listener);
if (removeIndex < 0) return false; if (removeIndex < 0) return false;
this.playlistChangeListener.splice(removeIndex, 1); this.playlistChangeListener.splice(removeIndex, 1);
@ -289,8 +284,8 @@ export default class SongPlayer {
* @returns {boolean} true if and only if the listener was successfully added. * @returns {boolean} true if and only if the listener was successfully added.
*/ */
addVolumeChangeListener(listener) { addVolumeChangeListener(listener) {
if (this._volumeChangeListeners.includes(listener)) return false; if (this.#volumeChangeListeners.includes(listener)) return false;
this._volumeChangeListeners.push(listener); this.#volumeChangeListeners.push(listener);
return true; return true;
} }
@ -300,10 +295,10 @@ export default class SongPlayer {
* @returns {boolean} true if and only if a listener was successfully removed from the callback list. * @returns {boolean} true if and only if a listener was successfully removed from the callback list.
*/ */
removeVolumeChangeListener(listener) { removeVolumeChangeListener(listener) {
const removeIndex = this._volumeChangeListeners.indexOf(listener); const removeIndex = this.#volumeChangeListeners.indexOf(listener);
if (removeIndex < 0) return false; if (removeIndex < 0) return false;
this._volumeChangeListeners.splice(removeIndex, 1); this.#volumeChangeListeners.splice(removeIndex, 1);
return true; return true;
} }
@ -314,8 +309,8 @@ export default class SongPlayer {
* @returns {boolean} true if and only if the listener was successfully added. * @returns {boolean} true if and only if the listener was successfully added.
*/ */
addPlayChangeListener(listener) { addPlayChangeListener(listener) {
if (this._playingChangeListener.includes(listener)) return false; if (this.#playingChangeListeners.includes(listener)) return false;
this._playingChangeListener.push(listener); this.#playingChangeListeners.push(listener);
return true; return true;
} }
@ -325,10 +320,10 @@ export default class SongPlayer {
* @returns {boolean} true if and only if a listener was successfully removed from the callback list. * @returns {boolean} true if and only if a listener was successfully removed from the callback list.
*/ */
removePlayChangeListener(listener) { removePlayChangeListener(listener) {
const removeIndex = this._playingChangeListener.indexOf(listener); const removeIndex = this.#playingChangeListeners.indexOf(listener);
if (removeIndex < 0) return false; if (removeIndex < 0) return false;
this._playingChangeListener.splice(removeIndex, 1); this.#playingChangeListeners.splice(removeIndex, 1);
return true; return true;
} }
@ -339,10 +334,10 @@ export default class SongPlayer {
* @returns {boolean} true if and only if successfully registered event listener. * @returns {boolean} true if and only if successfully registered event listener.
*/ */
addEventListenerToCurrentAudio(type, eventListener) { addEventListenerToCurrentAudio(type, eventListener) {
let typeListeners = this._allAudioEventListeners[type]; let typeListeners = this.#allAudioEventListeners[type];
if (!typeListeners) { if (!typeListeners) {
typeListeners = []; typeListeners = [];
this._allAudioEventListeners[type] = typeListeners; this.#allAudioEventListeners[type] = typeListeners;
} }
if (typeListeners.includes(eventListener)) return false; if (typeListeners.includes(eventListener)) return false;
typeListeners.push(eventListener); typeListeners.push(eventListener);
@ -357,7 +352,7 @@ export default class SongPlayer {
* @returns {boolean} true if and only if the event listener was successfully added. * @returns {boolean} true if and only if the event listener was successfully added.
*/ */
removeEventListenerFromCurrentAudio(type, eventListener) { removeEventListenerFromCurrentAudio(type, eventListener) {
let typeListeners = this._allAudioEventListeners[type]; let typeListeners = this.#allAudioEventListeners[type];
if (!typeListeners) return false; if (!typeListeners) return false;
const removeIndex = typeListeners.indexOf(eventListener); const removeIndex = typeListeners.indexOf(eventListener);
if (removeIndex < 0) return false; if (removeIndex < 0) return false;

View File

@ -4,6 +4,8 @@ import PlaylistSong from "./PlaylistSong.js";
* A playlist that holds a multitude of songs in the form of {@see PlaylistSong}. * A playlist that holds a multitude of songs in the form of {@see PlaylistSong}.
*/ */
export default class SongPlaylist { export default class SongPlaylist {
#list = [];
#name;
/** /**
* Instantiates a playlist for songs. * Instantiates a playlist for songs.
@ -11,17 +13,15 @@ export default class SongPlaylist {
* @param {string} name The name of the playlist. * @param {string} name The name of the playlist.
*/ */
constructor(name) { constructor(name) {
this._list = []; this.#name = name;
this._name = name;
this._current = 0;
} }
/** /**
* *
* @returns {string} the name of the string. * The name of the playlist.
*/ */
getName() { get name() {
return this._name; return this.#name;
} }
/** /**
@ -30,7 +30,7 @@ export default class SongPlaylist {
* @returns {PlaylistSong} the song at the given index. * @returns {PlaylistSong} the song at the given index.
*/ */
songAtIndex(index) { songAtIndex(index) {
if (index >= this._list.length) { if (index >= this.#list.length) {
return null; return null;
} }
return this.list[index]; return this.list[index];
@ -44,7 +44,7 @@ export default class SongPlaylist {
* @param {string} author the author(s) of the song. * @param {string} author the author(s) of the song.
*/ */
add(url, name, author) { add(url, name, author) {
this._list.push(new PlaylistSong(url, name, author, this)); this.#list.push(new PlaylistSong(url, name, author, this));
} }
/** /**
@ -54,10 +54,10 @@ export default class SongPlaylist {
* @returns {PlaylistSong} the song that was removed, or null if the index of was invalid. * @returns {PlaylistSong} the song that was removed, or null if the index of was invalid.
*/ */
remove(index) { remove(index) {
if (index >= this._list.length) { if (index >= this.#list.length) {
return null; return null;
} }
let removed = this._list.splice(index, 1)[0]; let removed = this.#list.splice(index, 1)[0];
if (removed.length > 0) { if (removed.length > 0) {
return removed[0]; return removed[0];
} }
@ -70,7 +70,7 @@ export default class SongPlaylist {
* @returns {number} the index of the song found, or -1 if it was not found. * @returns {number} the index of the song found, or -1 if it was not found.
*/ */
findSongIndex(name) { findSongIndex(name) {
return this._list.findIndex((item) => item.getDisplayName() == name); return this.#list.findIndex((item) => item.getDisplayName() == name);
} }
/** /**
@ -78,14 +78,14 @@ export default class SongPlaylist {
* @returns {number} total number of songs in this playlist. * @returns {number} total number of songs in this playlist.
*/ */
total() { total() {
return this._list.length; return this.#list.length;
} }
/** /**
* Unloads the audio data of all songs in this playlist. * Unloads the audio data of all songs in this playlist.
*/ */
unloadAllAudio() { unloadAllAudio() {
this._list.forEach(playlistSong => { this.#list.forEach(playlistSong => {
playlistSong.unloadAudio(); playlistSong.unloadAudio();
}); });
} }

View File

@ -9,6 +9,8 @@ import SongPlayer from "./SongPlayer.js";
* Automatically loads the songs. * Automatically loads the songs.
*/ */
export default class VisualizedSongPlayer extends SongPlayer { export default class VisualizedSongPlayer extends SongPlayer {
#fftSize = 1024;
#visualizerUpdateManager;
/** /**
* Instantiates a song player with visualization features. * Instantiates a song player with visualization features.
@ -18,18 +20,16 @@ export default class VisualizedSongPlayer extends SongPlayer {
*/ */
constructor(playlist, fftSize = 1024) { constructor(playlist, fftSize = 1024) {
super(playlist); super(playlist);
this._fftSize = fftSize; this.#fftSize = fftSize;
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer()); this.#visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
} }
/** /**
* Sets the the playlist. * Sets the the playlist.
*
* @param {SongPlaylist} playlist the new playlist of songs that this player is in charge of.
*/ */
setPlaylist(playlist) { set playlist(playlist) {
super.setPlaylist(playlist); super.playlist = playlist;
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer()); this.#visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
} }
/** /**
@ -37,10 +37,10 @@ export default class VisualizedSongPlayer extends SongPlayer {
* @returns {boolean} true if and only if successful in changing to the next song. * @returns {boolean} true if and only if successful in changing to the next song.
*/ */
next() { next() {
const updateListeners = this._visualizerUpdateManager.getBinnedListeners(); const updateListeners = this.#visualizerUpdateManager.getBinnedListeners();
if (!super.next()) return false; if (!super.next()) return false;
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer()); this.#visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
this._visualizerUpdateManager.setBinnedListeners(updateListeners); this.#visualizerUpdateManager.setBinnedListeners(updateListeners);
return true; return true;
} }
@ -50,11 +50,11 @@ export default class VisualizedSongPlayer extends SongPlayer {
* @returns {boolean} true if and only if successful in switching to the previous song in the playlist. * @returns {boolean} true if and only if successful in switching to the previous song in the playlist.
*/ */
previous() { previous() {
const updateListeners = this._visualizerUpdateManager.getBinnedListeners(); const updateListeners = this.#visualizerUpdateManager.getBinnedListeners();
if (!super.previous()) return false; if (!super.previous()) return false;
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer()); this.#visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
this._visualizerUpdateManager.setBinnedListeners(updateListeners); this.#visualizerUpdateManager.setBinnedListeners(updateListeners);
return true; return true;
} }
@ -66,24 +66,22 @@ export default class VisualizedSongPlayer extends SongPlayer {
changeCurrent(index) { changeCurrent(index) {
const updateListeners = this.VisualizerUpdateManager.getBinnedListeners(); const updateListeners = this.VisualizerUpdateManager.getBinnedListeners();
if (!super.changeCurrent(index)) return false; if (!super.changeCurrent(index)) return false;
this._visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer()); this.#visualizerUpdateManager = new VisualizerUpdateManager(this.getCurrentSongVisualizer());
this._visualizerUpdateManager.setBinnedListeners = updateListeners(); this.#visualizerUpdateManager.setBinnedListeners = updateListeners();
return true; return true;
} }
/** /**
* * The current visualizer update manager.
* @returns {VisualizerUpdateManager} the current visualizer update manager.
*/ */
getCurrentVisualizerUpdateManager() { get currentVisualizerUpdateManager() {
return this._visualizerUpdateManager(); return this.#visualizerUpdateManager();
} }
/** /**
* * The current song's visualizer.
* @returns {Visualizer} the current song's visualizer.
*/ */
getCurrentSongVisualizer() { get currentSongVisualizer() {
return this.getCurrentSong().getVisualizer(); return this.getCurrentSong().getVisualizer();
} }
} }

View File

@ -2,6 +2,14 @@
* Provides a simplified access point to the frequency bins in the form of a visualization update listener. * Provides a simplified access point to the frequency bins in the form of a visualization update listener.
*/ */
export default class Visualizer { export default class Visualizer {
#stream;
#analyzing = false;
#updateListeners = [];
#audioCtx;
#source;
#analyzer;
#buffer;
#lastUpdate;
/** /**
* @callback visualizerUpdateListener * @callback visualizerUpdateListener
@ -15,22 +23,22 @@ export default class Visualizer {
* @param {number} [fftSize = 1024] the size of the fft window. * @param {number} [fftSize = 1024] the size of the fft window.
*/ */
constructor(mediaSource, fftSize = 1024) { constructor(mediaSource, fftSize = 1024) {
this._stream = mediaSource; this.#stream = mediaSource;
this._analyzing = false; this.#analyzing = false;
this._updateListeners = []; this.#updateListeners = [];
this._audioCtx = new window.AudioContext(); this.#audioCtx = new window.AudioContext();
if (mediaSource instanceof HTMLMediaElement) { if (mediaSource instanceof HTMLMediaElement) {
this._source = this._audioCtx.createMediaElementSource(this._stream); this.#source = this.#audioCtx.createMediaElementSource(this.#stream);
} else { } else {
this._source = this._audioCtx.createMediaStreamSource(this._stream); this.#source = this.#audioCtx.createMediaStreamSource(this.#stream);
} }
this._analyzer = this._audioCtx.createAnalyser(); this.#analyzer = this.#audioCtx.createAnalyser();
this._analyzer.fftSize = fftSize; this.#analyzer.fftSize = fftSize;
this._buffer = new Uint8Array(this._analyzer.frequencyBinCount); this.#buffer = new Uint8Array(this.#analyzer.frequencyBinCount);
this._source.connect(this._analyzer); this.#source.connect(this.#analyzer);
this._analyzer.connect(this._audioCtx.destination); this.#analyzer.connect(this.#audioCtx.destination);
this.lastUpdate = null; this.#lastUpdate = null;
} }
@ -38,22 +46,22 @@ export default class Visualizer {
* Begins analyzing and sending out update pings. * Begins analyzing and sending out update pings.
*/ */
analyze() { analyze() {
if (this._analyzing) { if (this.#analyzing) {
return; return;
} }
this._analyzing = true; this.#analyzing = true;
let self = this; // since calling from requestAnimationFrame means "this" is no longer set to produced object. let self = this; // since calling from requestAnimationFrame means "this" is no longer set to produced object.
const update = (timestamp) => { const update = (timestamp) => {
if (!self._analyzing) return; if (!self.#analyzing) return;
if (!self.lastUpdate) { if (!self.#lastUpdate) {
self.lastUpdate = timestamp; self.#lastUpdate = timestamp;
} }
let delta = timestamp - self.lastUpdate; let delta = timestamp - self.#lastUpdate;
self._analyzer.getByteFrequencyData(self._buffer); self.#analyzer.getByteFrequencyData(self.#buffer);
self._updateListeners.forEach(listener => { self.#updateListeners.forEach(listener => {
listener(delta, self._buffer); listener(delta, self.#buffer);
}); });
requestAnimationFrame(update); requestAnimationFrame(update);
}; };
@ -64,7 +72,7 @@ export default class Visualizer {
* Stops the analysis. Listeners will stop receiving bins. * Stops the analysis. Listeners will stop receiving bins.
*/ */
stop() { stop() {
this._analyzing = false; this.#analyzing = false;
} }
/** /**
@ -73,8 +81,8 @@ export default class Visualizer {
* @returns {boolean} true if and only if the listener was successfully added. * @returns {boolean} true if and only if the listener was successfully added.
*/ */
addUpdateListener(listener) { addUpdateListener(listener) {
if (this._updateListeners.includes(listener)); if (this.#updateListeners.includes(listener));
this._updateListeners.push(listener); this.#updateListeners.push(listener);
return true; return true;
} }
@ -84,27 +92,26 @@ export default class Visualizer {
* @returns {boolean} true if and only if the removal of the listener was a success. * @returns {boolean} true if and only if the removal of the listener was a success.
*/ */
removeUpdateListener(listener) { removeUpdateListener(listener) {
const removeIndex = this._updateListeners.indexOf(listener); const removeIndex = this.#updateListeners.indexOf(listener);
if (removeIndex < 0) return false; if (removeIndex < 0) return false;
this._updateListeners.splice(removeIndex, 1); this.#updateListeners.splice(removeIndex, 1);
return true; return true;
} }
/** /**
* * The number of bins based on the size of the FFT window.
* @returns {number} the number of bins based on the size of the FFT window.
*/ */
getNumberOfBins() { get numberOfBins() {
return this._buffer.length; return this.#buffer.length;
} }
/** /**
* *
* @returns {number} the fft window size. * The fft window size.
*/ */
getFftSize() { get fftSize() {
return this._analyzer.fftSize; return this.#analyzer.fftSize;
} }
} }

View File

@ -5,24 +5,31 @@
* In the rare event that a bin has not changed, then it will not receive an update call. * In the rare event that a bin has not changed, then it will not receive an update call.
*/ */
export default class VisualizerUpdateManager { export default class VisualizerUpdateManager {
#binnedListeners = [];
#rangedListeners = [];
#lastBins;
#visualizer;
#visualizerListener;
/** /**
* *
* @param {Visualizer} visualizer the visualizer this manager obtains data from. * @param {Visualizer} visualizer the visualizer this manager obtains data from.
*/ */
constructor(visualizer) { constructor(visualizer) {
this._binnedListeners = []; this.#binnedListeners = [];
this._rangedListeners = []; this.#rangedListeners = [];
for (let i = 0; i < visualizer.getNumberOfBins(); i++) { for (let i = 0; i < visualizer.getNumberOfBins(); i++) {
this._binnedListeners.push([]); this.#binnedListeners.push([]);
} }
this._lastBins = new Uint8Array(this._binnedListeners.length); this.#lastBins = new Uint8Array(this.#binnedListeners.length);
this._visualizer = visualizer; this.#visualizer = visualizer;
this._visualizerListener = (delta, bins) => { this.#visualizerListener = (delta, bins) => {
const sortedCopyOfRangedListeners = [... this._rangedListeners]; // We assume this is sorted properly. A priority queue could be better. 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++) { for (let binInd = 0; binInd < this.#lastBins.length; binInd++) {
const lastBin = this._lastBins[binInd]; const lastBin = this.#lastBins[binInd];
if (lastBin !== bins[binInd]) { if (lastBin !== bins[binInd]) {
this._binnedListeners[binInd].forEach(listener => { this.#binnedListeners[binInd].forEach(listener => {
listener(delta, bins[binInd], bins[binInd] - lastBin); listener(delta, bins[binInd], bins[binInd] - lastBin);
}); });
for (let rangedInd = 0; rangedInd < sortedCopyOfRangedListeners.length; rangedInd++) { // Could switch to a while loop. for (let rangedInd = 0; rangedInd < sortedCopyOfRangedListeners.length; rangedInd++) { // Could switch to a while loop.
@ -34,22 +41,22 @@ export default class VisualizerUpdateManager {
rangedInd--; rangedInd--;
} }
} }
this._lastBins[binInd] = bins[binInd]; this.#lastBins[binInd] = bins[binInd];
} }
} }
}; };
visualizer.addUpdateListener(this._visualizerListener); visualizer.addUpdateListener(this.#visualizerListener);
} }
/** /**
* @callback VisualizerUpdateManager.visualizerBinUpdateListener * @typedef {function} visualizerBinUpdateListener
* @param {number} timeDelta elapsed time since last update. * @param {number} timeDelta elapsed time since last update.
* @param {number} amplitude The amplitude of the associated bin. * @param {number} amplitude The amplitude of the associated bin.
* @param {number} ampDelta change in amplitude of the frequency bin. * @param {number} ampDelta change in amplitude of the frequency bin.
*/ */
/** /**
* @callback VisualizerUpdateManager.visualizerRangedUpdateListener * @typedef {function} visualizerRangedUpdateListener
* @param {number} timeDelta elapsed time since last update. * @param {number} timeDelta elapsed time since last update.
* @param {number} bins the bins of the range. * @param {number} bins the bins of the range.
*/ */
@ -60,12 +67,12 @@ export default class VisualizerUpdateManager {
* *
* @param {object} freqBinListener the listener for 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 {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. * @param {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. * @returns {boolean} true if and only if the updater was added successfully.
*/ */
AddVisualizerBinUpdateListener({ freqBin, listener }) { AddVisualizerBinUpdateListener({ freqBin, listener }) {
if (this._binnedListeners[freqBin].includes(listener)) return false; if (this.#binnedListeners[freqBin].includes(listener)) return false;
this._binnedListeners[freqBin].push(listener); this.#binnedListeners[freqBin].push(listener);
return true; return true;
} }
@ -75,7 +82,7 @@ export default class VisualizerUpdateManager {
* @param {object} rangedUpdateListener The ranged update listener to add. * @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.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 {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. * @param {visualizerRangedUpdateListener} rangedUpdateListener.listener The listener to register to the range.
* @returns {boolean} True if and only if the ranged listener was added successfully. * @returns {boolean} True if and only if the ranged listener was added successfully.
*/ */
addVisualizerRangedUpdateListener({ lower, upper, listener }) { addVisualizerRangedUpdateListener({ lower, upper, listener }) {
@ -84,9 +91,9 @@ export default class VisualizerUpdateManager {
upper: upper, upper: upper,
listener: listener listener: listener
}; };
if (this._rangedListeners.includes(rangedListener)) return false; if (this.#rangedListeners.includes(rangedListener)) return false;
this._rangedListeners.push(rangedListener); this.#rangedListeners.push(rangedListener);
this._rangedListeners.sort((a, b) => a.lower - b.lower); this.#rangedListeners.sort((a, b) => a.lower - b.lower);
return true; return true;
} }
@ -94,13 +101,13 @@ export default class VisualizerUpdateManager {
* *
* @param {object} binFreqListener The bin frequency listener to remove. * @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 {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. * @param {visualizerBinUpdateListener} binFreqListener.listener the listener that is to be removed.
* @returns {boolean} true if and only if the listener was successfully removed. * @returns {boolean} true if and only if the listener was successfully removed.
*/ */
removeVisualizerBinUpdateListener({ freqBin, listener }) { removeVisualizerBinUpdateListener({ freqBin, listener }) {
const removeIndex = this._binnedListeners[freqBin].indexOf(listener); const removeIndex = this.#binnedListeners[freqBin].indexOf(listener);
if (removeIndex < 0) return false; if (removeIndex < 0) return false;
this._binnedListeners[freqBin].splice(removeIndex, 1); this.#binnedListeners[freqBin].splice(removeIndex, 1);
return true; return true;
} }
@ -110,7 +117,7 @@ export default class VisualizerUpdateManager {
* @param {object} rangedListener The ranged listener to remove. * @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.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 {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. * @param {visualizerRangedUpdateListener} rangedListener.listener The update listener to remove from the bins.
* @returns {boolean} True if and only if the given listener was removed. * @returns {boolean} True if and only if the given listener was removed.
*/ */
removeVisualizerRangedUpdateListener({ lower, upper, listener }) { removeVisualizerRangedUpdateListener({ lower, upper, listener }) {
@ -119,36 +126,36 @@ export default class VisualizerUpdateManager {
upper: upper, upper: upper,
listener: listener listener: listener
}; };
const removeIndex = this._rangedListeners.indexOf(rangedListener); const removeIndex = this.#rangedListeners.indexOf(rangedListener);
if (removeIndex < 0) return false; if (removeIndex < 0) return false;
this._binnedListeners.splice(removeIndex, 1); this.#binnedListeners.splice(removeIndex, 1);
return true; return true;
} }
/** /**
* @param {object} listeners The listeners that this visualizer update manager should be set to use. * @param {object} listeners The listeners that this visualizer update manager should be set to use.
* @param {VisualizerUpdateManager.visualizerBinUpdateListener[][]} listeners.binned The bin listeners. * @param {visualizerBinUpdateListener[][]} listeners.binned The bin listeners.
* @param {object[]} listeners.ranged The array of ranged 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[].lower The lower bound of the listener.
* @param {number} listeners.ranged[].upper The upper 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. * @param {visualizerRangedUpdateListener} listeners.ranged[].listener The listener for the previously mentioned ranges.
* @returns {boolean} true if and only if successfully loaded the new listeners. * @returns {boolean} true if and only if successfully loaded the new listeners.
*/ */
setBinnedListeners(listeners) { loadListeners(listeners) {
if (listeners.binned.length !== this._binnedListeners.length) return false; if (listeners.binned.length !== this.#binnedListeners.length) return false;
this._binnedListeners = listeners.binned; this.#binnedListeners = listeners.binned;
this._rangedListeners = listeners.ranged.sort((a, b) => a.lower - b.lower); this.#rangedListeners = listeners.ranged.sort((a, b) => a.lower - b.lower);
return true; 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. * @returns {object} All the listeners, both for binned, and ranged listeners. See {@link VisualizerUpdateManager#loadListeners} to see the structure of the returned object.
*/ */
getAllListeners() { retrieveListeners() {
return { return {
binned: this._binnedListeners, binned: this.#binnedListeners,
ranged: this._rangedListeners ranged: this.#rangedListeners
}; };
} }
@ -156,7 +163,7 @@ export default class VisualizerUpdateManager {
* Clears this manager of all listeners. * Clears this manager of all listeners.
*/ */
clearBinnedListeners() { clearBinnedListeners() {
this._binnedListeners.forEach(bin => { this.#binnedListeners.forEach(bin => {
bin.length = 0; bin.length = 0;
}); });
} }
@ -165,6 +172,6 @@ export default class VisualizerUpdateManager {
* Unbinds this update manager from the initial visualizer. Effectively meaning this manager will no longer be used. * Unbinds this update manager from the initial visualizer. Effectively meaning this manager will no longer be used.
*/ */
unbindVisualizer() { unbindVisualizer() {
this._visualizer.removeUpdateListener(this._visualizerListener); this.#visualizer.removeUpdateListener(this.#visualizerListener);
} }
} }